Symmetric encryption uses the same key to encrypt and decrypt. The one idea that matters most on this page is the difference between a plain cipher and an AEAD (Authenticated Encryption with Associated Data) cipher. AES-CBC or AES-CTR alone give confidentiality and nothing else: an attacker who cannot read the plaintext can still flip bits in the ciphertext, and the recipient will decrypt the tampered result without any indication that anything is wrong. AES-GCM and ChaCha20-Poly1305 authenticate as well as encrypt, so tampering is detected and decryption fails outright. Prefer an AEAD cipher unless something specific forces a plain mode.
All functions live in sgcCrypto_AES, sgcCrypto_Modes, sgcCrypto_ChaCha and sgcCrypto_Poly1305. Never reuse an IV or a nonce with the same key: for CTR and the AEAD ciphers, reuse does not just weaken confidentiality, for GCM specifically it breaks the authentication guarantee outright. Generate a fresh one with sgcRandomBytes for every message.
AES-GCM (recommended)
The AEAD mode most protocols default to. The nonce is 12 bytes; generate it at random per message. aAAD is authenticated but not encrypted, which is how a ciphertext gets bound to its context, for example a message id or a protocol version, without hiding that context from anyone who can see the ciphertext.
function sgcAES_GCM_Encrypt(const aKey, aIV, aPlain, aAAD: TBytes; out aTag: TBytes): TBytes;
function sgcAES_GCM_Decrypt(const aKey, aIV, aCipher, aAAD, aTag: TBytes; out aPlain: TBytes): Boolean;
var
oKey, oIV, oCipher, oTag, oPlain, oAAD: TBytes;
begin
oIV := sgcRandomBytes(12);
oCipher := sgcAES_GCM_Encrypt(oKey, oIV, oPlain, oAAD, oTag);
{ send oIV, oCipher and oTag to the recipient }
if not sgcAES_GCM_Decrypt(oKey, oIV, oCipher, oAAD, oTag, oPlain) then
{ the ciphertext, the tag or the AAD was altered: reject it };
end;
ChaCha20-Poly1305 and XChaCha20-Poly1305
The RFC 8439 AEAD pair. Where AES-GCM leans on AES hardware acceleration, ChaCha20-Poly1305 runs at constant speed in pure software, which is why TLS and WireGuard default to it on mobile and embedded devices without AES-NI. The nonce is 12 bytes, same restriction as GCM. XChaCha20-Poly1305 extends the nonce to 24 bytes, large enough to pick at random for the lifetime of a key without tracking a counter, which matters when many independent writers share one key and cannot coordinate a counter between them.
function sgcChaCha20Poly1305_Encrypt(const aKey, aNonce, aPlain, aAAD: TBytes; out aTag: TBytes): TBytes;
function sgcChaCha20Poly1305_Decrypt(const aKey, aNonce, aCipher, aAAD, aTag: TBytes; out aPlain: TBytes): Boolean;
function sgcXChaCha20Poly1305_Encrypt(const aKey, aNonce, aPlain, aAAD: TBytes; out aTag: TBytes): TBytes;
function sgcXChaCha20Poly1305_Decrypt(const aKey, aNonce, aCipher, aAAD, aTag: TBytes; out aPlain: TBytes): Boolean;
AES-CCM
Counter with CBC-MAC, RFC 3610 and NIST SP 800-38C. This is the AEAD that constrained and IoT stacks favour, because it needs only the block cipher itself and no GHASH table: Zigbee, 802.15.4, Bluetooth and the TLS CCM suites all specify it. Where there is room, GCM remains the better pick, since CCM makes two passes over the data and cannot be pipelined.
The nonce is 7 to 13 bytes and its length is not a free choice: 15 minus the nonce length is the size of the length field, so it fixes the maximum message size, and a 13 byte nonce caps the plaintext at 65535 bytes. A nonce must never repeat under one key, exactly as with GCM. aTagLen is 4, 6, 8, 10, 12, 14 or 16 bytes.
function sgcAES_CCM_Encrypt(const aKey, aNonce, aPlain, aAAD: TBytes; aTagLen: Integer; out aTag: TBytes): TBytes;
function sgcAES_CCM_Decrypt(const aKey, aNonce, aCipher, aAAD, aTag: TBytes; out aPlain: TBytes): Boolean;
var
oKey, oNonce, oCipher, oTag, oPlain, oAAD: TBytes;
begin
oNonce := sgcRandomBytes(12);
oCipher := sgcAES_CCM_Encrypt(oKey, oNonce, oPlain, oAAD, 16, oTag);
{ send oNonce, oCipher and oTag to the recipient }
if not sgcAES_CCM_Decrypt(oKey, oNonce, oCipher, oAAD, oTag, oPlain) then
{ the ciphertext, the tag or the AAD was altered: reject it };
end;
AES-GMAC
GMAC is exactly AES-GCM run with an empty plaintext and the whole message passed as additional data, which is how the standard defines it. It therefore inherits every one of GCM's nonce rules: aIV is normally 12 bytes and must never repeat under one key. Reusing an IV under GMAC does not merely weaken it, it leaks the hash subkey and lets an attacker forge tags outright, so the failure is total rather than gradual. AES-CMAC on the Hashing & Message Authentication page needs no IV at all and is the safer pick unless GCM is already in the protocol. The tag is 16 bytes.
function sgcAES_GMAC(const aKey, aIV, aData: TBytes): TBytes;
function sgcAES_GMAC_Verify(const aKey, aIV, aData, aTag: TBytes): Boolean;
AES block modes
These give confidentiality only, with no authentication: pair with a MAC from Hashing & Message Authentication if the plaintext needs integrity, or use AES-GCM above instead.
function sgcAES_CBC_Encrypt(const aKey, aIV, aPlain: TBytes): TBytes;
function sgcAES_CBC_Decrypt(const aKey, aIV, aCipher: TBytes): TBytes;
function sgcAES_CTR(const aKey, aIV, aData: TBytes): TBytes;
function sgcAES_CFB_Encrypt(const aKey, aIV, aPlain: TBytes): TBytes;
function sgcAES_CFB_Decrypt(const aKey, aIV, aCipher: TBytes): TBytes;
function sgcAES_OFB(const aKey, aIV, aData: TBytes): TBytes;
function sgcAES_ECB_Encrypt(const aKey, aPlain: TBytes): TBytes;
function sgcAES_ECB_Decrypt(const aKey, aCipher: TBytes): TBytes;
sgcAES_CTR and sgcAES_OFB are their own inverse: call the same function again with the same key and IV to decrypt. ECB is included only for interoperating with old formats that specify it; identical plaintext blocks produce identical ciphertext blocks, which leaks the structure of the data, so it should not be chosen for anything new.
CBC is also available with no padding at all, the mode Java writes as AES/CBC/NoPadding. The input length must be an exact multiple of 16 and an exception is raised when it is not, rather than the call quietly padding and producing a ciphertext the other side cannot read. It is only safe where the protocol already guarantees block aligned data.
function sgcAES_CBC_Encrypt_NoPad(const aKey, aIV, aPlain: TBytes): TBytes;
function sgcAES_CBC_Decrypt_NoPad(const aKey, aIV, aCipher: TBytes): TBytes;
Ciphertext stealing avoids the padding overhead of CBC for data that is not a multiple of the block size:
function sgcAES_CTS_Encrypt(const aKey, aIV, aPlain: TBytes): TBytes;
function sgcAES_CTS_Decrypt(const aKey, aIV, aCipher: TBytes): TBytes;
Padding schemes
A block mode needs the plaintext to be a multiple of 16 bytes, and the padding scheme is how it gets there. PKCS#7 is the only right choice for something new; the other six exist to read and write data from a system that already chose otherwise. padPKCS5 is not in the list because for a 16 byte block it is PKCS#7, the two names differ only in the block sizes they were defined for.
type
TsgcPadding = (padNone, padPKCS7, padZero, padANSIX923, padISO7816_4,
padISO10126_2, padTBC);
function sgcPad_Add(const aData: TBytes; aBlockSize: Integer; aPadding: TsgcPadding): TBytes;
function sgcPad_Remove(const aData: TBytes; aBlockSize: Integer; aPadding: TsgcPadding; out aPlain: TBytes): Boolean;
function sgcAES_CBC_EncryptPad(const aKey, aIV, aPlain: TBytes; aPadding: TsgcPadding): TBytes;
function sgcAES_CBC_DecryptPad(const aKey, aIV, aCipher: TBytes; aPadding: TsgcPadding): TBytes;
function sgcAES_ECB_EncryptPad(const aKey, aPlain: TBytes; aPadding: TsgcPadding): TBytes;
function sgcAES_ECB_DecryptPad(const aKey, aCipher: TBytes; aPadding: TsgcPadding): TBytes;
var
oKey, oIV, oPlain, oCipher: TBytes;
begin
oCipher := sgcAES_CBC_EncryptPad(oKey, oIV, oPlain, padANSIX923);
end;
sgcAES_CBC_Encrypt and sgcAES_ECB_Encrypt further up are the PKCS#7 shorthand for these. sgcPad_Remove returns False on a malformed pad rather than raising, and takes the same path for every kind of failure, because distinguishing them is what a padding oracle needs. Take particular care with padZero: it cannot distinguish a padding byte from a trailing plaintext zero, so it strips every trailing zero, and data that can legitimately end in one comes back truncated.
AES Key Wrap
Key wrapping protects a key, not a message: it needs no IV, it is deterministic, and it authenticates the wrapped key. This is the standard way to store a data encryption key under a master key, for example an HSM or KMS master key wrapping a per-file key. RFC 3394 requires the key being wrapped to be a multiple of 8 bytes; RFC 5649 adds padding for keys of any length.
function sgcAES_KeyWrap(const aKEK, aPlainKey: TBytes): TBytes;
function sgcAES_KeyUnwrap(const aKEK, aWrapped: TBytes; out aPlainKey: TBytes): Boolean;
function sgcAES_KeyWrapPad(const aKEK, aPlainKey: TBytes): TBytes;
function sgcAES_KeyUnwrapPad(const aKEK, aWrapped: TBytes; out aPlainKey: TBytes): Boolean;
ChaCha20 and Salsa20 (raw stream ciphers)
The stream ciphers behind the AEAD constructions above, provided directly for protocols that specify the raw cipher. Neither authenticates: almost always the Poly1305 AEAD versions further up this page are what you want instead. Salsa20 is the predecessor of ChaCha20, kept for compatibility with NaCl era formats; choose ChaCha20 for anything new.
function sgcChaCha20(const aKey, aNonce, aData: TBytes; aCounter: Cardinal): TBytes;
function sgcXChaCha20(const aKey, aNonce, aData: TBytes; aCounter: Cardinal): TBytes;
function sgcSalsa20(const aKey, aNonce, aData: TBytes; aCounter: Int64): TBytes;
function sgcXSalsa20(const aKey, aNonce, aData: TBytes; aCounter: Int64): TBytes;
Legacy: DES and NTLM
sgcCrypto_Legacy also provides single DES in ECB mode and the DES based NTLM v1 key derivation. DES has a 56 bit effective key, brute forceable on modern hardware in hours; it exists only to interoperate with protocols that specify it by name (NTLM authentication is the usual reason). Do not choose it for anything new; use AES instead.
function sgcDES_EncryptECB(const aKey, aData: TBytes): TBytes;
function sgcDES_DecryptECB(const aKey, aData: TBytes): TBytes;
function sgcDES_NTLM7to8(const aKey7: TBytes): TBytes;