Desafio 04: ISO 8583 — Mensagens Binárias de Autorização Financeira
🇧🇷 Simulador de Mensagens Financeiras Binárias
🇬🇧 ISO 8583 Financial Message Simulator
O ISO 8583 é o padrão internacional para mensagens de transações com cartão. É o protocolo que conecta maquininhas (POS), adquirentes (Cielo, Rede, Stone), bandeiras (Visa, Mastercard) e bancos emissores. Todo swipe, tap ou inserção de chip passa por ISO 8583.
Switch: TypeScript vs Go
O que é ISO 8583?
| Característica | Descrição |
|---|---|
| Binário | Dados em bytes, não JSON/XML |
| TCP puro | Sem HTTP, sem overhead |
| Bitmaps | Indicam quais campos estão presentes |
| Baixa latência | Milissegundos por transação |
| MAC/Criptografia | 3DES, AES, RSA |
| Conexões persistentes | Sessões longas |
Estrutura de uma Mensagem ISO 8583
┌─────────────┬──────────────┬────────────────┬──────────┐
│ MTI (4B) │ Bitmap (8B) │ Data Elements │ MAC (8B) │
│ ASCII │ Primary │ Variável │ Binário │
└─────────────┴──────────────┴────────────────┴──────────┘| Componente | Tamanho | Descrição |
|---|---|---|
| MTI | 4 bytes | Message Type Indicator (0100, 0200, etc) |
| Bitmap | 8 ou 16 bytes | Bits indicando DEs presentes |
| Data Elements | Variável | Campos de dados (PAN, valor, etc) |
| MAC | 8 bytes | Message Authentication Code |
MTIs Principais
| MTI | Nome | Uso |
|---|---|---|
| 0100/0110 | Authorization Request/Response | Pré-autorização |
| 0200/0210 | Financial Request/Response | Compra efetiva |
| 0400/0410 | Reversal Request/Response | Cancelamento |
| 0800/0810 | Network Management Request/Response | Heartbeat, login |
Bitmaps: O Coração do ISO 8583
O bitmap é uma sequência de bits que indica quais Data Elements (DE) estão presentes. Se o bit N está setado (1), o DE-N está presente.
| Bit | DE | Nome |
|---|---|---|
| 2 | DE-2 | PAN (número do cartão) |
| 3 | DE-3 | Processing Code |
| 4 | DE-4 | Amount Transaction |
| 7 | DE-7 | Date/Time |
| 11 | DE-11 | STAN |
| 14 | DE-14 | Date Expiration |
| 22 | DE-22 | POS Entry Mode |
| 35 | DE-35 | Track 2 Data |
| 38 | DE-38 | Auth ID Response |
| 39 | DE-39 | Response Code |
| 41 | DE-41 | Terminal ID |
| 42 | DE-42 | Merchant ID |
| 52 | DE-52 | PIN Data |
| 55 | DE-55 | ICC Data (EMV) |
Arquitetura do Simulador
Message Parser e Builder
typescript
export class ISO8583Message {
public mti: string;
public bitmap: Bitmap;
public dataElements: Map<number, DataElement>;
constructor(mti: string) {
this.mti = mti;
this.bitmap = new Bitmap();
this.dataElements = new Map();
}
public setDE(de: number, value: any, type: DataElementType): void {
if (de > 64) this.bitmap.set(1);
this.bitmap.set(de);
this.dataElements.set(de, new DataElement(de, value, type));
}
public hasDE(de: number): boolean {
return this.bitmap.isSet(de);
}
public toBuffer(): Buffer {
const parts: Buffer[] = [];
parts.push(Buffer.from(this.mti, 'ascii'));
parts.push(this.bitmap.toPrimaryBuffer());
if (this.bitmap.isSet(1)) parts.push(this.bitmap.toSecondaryBuffer());
const sortedDEs = Array.from(this.dataElements.keys()).sort((a, b) => a - b);
for (const de of sortedDEs) {
parts.push(this.dataElements.get(de)!.toBuffer());
}
return Buffer.concat(parts);
}
public static fromBuffer(buffer: Buffer): ISO8583Message {
let offset = 0;
const mti = buffer.slice(offset, offset + 4).toString('ascii');
offset += 4;
const message = new ISO8583Message(mti);
const primaryBitmap = buffer.slice(offset, offset + 8);
message.bitmap.fromPrimaryBuffer(primaryBitmap);
offset += 8;
if (message.bitmap.isSet(1)) {
const secondaryBitmap = buffer.slice(offset, offset + 8);
message.bitmap.fromSecondaryBuffer(secondaryBitmap);
offset += 8;
}
for (const de of message.bitmap.getSetBits().filter(b => b > 1)) {
const type = DataElement.getTypeForDE(de);
const element = DataElement.fromBuffer(buffer, offset, de, type);
message.dataElements.set(de, element);
offset += element.getByteLength();
}
return message;
}
}Bitmap — Manipulação de Bits
typescript
export class Bitmap {
private primary: bigint;
private secondary: bigint;
public set(bit: number): void {
if (bit <= 64) {
const position = BigInt(64 - bit);
this.primary |= (1n << position);
} else {
const position = BigInt(128 - bit);
this.secondary |= (1n << position);
}
}
public isSet(bit: number): boolean {
if (bit <= 64) {
return (this.primary & (1n << BigInt(64 - bit))) !== 0n;
}
return (this.secondary & (1n << BigInt(128 - bit))) !== 0n;
}
public toPrimaryBuffer(): Buffer {
const buffer = Buffer.alloc(8);
buffer.writeBigUInt64BE(this.primary);
return buffer;
}
public getSetBits(): number[] {
const bits: number[] = [];
for (let i = 1; i <= 64; i++) {
if (this.isSet(i)) bits.push(i);
}
if (this.isSet(1)) {
for (let i = 65; i <= 128; i++) {
if (this.isSet(i)) bits.push(i);
}
}
return bits;
}
}Data Elements — Tipos de Campos
typescript
export enum DataElementType {
FIXED = 'FIXED',
LLVAR = 'LLVAR',
LLLVAR = 'LLLVAR',
}
export class DataElement {
public static getTypeForDE(de: number): DataElementType {
const spec = ISO8583Spec.getDESpec(de);
return spec.type;
}
public toBuffer(): Buffer {
const spec = ISO8583Spec.getDESpec(this.de);
switch (this.type) {
case DataElementType.FIXED:
return Buffer.from(String(this.value).padStart(spec.length, '0'), 'ascii');
case DataElementType.LLVAR: {
const len = String(this.value).length.toString().padStart(2, '0');
return Buffer.from(len + this.value, 'ascii');
}
case DataElementType.LLLVAR: {
const len = String(this.value).length.toString().padStart(3, '0');
return Buffer.from(len + this.value, 'ascii');
}
}
}
}TCP Server — Comunicação de Baixo Nível
typescript
import * as net from 'net';
export class ISO8583TCPServer {
private server: net.Server;
private connections: Map<string, net.Socket> = new Map();
public start(): Promise<void> {
return new Promise((resolve) => {
this.server = net.createServer((socket) => this.handleConnection(socket));
this.server.listen(this.config.port, this.config.host, resolve);
});
}
private handleConnection(socket: net.Socket): void {
const connectionId = `${socket.remoteAddress}:${socket.remotePort}`;
this.connections.set(connectionId, socket);
let messageBuffer = Buffer.alloc(0);
socket.on('data', (data) => {
messageBuffer = Buffer.concat([messageBuffer, data]);
while (messageBuffer.length > 0) {
const msgLength = messageBuffer.readUInt16BE(0);
if (messageBuffer.length < 2 + msgLength) break;
const msgData = messageBuffer.slice(2, 2 + msgLength);
messageBuffer = messageBuffer.slice(2 + msgLength);
const isoMessage = ISO8583Message.fromBuffer(msgData);
this.messageRouter.route(isoMessage, session).then((response) => {
if (response) socket.write(response.toBuffer());
});
}
});
}
}Authorization Handler
typescript
export class AuthorizationHandler implements MessageHandler {
public async handle(message: ISO8583Message, session: Session): Promise<ISO8583Message> {
const pan = message.getDEValue(2);
const amount = parseInt(message.getDEValue(4), 10);
const terminalId = message.getDEValue(41);
const merchantId = message.getDEValue(42);
const card = await this.cardRepo.findByPAN(pan);
if (!card) return this.buildResponse(message, '14', 'Invalid card');
if (card.status !== 'ACTIVE') return this.buildResponse(message, '62', 'Restricted');
if (message.hasDE(52)) {
const pinValid = await this.hsm.verifyPIN(message.getDEValue(52), card.pan, card.pinBlock);
if (!pinValid) return this.buildResponse(message, '55', 'Incorrect PIN');
}
const fraudCheck = await this.fraudService.check({ pan, amount, merchantId, terminalId });
if (fraudCheck.isHighRisk) return this.buildResponse(message, '05', 'Do not honor');
const balance = await this.cardRepo.getAvailableBalance(card.id);
if (balance < amount) return this.buildResponse(message, '51', 'Insufficient funds');
const authCode = this.generateAuthCode();
const rrn = this.generateRRN();
await this.cardRepo.reserveFunds(card.id, amount, rrn);
const response = this.buildResponse(message, '00', 'Approved');
response.setDE(37, rrn, DataElementType.FIXED);
response.setDE(38, authCode, DataElementType.FIXED);
return response;
}
private generateRRN(): string {
const now = new Date();
const julianDay = this.getJulianDay(now).toString().padStart(3, '0');
const seq = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
return julianDay + now.getHours().toString().padStart(2, '0') + seq;
}
}Fluxo Completo: Transação de Cartão
Response Codes Mais Comuns
| Código | Significado |
|---|---|
| 00 | Approved |
| 05 | Do not honor |
| 14 | Invalid card number |
| 30 | Format error |
| 51 | Insufficient funds |
| 54 | Expired card |
| 55 | Incorrect PIN |
| 62 | Restricted card |
| 91 | Issuer unavailable |
| 96 | System malfunction |
Comparação: TypeScript vs Go
| Aspecto | TypeScript | Go |
|---|---|---|
| Parse binário | Buffer API (ok) | encoding/binary (nativo) |
| TCP Server | net module (bom) | net package (otimizado) |
| Bitmap | BigInt (funcional) | uint64 (zero overhead) |
| Memória/conexão | ~5MB | ~0.5MB |
| Latência parse | 5-20ms | 0.5-2ms |
| Throughput | ~5K msg/s | ~50K msg/s |
| Crypto | node crypto (ok) | stdlib (AES-NI hardware) |
Casos Reais no Brasil
- Stone (Go) — 5M+ maquininhas, P99 < 3ms, 50K TPS
- Cielo (Java + Go) — 6M+ estabelecimentos, multi-network
- Rede/Itaú (Java) — Stack enterprise, HSM Thales
- PagSeguro (Híbrido) — 30M+ clientes, TypeScript + Java + Go
Boas Práticas
Faça:
- Connection pooling e heartbeats
- Idempotência com STAN + RRN
- Audit trail de todas as mensagens
- Circuit breakers para emissores
- PCI-DSS compliance
Evite:
- Logar PANs (viola PCI-DSS)
- Logar PINs (jamais)
- Chaves em código (use HSM)
- Timeouts longos
- Conexões sem heartbeat
Como testar
bash
# TypeScript
cd packages/backend/iso8583
pnpm dev
# Go
cd packages/backend/iso8583-go
go run .
# Enviar mensagem de teste
echo -n "0100..." | nc localhost 3004Lições aprendidas
- Bitmaps são o coração — Sem entender bitmaps, não se entende ISO 8583
- Binário é preciso — Um byte errado = mensagem rejeitada
- Response codes importam — 00=OK, 51=Sem fundos, 55=PIN errado
- RRN é crucial — Referência única para reconciliação
- PCI-DSS não é opcional — Nunca logar PANs ou PINs
- HSM é obrigatório — Criptografia de PINs em hardware
- Go é a escolha natural — 4-8x mais rápido que Node.js
- Heartbeats salvam vidas — Conexões TCP precisam de 0800 periódico
- Circuit breakers — Emissores caem, prepare-se
- O protocolo tem 35+ anos — E continua sendo essencial