Skip to content

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ísticaDescrição
BinárioDados em bytes, não JSON/XML
TCP puroSem HTTP, sem overhead
BitmapsIndicam quais campos estão presentes
Baixa latênciaMilissegundos por transação
MAC/Criptografia3DES, AES, RSA
Conexões persistentesSessões longas

Estrutura de uma Mensagem ISO 8583

┌─────────────┬──────────────┬────────────────┬──────────┐
│  MTI (4B)   │ Bitmap (8B)  │ Data Elements  │ MAC (8B) │
│  ASCII      │  Primary     │  Variável      │ Binário  │
└─────────────┴──────────────┴────────────────┴──────────┘
ComponenteTamanhoDescrição
MTI4 bytesMessage Type Indicator (0100, 0200, etc)
Bitmap8 ou 16 bytesBits indicando DEs presentes
Data ElementsVariávelCampos de dados (PAN, valor, etc)
MAC8 bytesMessage Authentication Code

MTIs Principais

MTINomeUso
0100/0110Authorization Request/ResponsePré-autorização
0200/0210Financial Request/ResponseCompra efetiva
0400/0410Reversal Request/ResponseCancelamento
0800/0810Network Management Request/ResponseHeartbeat, 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.

BitDENome
2DE-2PAN (número do cartão)
3DE-3Processing Code
4DE-4Amount Transaction
7DE-7Date/Time
11DE-11STAN
14DE-14Date Expiration
22DE-22POS Entry Mode
35DE-35Track 2 Data
38DE-38Auth ID Response
39DE-39Response Code
41DE-41Terminal ID
42DE-42Merchant ID
52DE-52PIN Data
55DE-55ICC 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ódigoSignificado
00Approved
05Do not honor
14Invalid card number
30Format error
51Insufficient funds
54Expired card
55Incorrect PIN
62Restricted card
91Issuer unavailable
96System malfunction

Comparação: TypeScript vs Go

AspectoTypeScriptGo
Parse binárioBuffer API (ok)encoding/binary (nativo)
TCP Servernet module (bom)net package (otimizado)
BitmapBigInt (funcional)uint64 (zero overhead)
Memória/conexão~5MB~0.5MB
Latência parse5-20ms0.5-2ms
Throughput~5K msg/s~50K msg/s
Cryptonode 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 3004

Lições aprendidas

  1. Bitmaps são o coração — Sem entender bitmaps, não se entende ISO 8583
  2. Binário é preciso — Um byte errado = mensagem rejeitada
  3. Response codes importam — 00=OK, 51=Sem fundos, 55=PIN errado
  4. RRN é crucial — Referência única para reconciliação
  5. PCI-DSS não é opcional — Nunca logar PANs ou PINs
  6. HSM é obrigatório — Criptografia de PINs em hardware
  7. Go é a escolha natural — 4-8x mais rápido que Node.js
  8. Heartbeats salvam vidas — Conexões TCP precisam de 0800 periódico
  9. Circuit breakers — Emissores caem, prepare-se
  10. O protocolo tem 35+ anos — E continua sendo essencial