OpenPGP é o padrão por trás do PGP e do GnuPG. Ele criptografa arquivos e mensagens para uma ou mais pessoas, assina-os para que o destinatário possa verificar quem os escreveu e que nada foi alterado, e faz as duas coisas sem uma autoridade certificadora: cada usuário possui um par de chaves e publica a metade pública. É o formato usado em e-mail criptografado, em versões de software assinadas, em repositórios de pacotes e na troca de arquivos com parceiros e bancos.
As units sgcCrypto implementam o OpenPGP em Object Pascal puro, sem DLL externa, sem OpenSSL e sem Indy. Elas compilam do Delphi 7 ao Delphi 13 em Win32 e Win64, e as units também compilam para Linux64. Tudo funciona em memória sobre TBytes: a saída em armor é texto ASCII retornado como TBytes, e os auxiliares sgcPGP_UTF8 e sgcPGP_UTF8ToString convertem entre strings e bytes.
Versão 4 ou versão 6
Duas versões do formato são suportadas, e ambas são lidas e escritas.
Versão 4 (RFC 4880 e RFC 6637) é a que o GnuPG, o Kleopatra, o Thunderbird e quase todas as ferramentas em uso hoje utilizam. Escolha-a sempre que o outro lado executar o GnuPG ou que você não controle o software dele. As mensagens são protegidas com SEIPD v1 (AES em modo CFB mais uma verificação de integridade MDC).
Versão 6 (RFC 9580) é a revisão atual do padrão. As chaves usam Ed25519/X25519 ou Ed448/X448 nativamente, as chaves secretas são protegidas com Argon2, e as mensagens usam SEIPD v2, uma construção AEAD de verdade (OCB por padrão). Escolha-a quando todas as partes suportarem a RFC 9580, por exemplo entre duas aplicações que você mesmo constrói. O GnuPG 2.4 não lê chaves v6.
Você não precisa escolher por mensagem. Ao criptografar, o SEIPD v2 só é usado se todos os destinatários o suportarem (chaves v6). Se algum destinatário tiver uma chave v4, a mensagem é escrita como SEIPD v1, de modo que uma lista de destinatários mista continua legível pelo GnuPG.
Recursos suportados
| Área | Suportado |
| Formatos | OpenPGP v4 (RFC 4880, RFC 6637, compatível com o GnuPG) e v6 (RFC 9580) |
| Algoritmos de chave pública | RSA 2048/3072/4096, Ed25519, Ed448 (somente v6), X25519, X448, ECDSA e ECDH nas curvas NIST P-256/P-384/P-521, ECDSA e ECDH nas curvas Brainpool (somente importação) |
| Cifras simétricas | AES-128, AES-192, AES-256 |
| Dados criptografados | SEIPD v1 (CFB mais MDC), SEIPD v2 AEAD com OCB, EAX ou GCM |
| Chaves de sessão | Chave pública (PKESK v3 e v6) e senha (SKESK v4 e v6) |
| Proteção de chave (S2K) | S2K iterado e com salt, S2K Argon2 |
| Compressão | ZIP (DEFLATE bruto) e ZLIB |
| Assinaturas | Assinaturas destacadas, em linha (uma passagem) e em texto claro, modo binário e modo texto |
| Codificação | ASCII armor e binário |
| GnuPG 2.4 LibrePGP | Lê mensagens criptografadas com OCB (pacote 20) e pacotes SKESK v5, nunca os escreve |
Units
| Unit | Conteúdo |
| sgcCrypto_PGP | API de alto nível: criptografar, descriptografar, assinar, verificar, records de resultado |
| sgcCrypto_PGP_Cert | Chaves transferíveis, chaveiros e geração de chaves: TsgcPGPKey, TsgcPGPKeyRing, sgcPGP_GenerateKey |
| sgcCrypto_PGP_Message | Pacotes de mensagem: chaves de sessão, dados criptografados, dados literais e compactados |
| sgcCrypto_PGP_Signature | Pacotes de assinatura, criação e verificação |
| sgcCrypto_PGP_Keys | Pacotes de chave e proteção da chave secreta |
| sgcCrypto_PGP_Packets | Enquadramento de pacotes, ASCII armor, S2K, constantes e auxiliares |
| sgcCrypto_Modes | AES-OCB e AES-EAX, usados pelo SEIPD v2 (veja Criptografia Simétrica) |
| sgcCrypto_Deflate | Compressão DEFLATE e ZLIB (veja Criptografia de Arquivos WinZip AE-2) |
A maioria das aplicações só precisa de sgcCrypto_PGP, sgcCrypto_PGP_Cert e sgcCrypto_PGP_Packets na cláusula uses. As units fazem parte do pacote Cryptography e são excluídas das builds FIPS (SGC_CRYPTO_FIPS, veja Modo FIPS).
Gerenciamento de chaves
sgcPGP_GenerateKey cria uma chave primária de assinatura mais uma subchave de criptografia, vincula o ID de usuário com uma autoassinatura e retorna um TsgcPGPKey. A senha protege a chave secreta quando ela é salva; o objeto retornado já está desbloqueado e pode assinar e descriptografar imediatamente. Uma senha vazia salva a chave secreta sem proteção. aExpirationSeconds define o tempo de vida da chave contado a partir da criação, 0 significa que ela nunca expira.
| TsgcPGPKeyType | Versão 4 | Versão 6 |
| pgpkEd25519 | EdDSA Ed25519 + ECDH Curve25519 | Ed25519 + X25519 |
| pgpkEd448 | não disponível | Ed448 + X448 |
| pgpkRSA2048 | RSA 2048 | não disponível (o mínimo da RFC 9580 é 3072) |
| pgpkRSA3072 | RSA 3072 | RSA 3072 |
| pgpkRSA4096 | RSA 4096 | RSA 4096 |
| pgpkNISTP256 | ECDSA + ECDH P-256 | ECDSA + ECDH P-256 |
| pgpkNISTP384 | ECDSA + ECDH P-384 | ECDSA + ECDH P-384 |
| pgpkNISTP521 | ECDSA + ECDH P-521 | ECDSA + ECDH P-521 |
function sgcPGP_GenerateKey(const aUserID, aPassphrase: string;
aKeyType: TsgcPGPKeyType = pgpkEd25519; aVersion: Integer = 4;
aExpirationSeconds: Cardinal = 0): TsgcPGPKey;
var
oAlice: TsgcPGPKey;
vPublic, vPrivate: string;
begin
oAlice := sgcPGP_GenerateKey('Alice <alice@example.com>', 'alice passphrase',
pgpkEd25519, 4);
try
vPublic := oAlice.SaveToArmored(False); { share this one }
vPrivate := oAlice.SaveToArmored(True); { keep this one, protected by the passphrase }
WriteLn(oAlice.FingerprintHex + ' ' + oAlice.PrimaryUserID);
finally
oAlice.Free;
end;
end;
Uma chave carregada de um arquivo fica bloqueada se foi salva com uma senha. Chame Unlock antes de usá-la para assinar; ele retorna False quando a senha está errada. Protect criptografa novamente o material secreto com uma nova senha, que é a forma de alterá-la. As chaves v4 são protegidas com AES-256 e o S2K iterado e com salt; as chaves v6 com AES-256 em modo OCB e um S2K Argon2.
var
oKey: TsgcPGPKey;
begin
oKey := TsgcPGPKey.Create;
try
oKey.LoadFromBytes(sgcPGP_UTF8(vPrivate)); { armored or binary }
if not oKey.Unlock('alice passphrase') then
raise Exception.Create('wrong passphrase');
oKey.Protect('new passphrase');
vPrivate := oKey.SaveToArmored(True);
finally
oKey.Free;
end;
end;
Um TsgcPGPKeyRing guarda qualquer número de chaves e é o dono delas: uma chave passada a Add é liberada junto com o chaveiro, portanto não a libere você mesmo. LoadFromFile e LoadFromBytes aceitam dados em armor ou binários com uma ou várias chaves (uma exportação de um chaveiro inteiro do GnuPG funciona) e acrescentam ao que já está carregado. Buscas:
Cada chave expõe Version, FingerprintHex, KeyIDHex, PrimaryUserID, HasSecret, IsValid, IsRevoked, IsExpired e ExpirationTime, além de seus IDs de usuário e subchaves. As autoassinaturas, os vínculos de subchave, a expiração e a revogação são verificados no carregamento, e uma chave revogada ou expirada nunca é selecionada para criptografar ou assinar.
Criptografar e descriptografar
sgcPGP_Encrypt criptografa para uma ou mais chaves públicas. Informar um signatário assina os dados dentro da mensagem criptografada, que é o que gpg --sign --encrypt produz. O signatário precisa estar desbloqueado. Os dados são compactados com ZIP por padrão; passe CSGC_PGP_COMP_ZLIB ou CSGC_PGP_COMP_NONE para alterar isso. O AES-256 é usado quando todos os destinatários o listam em suas preferências (sempre para SEIPD v2), caso contrário o AES-128.
function sgcPGP_Encrypt(const aData: TBytes;
const aRecipients: array of TsgcPGPKey; aSigner: TsgcPGPKey = nil;
aArmor: Boolean = True; aCompression: Integer = CSGC_PGP_COMP_ZIP;
const aFileName: string = ''): TBytes;
var
oPublicRing: TsgcPGPKeyRing;
oBob: TsgcPGPKey;
oEncrypted: TBytes;
begin
oPublicRing := TsgcPGPKeyRing.Create;
try
oPublicRing.LoadFromFile('public-keys.asc');
oBob := oPublicRing.FindByUserID('bob@example.com');
if oBob = nil then
raise Exception.Create('Bob is not in the keyring');
{ oAlice is an unlocked secret key, used as signer }
oEncrypted := sgcPGP_Encrypt(sgcPGP_UTF8('Hello Bob'), [oBob], oAlice);
WriteLn(sgcPGP_UTF8ToString(oEncrypted)); { -----BEGIN PGP MESSAGE----- }
finally
oPublicRing.Free;
end;
end;
Para uma senha compartilhada em vez de chaves, use sgcPGP_EncryptSymmetric. Com aVersion = 4 ele escreve um SKESK v4 e SEIPD v1, que o gpg --decrypt lê. Com 6 ele escreve um SKESK v6 com Argon2 e SEIPD v2 com AES-256 OCB.
function sgcPGP_EncryptSymmetric(const aData: TBytes; const aPassphrase: string;
aArmor: Boolean = True; aVersion: Integer = 4;
aCompression: Integer = CSGC_PGP_COMP_ZIP;
const aFileName: string = ''): TBytes;
oEncrypted := sgcPGP_EncryptSymmetric(sgcPGP_UTF8('Hello'), 'shared passphrase');
sgcPGP_Decrypt trata os dois tipos, em armor ou binários. aSecretKeys contém as chaves secretas a tentar; aPassphrase as desbloqueia e também é tentada como a senha da mensagem. Uma chave bloqueada é desbloqueada em uma cópia privada, portanto o chaveiro passado nunca é modificado. aVerifyKeys contém as chaves públicas usadas para verificar uma assinatura embutida e pode ser nil. Uma mensagem que não pode ser descriptografada gera EsgcPGPError.
function sgcPGP_Decrypt(const aData: TBytes; aSecretKeys: TsgcPGPKeyRing;
const aPassphrase: string; aVerifyKeys: TsgcPGPKeyRing;
out aResult: TsgcPGPDecryptResult): TBytes;
var
oSecretRing: TsgcPGPKeyRing;
oResult: TsgcPGPDecryptResult;
oData: TBytes;
begin
oSecretRing := TsgcPGPKeyRing.Create;
try
oSecretRing.LoadFromFile('bob-secret.asc');
try
oData := sgcPGP_Decrypt(oEncrypted, oSecretRing, 'bob passphrase',
oPublicRing, oResult);
WriteLn(sgcPGP_UTF8ToString(oData));
if oResult.Verify.Valid then
WriteLn('signed by ' + oResult.Verify.SignerUserID)
else if oResult.Verify.Signed then
WriteLn('signature not valid: ' + oResult.Verify.Status);
except
on E: EsgcPGPError do
WriteLn('cannot decrypt: ' + E.Message);
end;
finally
oSecretRing.Free;
end;
end;
O record TsgcPGPDecryptResult descreve o que foi descriptografado:
| Campo | Significado |
| FileName | Nome de arquivo armazenado no pacote de dados literais, pode estar vazio |
| Date | Data dos dados literais, tempo Unix |
| Format | Byte de formato dos dados literais: 'b' binário, 't' ou 'u' texto |
| Encrypted | True quando a entrada estava criptografada, False para uma mensagem somente assinada |
| SymAlgo | ID do algoritmo simétrico (7, 8, 9 para AES-128, AES-192, AES-256) |
| AEADAlgo | ID do modo AEAD (1 EAX, 2 OCB, 3 GCM), 0 para SEIPD v1 |
| RecipientKeyID | ID da chave secreta que descriptografou a mensagem, vazio para uma senha |
| Verify | Um TsgcPGPVerifyResult para a assinatura embutida, veja abaixo |
Assinar e verificar
Três tipos de assinatura cobrem os casos usuais:
O signatário precisa estar desbloqueado. aTextMode assina o texto com finais de linha CRLF canônicos, de modo que a assinatura continua sendo verificada depois que o arquivo passa entre Windows e Linux.
function sgcPGP_Sign(const aData: TBytes; aSigner: TsgcPGPKey;
aDetached: Boolean = True; aArmor: Boolean = True; aTextMode: Boolean = False;
const aFileName: string = ''): TBytes;
function sgcPGP_ClearSign(const aText: string; aSigner: TsgcPGPKey): string;
function sgcPGP_Verify(const aSignedMessage: TBytes; aKeys: TsgcPGPKeyRing;
out aData: TBytes; out aResult: TsgcPGPVerifyResult): Boolean;
function sgcPGP_VerifyDetached(const aData, aSignature: TBytes;
aKeys: TsgcPGPKeyRing; out aResult: TsgcPGPVerifyResult): Boolean;
var
oData, oSignature, oSigned, oOut: TBytes;
oVerify: TsgcPGPVerifyResult;
vClear: string;
begin
oData := sgcPGP_UTF8('release 1.0');
{ detached }
oSignature := sgcPGP_Sign(oData, oAlice);
if sgcPGP_VerifyDetached(oData, oSignature, oPublicRing, oVerify) then
WriteLn('valid, signed by ' + oVerify.SignerUserID);
{ inline }
oSigned := sgcPGP_Sign(oData, oAlice, False);
if sgcPGP_Verify(oSigned, oPublicRing, oOut, oVerify) then
WriteLn(sgcPGP_UTF8ToString(oOut));
{ cleartext }
vClear := sgcPGP_ClearSign('Hello', oAlice);
if sgcPGP_Verify(sgcPGP_UTF8(vClear), oPublicRing, oOut, oVerify) then
WriteLn('valid cleartext signature');
end;
O record TsgcPGPVerifyResult informa sobre a primeira assinatura válida encontrada, ou sobre a primeira assinatura quando nenhuma é válida:
| Campo | Significado |
| Signed | True quando a mensagem traz pelo menos uma assinatura |
| Valid | True quando uma assinatura foi verificada com uma chave válida do chaveiro |
| SignerKeyID, SignerFingerprint | ID da chave e impressão digital em hexadecimal da chave (ou subchave) que assinou |
| SignerUserID | ID de usuário primário do signatário |
| SignatureTime | Hora de criação da assinatura, tempo Unix |
| HashAlgo | ID do algoritmo de hash, sgcPGP_HashName fornece seu nome |
| Status | Vazio quando válida, caso contrário o motivo: not signed, signer key not found, bad signature, signer key not valid at signature time, signature expired, malformed signature, unexpected signature type |
Uma assinatura de uma chave que não está no chaveiro nunca é válida: Status é signer key not found e os campos do signatário permanecem vazios. Confie apenas em Valid, e somente para chaves que você mesmo colocou no chaveiro.
Interoperabilidade com o GnuPG
Use chaves v4 para trocar dados com o GnuPG. Os arquivos em armor funcionam nos dois sentidos sem alterações. Comandos típicos do lado do GnuPG:
gpg --import alice-public.asc
gpg --armor --export bob@example.com > bob-public.asc
gpg --armor --export-secret-keys bob@example.com > bob-secret.asc
gpg --decrypt message.asc
gpg --armor --sign --encrypt --recipient alice@example.com file.txt
gpg --armor --symmetric file.txt
gpg --verify file.txt.asc file.txt
gpg --verify clearsigned.asc
gpg --armor --detach-sign file.txt
gpg --clearsign note.txt
As chaves exportadas pelo GnuPG são carregadas com TsgcPGPKeyRing.LoadFromFile, inclusive as chaves secretas protegidas por senha. O GnuPG 2.4 marca suas chaves com o recurso OCB do LibrePGP e pode criptografar para elas com o pacote 20 (dados criptografados com OCB) ou com pacotes SKESK v5. Essas mensagens são lidas e descriptografadas, mas nunca são escritas: os dados criptografados aqui para uma chave v4 sempre usam SEIPD v1, que toda versão do GnuPG lê. As chaves secretas movidas para um smartcard aparecem em uma exportação do GnuPG como stubs sem material secreto; elas são carregadas, mas não podem assinar nem descriptografar.
Notas de segurança
Limitações
Referência da API
| Nome | Unit | Finalidade |
| sgcPGP_Encrypt | sgcCrypto_PGP | Criptografar para chaves públicas, opcionalmente assinado |
| sgcPGP_EncryptSymmetric | sgcCrypto_PGP | Criptografar com uma senha, v4 ou v6 |
| sgcPGP_Decrypt | sgcCrypto_PGP | Descriptografar e verificar uma assinatura embutida |
| sgcPGP_Sign | sgcCrypto_PGP | Assinatura destacada ou em linha |
| sgcPGP_ClearSign | sgcCrypto_PGP | Assinatura em texto claro |
| sgcPGP_Verify | sgcCrypto_PGP | Verificar uma mensagem assinada em linha ou em texto claro |
| sgcPGP_VerifyDetached | sgcCrypto_PGP | Verificar uma assinatura destacada |
| TsgcPGPDecryptResult, TsgcPGPVerifyResult | sgcCrypto_PGP | Records de resultado |
| sgcPGPMaxDecompressedSize | sgcCrypto_PGP | Limite de descompressão, 0 é ilimitado |
| sgcPGP_GenerateKey | sgcCrypto_PGP_Cert | Gerar uma chave v4 ou v6 |
| TsgcPGPKey | sgcCrypto_PGP_Cert | Uma chave com seus IDs de usuário e subchaves: LoadFromBytes, SaveToBytes, SaveToArmored, Unlock, Protect |
| TsgcPGPKeyRing | sgcCrypto_PGP_Cert | Lista proprietária de chaves: Add, Remove, Clear, LoadFromBytes, LoadFromFile, SaveToArmored, FindByID, FindByUserID, FindKeyPacket |
| TsgcPGPKeyType | sgcCrypto_PGP_Cert | Algoritmo de chave para geração |
| sgcPGP_Armor, sgcPGP_Dearmor, sgcPGP_IsArmored | sgcCrypto_PGP_Packets | Codificação e decodificação ASCII armor |
| sgcPGP_UTF8, sgcPGP_UTF8ToString | sgcCrypto_PGP_Packets | String para bytes UTF-8 e de volta |
| sgcPGPMaxArgon2EncodedM, sgcPGPMaxArgon2Passes, sgcPGPMaxArgon2Lanes | sgcCrypto_PGP_Packets | Limites do S2K Argon2 |
| CSGC_PGP_COMP_NONE, CSGC_PGP_COMP_ZIP, CSGC_PGP_COMP_ZLIB | sgcCrypto_PGP_Packets | Compressão para criptografia |
| EsgcPGPError | sgcCrypto_PGP_Packets | Exceção gerada em qualquer erro do OpenPGP |
function sgcPGP_Armor(const aData: TBytes; aType: TsgcPGPArmorType;
aWithChecksum: Boolean = True): string;
function sgcPGP_Dearmor(const aText: string;
out aType: TsgcPGPArmorType): TBytes;
{ TsgcPGPArmorType = (pgpaMessage, pgpaPublicKey, pgpaPrivateKey, pgpaSignature) }
Demo
A pasta Demos\18.Cryptography\09.OpenPGP (sgcOpenPGP.dpr) mostra a API desta página em uso: geração de chaves, criptografia e descriptografia, assinatura e verificação.