<?php
/**
 * Classe SpbSeguranca
 * 
 * Implementa o protocolo de segurança SPB.
 * Suporta versão 2 (3DES-EDE-CBC) compatível com o exemplo Java legado
 * e versão 3 (AES-256-GCM).
 * 
 * Cabeçalho de 588 bytes (seção 8.6 do manual):
 *   C01 (001-002): Tamanho do cabeçalho = 0x024C (588 bytes)
 *   C02 (003):     Versão do protocolo (0x02 = v2, 0x03 = v3)
 *   C03 (004):     Código de erro = 0x00
 *   C04 (005):     Tratamento especial = 0x08 no fluxo Java legado
 *   C05 (006):     Reservado = 0x00
 *   C06 (007):     Algoritmo assimétrico destino = 0x02 (RSA 2048)
 *   C07 (008):     Algoritmo simétrico (0x01 = 3DES, 0x02 = AES-256)
 *   C08 (009):     Algoritmo assimétrico local = 0x02 (RSA 2048)
 *   C09 (010):     Algoritmo hash = 0x03 (SHA-256)
 *   C10 (011):     PC certificado destino
 *   C11 (012-043): Série certificado destino (32 bytes ASCII hex)
 *   C12 (044):     PC certificado emissor
 *   C13 (045-076): Série certificado emissor (32 bytes ASCII hex)
 *   C14 (077-332): Buffer criptografia chave simétrica (256 bytes)
 *   C15 (333-588): Buffer assinatura (256 bytes)
 */
class SpbSeguranca
{
    const TAMANHO_CABECALHO = 588;

    // Versões do protocolo
    const VERSAO_2 = 0x02; // 3DES
    const VERSAO_3 = 0x03; // AES-256-GCM

    // Algoritmos
    const ALG_RSA_2048 = 0x02;
    const ALG_3DES     = 0x01;
    const ALG_AES256   = 0x02;
    const ALG_SHA256   = 0x03;

    // Códigos de AC (campo C10/C12)
    const AC_SERPRO    = 0x01;
    const AC_CERTISIGN = 0x02;
    const AC_SERASA    = 0x04;
    const AC_CAIXA     = 0x05;
    const AC_VALID     = 0x06;
    const AC_SOLUTI    = 0x07;

    private $chavePrivadaEmissor;
    private $chavePublicaDestinatario;
    private $codigoAcEmissor;
    private $serialEmissor;
    private $codigoAcDestinatario;
    private $serialDestinatario;
    private $versaoProtocolo;
    private $tratamentoEspecial;

    /**
     * Cria instância a partir de arquivos PEM.
     */
    public function __construct($chavePrivada, $certDestinatario, $codigoAcEmissor = self::AC_VALID, $codigoAcDestinatario = self::AC_VALID, $versao = self::VERSAO_3, $tratamentoEspecial = null)
    {
        $this->codigoAcEmissor = $codigoAcEmissor;
        $this->codigoAcDestinatario = $codigoAcDestinatario;
        $this->versaoProtocolo = $versao;
        $this->tratamentoEspecial = $tratamentoEspecial === null
            ? ($versao === self::VERSAO_2 ? 0x08 : 0x00)
            : $tratamentoEspecial;
        $this->serialEmissor = str_repeat('0', 32);

        // Chave privada (pode ser recurso OpenSSL ou string PEM)
        if (is_string($chavePrivada) && strpos($chavePrivada, '-----') !== false) {
            $this->chavePrivadaEmissor = openssl_pkey_get_private($chavePrivada);
        } else {
            $this->chavePrivadaEmissor = $chavePrivada;
        }
        if ($this->chavePrivadaEmissor === false) {
            throw new Exception("Erro ao carregar chave privada: " . openssl_error_string());
        }

        // Certificado do destinatário
        if (is_string($certDestinatario) && strpos($certDestinatario, '-----') !== false) {
            $pemCert = $certDestinatario;
        } else {
            $pemCert = $certDestinatario;
        }
        $certInfo = openssl_x509_parse($pemCert);
        if ($certInfo === false) {
            throw new Exception("Erro ao parsear certificado destinatario: " . openssl_error_string());
        }
        $this->serialDestinatario = strtoupper($certInfo['serialNumberHex']);

        $pubKey = openssl_pkey_get_public($pemCert);
        if ($pubKey === false) {
            throw new Exception("Erro ao extrair chave publica: " . openssl_error_string());
        }
        $this->chavePublicaDestinatario = $pubKey;
    }

    /**
     * Cria instância a partir de um arquivo PFX (PKCS#12) + certificado PEM do destinatário.
     */
    public static function fromPfx($caminhoPfx, $senhaPfx, $caminhoCertDestinatario, $codigoAcEmissor = self::AC_VALID, $codigoAcDestinatario = self::AC_VALID, $versao = self::VERSAO_3, $tratamentoEspecial = null)
    {
        $pfxData = file_get_contents($caminhoPfx);
        if ($pfxData === false) {
            throw new Exception("Erro ao ler PFX: $caminhoPfx");
        }

        $certs = [];
        if (!openssl_pkcs12_read($pfxData, $certs, $senhaPfx)) {
            throw new Exception("Erro ao abrir PFX (senha incorreta?): " . openssl_error_string());
        }

        // Ler certificado do destinatário
        $certDestPem = file_get_contents($caminhoCertDestinatario);
        if ($certDestPem === false) {
            throw new Exception("Erro ao ler certificado destinatario: $caminhoCertDestinatario");
        }

        $instancia = new self($certs['pkey'], $certDestPem, $codigoAcEmissor, $codigoAcDestinatario, $versao, $tratamentoEspecial);

        // Extrair serial do certificado do emissor (do PFX)
        $certEmissorInfo = openssl_x509_parse($certs['cert']);
        if ($certEmissorInfo !== false) {
            $instancia->setSerialEmissor($certEmissorInfo['serialNumberHex']);
        }

        return $instancia;
    }

    public function setSerialEmissor($serialHex)
    {
        $this->serialEmissor = str_pad(strtoupper($serialHex), 32, '0', STR_PAD_LEFT);
    }

    /**
     * Assina e criptografa conteúdo XML gerando arquivo SPB.
     * 
     * @param string $conteudoXml Conteúdo XML
     * @return string Binário do arquivo SPB (588 bytes cabeçalho + dados cifrados)
     */
    public function assinarECriptografar($conteudoXml)
    {
        // 1. Compactar GZIP (RFC 1952)
        $gzipData = gzencode($conteudoXml, 9);
        if ($gzipData === false) {
            throw new Exception("Erro na compactacao GZIP");
        }

        if ($this->versaoProtocolo === self::VERSAO_3) {
            return $this->criptografarV3($gzipData);
        } else {
            return $this->criptografarV2($gzipData);
        }
    }

    /**
     * Protocolo v3: AES-256-GCM (conforme manual seção 8.6 itens iv-viii)
     */
    private function criptografarV3($gzipData)
    {
        // Padding para múltiplo de 16 bytes (AES block size)
        $resto = strlen($gzipData) % 16;
        if ($resto > 0) {
            $gzipData .= str_repeat("\x00", 16 - $resto);
        }

        // Assinar com SHA-256 + RSA (RSASSA-PKCS1-V1_5-SIGN)
        $assinatura = '';
        if (!openssl_sign($gzipData, $assinatura, $this->chavePrivadaEmissor, OPENSSL_ALGO_SHA256)) {
            throw new Exception("Erro na assinatura: " . openssl_error_string());
        }

        // Gerar chave AES-256 (32 bytes) e IV (12 bytes) — conforme manual item vi
        $chaveAes = openssl_random_pseudo_bytes(32);
        $iv = openssl_random_pseudo_bytes(12); // 96 bits conforme manual

        // Cifrar com AES-256-GCM (AEAD_AES_256_GCM, RFC 5116) — conforme manual item v
        // AD (Associated Data) vazia — conforme manual item vii
        $tag = '';
        $dadosCifrados = openssl_encrypt(
            $gzipData,
            'aes-256-gcm',
            $chaveAes,
            OPENSSL_RAW_DATA,
            $iv,
            $tag,
            '',    // AAD vazia (item vii)
            16     // tag de 128 bits / 16 bytes (item viii)
        );
        if ($dadosCifrados === false) {
            throw new Exception("Erro na criptografia AES-256-GCM: " . openssl_error_string());
        }

        // Tag de autenticação apensada ao final do criptograma (item viii)
        $dadosCifrados .= $tag;

        // Cifrar chave AES + IV com RSA (PKCS#1 v1.5) — conforme manual item C14 v3
        // C14 contém: chave AES (32 bytes) + IV (12 bytes) = 44 bytes, cifrados com RSA
        $materialCripto = $chaveAes . $iv;
        $chaveCifrada = '';
        if (!openssl_public_encrypt($materialCripto, $chaveCifrada, $this->chavePublicaDestinatario, OPENSSL_PKCS1_PADDING)) {
            throw new Exception("Erro ao cifrar chave simetrica: " . openssl_error_string());
        }

        // Montar cabeçalho
        $cabecalho = $this->montarCabecalho($chaveCifrada, $assinatura, self::VERSAO_3, self::ALG_AES256);

        return $cabecalho . $dadosCifrados;
    }

    /**
     * Protocolo v2: 3DES-EDE-CBC compatível com o exemplo Java legado.
     */
    private function criptografarV2($gzipData)
    {
        // Padding para múltiplo de 8 bytes (3DES block size)
        $resto = strlen($gzipData) % 8;
        if ($resto > 0) {
            $gzipData .= str_repeat("\x00", 8 - $resto);
        }

        // Assinar com SHA-256 + RSA
        $assinatura = '';
        if (!openssl_sign($gzipData, $assinatura, $this->chavePrivadaEmissor, OPENSSL_ALGO_SHA256)) {
            throw new Exception("Erro na assinatura: " . openssl_error_string());
        }

        // Gerar chave 3DES (24 bytes), IV = primeiros 8 bytes, como no Java.
        $chave3des = openssl_random_pseudo_bytes(24);
        $iv = substr($chave3des, 0, 8);

        // Cifrar com 3DES-EDE-CBC
        $dadosCifrados = openssl_encrypt(
            $gzipData,
            'des-ede3-cbc',
            $chave3des,
            OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
            $iv
        );
        if ($dadosCifrados === false) {
            throw new Exception("Erro na criptografia 3DES: " . openssl_error_string());
        }

        // Cifrar chave 3DES com RSA
        $chaveCifrada = '';
        if (!openssl_public_encrypt($chave3des, $chaveCifrada, $this->chavePublicaDestinatario, OPENSSL_PKCS1_PADDING)) {
            throw new Exception("Erro ao cifrar chave simetrica: " . openssl_error_string());
        }

        $cabecalho = $this->montarCabecalho($chaveCifrada, $assinatura, self::VERSAO_2, self::ALG_3DES);

        return $cabecalho . $dadosCifrados;
    }

    private function montarCabecalho($bufferChaveSimetrica, $bufferAssinatura, $versao, $algSimetrico)
    {
        $cab = '';
        $cab .= pack('n', self::TAMANHO_CABECALHO);  // C01
        $cab .= chr($versao);                         // C02
        $cab .= chr(0x00);                            // C03 erro
        $cab .= chr($this->tratamentoEspecial);       // C04 tratamento
        $cab .= chr(0x00);                            // C05 reservado
        $cab .= chr(self::ALG_RSA_2048);              // C06
        $cab .= chr($algSimetrico);                   // C07
        $cab .= chr(self::ALG_RSA_2048);              // C08
        $cab .= chr(self::ALG_SHA256);                // C09
        $cab .= chr($this->codigoAcDestinatario);     // C10
        $cab .= str_pad($this->serialDestinatario, 32, '0', STR_PAD_LEFT); // C11
        $cab .= chr($this->codigoAcEmissor);          // C12
        $cab .= str_pad($this->serialEmissor, 32, '0', STR_PAD_LEFT);      // C13
        $cab .= str_pad($bufferChaveSimetrica, 256, "\x00", STR_PAD_LEFT); // C14
        $cab .= str_pad($bufferAssinatura, 256, "\x00", STR_PAD_LEFT);     // C15

        if (strlen($cab) !== self::TAMANHO_CABECALHO) {
            throw new Exception("Cabecalho SPB com tamanho incorreto: " . strlen($cab) . " (esperado: " . self::TAMANHO_CABECALHO . ")");
        }
        return $cab;
    }

    /**
     * Decifra e verifica um arquivo SPB recebido.
     * 
     * @param string $dadosSpb     Conteúdo binário do arquivo SPB
     * @param string $chavePrivada Chave privada PEM do destinatário (para decifrar)
     * @param string $certEmissor  Certificado PEM do emissor (para verificar assinatura)
     * @return string XML original descompactado
     */
    public static function decifrarEVerificar($dadosSpb, $chavePrivada, $certEmissor = null)
    {
        if (strlen($dadosSpb) < self::TAMANHO_CABECALHO) {
            throw new Exception("Arquivo SPB muito pequeno");
        }

        $versao = ord($dadosSpb[2]);
        $c04 = ord($dadosSpb[4]);
        $algSim = ord($dadosSpb[7]);
        $bufferChave = substr($dadosSpb, 76, 256);
        $bufferAssinatura = substr($dadosSpb, 332, 256);
        $payload = substr($dadosSpb, self::TAMANHO_CABECALHO);

        // Decifrar chave simétrica com chave privada
        $privKey = is_string($chavePrivada) ? openssl_pkey_get_private($chavePrivada) : $chavePrivada;
        $materialCripto = '';
        if (!openssl_private_decrypt($bufferChave, $materialCripto, $privKey, OPENSSL_PKCS1_PADDING)) {
            throw new Exception("Erro ao decifrar chave simetrica: " . openssl_error_string());
        }

        if ($versao === self::VERSAO_3 && $algSim === self::ALG_AES256) {
            // AES-256-GCM: material = chave (32) + IV (12)
            $chaveAes = substr($materialCripto, 0, 32);
            $iv = substr($materialCripto, 32, 12);
            // Tag está nos últimos 16 bytes do payload
            $tag = substr($payload, -16);
            $cifrado = substr($payload, 0, -16);

            $decifrado = openssl_decrypt($cifrado, 'aes-256-gcm', $chaveAes, OPENSSL_RAW_DATA, $iv, $tag);
        } else {
            // 3DES-EDE-CBC: material = chave (24), IV = primeiros 8 bytes
            $chave3des = $materialCripto;
            $iv = substr($chave3des, 0, 8);
            $decifrado = openssl_decrypt($payload, 'des-ede3-cbc', $chave3des, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $iv);
        }

        if ($decifrado === false) {
            throw new Exception("Erro ao decifrar dados: " . openssl_error_string());
        }

        // Verificar assinatura se certificado do emissor fornecido
        if ($certEmissor !== null) {
            $pubKey = openssl_pkey_get_public($certEmissor);
            $verificacao = openssl_verify($decifrado, $bufferAssinatura, $pubKey, OPENSSL_ALGO_SHA256);
            if ($verificacao !== 1) {
                throw new Exception("Assinatura digital INVALIDA");
            }
        }

        // Descompactar GZIP
        $xml = @gzdecode($decifrado);
        if ($xml === false) {
            throw new Exception("Erro ao descompactar GZIP");
        }

        return $xml;
    }

    public static function debugCabecalho($dados)
    {
        if (strlen($dados) < self::TAMANHO_CABECALHO) {
            echo "Dados insuficientes para cabecalho SPB\n";
            return;
        }
        $tamCab = unpack('n', substr($dados, 0, 2))[1];
        $algSim = ord($dados[7]);
        echo "=== Cabecalho SPB ===\n";
        echo "C01 - Tamanho: $tamCab bytes\n";
        echo "C02 - Versao: " . ord($dados[2]) . "\n";
        echo "C03 - Erro: " . ord($dados[3]) . "\n";
        echo "C04 - Tratamento: " . ord($dados[4]) . "\n";
        echo "C06 - RSA destino: " . (ord($dados[6]) == 2 ? "2048" : "1024") . "\n";
        echo "C07 - Simetrico: " . ($algSim == 1 ? "3DES" : "AES-256-GCM") . "\n";
        echo "C08 - RSA local: " . (ord($dados[8]) == 2 ? "2048" : "1024") . "\n";
        echo "C09 - Hash: " . (ord($dados[9]) == 3 ? "SHA-256" : "SHA-1") . "\n";
        echo "C10 - AC destino: 0x" . dechex(ord($dados[10])) . "\n";
        echo "C11 - Serial destino: " . substr($dados, 11, 32) . "\n";
        echo "C12 - AC emissor: 0x" . dechex(ord($dados[43])) . "\n";
        echo "C13 - Serial emissor: " . substr($dados, 44, 32) . "\n";
        echo "Dados: " . (strlen($dados) - self::TAMANHO_CABECALHO) . " bytes\n";
        echo "=====================\n";
    }
}
?>
