对称加密

对称加密使用同一个密钥进行加密和解密。本页最重要的一个概念,是普通密码与 AEAD (带关联数据的认证加密,Authenticated Encryption with Associated Data)密码之间的区别。 单独使用 AES-CBC 或 AES-CTR 只能提供机密性,别无其他:无法读取明文的攻击者仍然可以翻转密文中的比特, 接收方会在没有任何异常提示的情况下解密出被篡改的结果。AES-GCM 和 ChaCha20-Poly1305 在加密的同时也提供认证, 因此篡改可以被检测出来,解密会直接失败。除非有特定原因必须使用普通模式,否则应优先选择 AEAD 密码。

 

所有函数分别位于 sgcCrypto_AES、 sgcCrypto_Modes、 sgcCrypto_ChaCha 和 sgcCrypto_Poly1305 单元中。 切勿对同一个密钥重复使用 IV 或随机数:对于 CTR 及各种 AEAD 密码而言,重复使用不仅会削弱机密性,对 GCM 而言更会直接破坏其认证保证。请为每条消息使用 sgcRandomBytes 生成一个全新的值。

 

AES-GCM(推荐)

 

这是大多数协议默认采用的 AEAD 模式。随机数长度为 12 字节;请为每条消息随机生成。 aAAD 会被认证但不会被加密, 这正是密文与其上下文(例如消息 ID 或协议版本)绑定的方式,同时又不会向任何能看到密文的人隐藏该上下文。

 


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 和 XChaCha20-Poly1305

 

这是 RFC 8439 定义的 AEAD 组合。AES-GCM 依赖 AES 硬件加速,而 ChaCha20-Poly1305 在纯软件实现下也能保持恒定速度,这正是 TLS 和 WireGuard 在没有 AES-NI 的移动设备和嵌入式设备上 默认选用它的原因。其随机数长度为 12 字节,限制与 GCM 相同。XChaCha20-Poly1305 将随机数扩展到 24 字节, 长度足以在一个密钥的整个生命周期内随机选取而无需跟踪计数器,这在多个相互独立的写入方共用同一个密钥、 彼此又无法协调计数器时尤为重要。

 


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

 

计数器模式结合 CBC-MAC,RFC 3610 与 NIST SP 800-38C。这是资源受限设备和物联网协议栈偏爱的 AEAD, 因为它只需要分组密码本身,不需要 GHASH 表:Zigbee、802.15.4、蓝牙以及 TLS CCM 密码套件都指定使用它。 在空间允许的情况下,GCM 仍是更好的选择,因为 CCM 需要对数据进行两次遍历,无法实现流水线化处理。

 

随机数长度为 7 到 13 字节,且这个长度并非可以随意选择:15 减去随机数长度就是长度字段的大小, 因此它决定了消息的最大长度,13 字节的随机数会把明文上限限制在 65535 字节。在同一个密钥下, 随机数绝不能重复,这一点与 GCM 完全相同。aTagLen 可以是 4、6、8、10、12、14 或 16 字节。

 


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 正是以空明文运行 AES-GCM,并把整条消息都作为附加数据传入,标准正是这样定义它的。 因此它继承了 GCM 的每一条随机数规则:aIV 通常为 12 字节,且在同一个密钥下绝不能重复。在 GMAC 下重复使用 IV 不仅仅会削弱它, 还会泄露哈希子密钥,让攻击者能够彻底伪造标签,因此这种失效是彻底性的,而非渐进式的。 哈希与消息认证页面中的 AES-CMAC 完全不需要 IV, 除非协议中已经用到了 GCM,否则它是更安全的选择。标签长度为 16 字节。

 


function sgcAES_GMAC(const aKey, aIV, aData: TBytes): TBytes;
function sgcAES_GMAC_Verify(const aKey, aIV, aData, aTag: TBytes): Boolean;

 

AES 分组模式

 

这些模式只提供机密性,不提供认证:如果明文需要完整性保护,请配合哈希与消息认证 页面中的 MAC 一起使用,或者改用上面的 AES-GCM。

 


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 和 sgcAES_OFB 都是自身的逆运算:使用相同的密钥和 IV 再次调用同一个函数即可解密。ECB 之所以被保留, 仅仅是为了与指定使用它的旧格式互操作;相同的明文分组会产生相同的密文分组,这会泄露数据的结构, 因此不应在任何新场景中选用它。

 

CBC 也可以在完全不使用填充的情况下使用,即 Java 写作 AES/CBC/NoPadding 的模式。输入长度必须恰好是 16 的整数倍,否则会引发异常,而不是悄悄地进行填充、生成对方无法读取的密文。 它只适合协议本身已经保证数据按块对齐的场景。

 


function sgcAES_CBC_Encrypt_NoPad(const aKey, aIV, aPlain: TBytes): TBytes;
function sgcAES_CBC_Decrypt_NoPad(const aKey, aIV, aCipher: TBytes): TBytes;

 

对于长度不是分组大小整数倍的数据,密文窃取可以避免 CBC 所带来的填充开销:

 


function sgcAES_CTS_Encrypt(const aKey, aIV, aPlain: TBytes): TBytes;
function sgcAES_CTS_Decrypt(const aKey, aIV, aCipher: TBytes): TBytes;

 

填充方案

 

分组模式要求明文长度是 16 字节的整数倍,而填充方案正是达到这一要求的手段。对于新场景而言, PKCS#7 是唯一正确的选择;其余六种的存在,只是为了读写那些已经选用了别的方案的系统所产生的数据。 padPKCS5 之所以不在列表中, 是因为对 16 字节的分组而言它就是 PKCS#7,这两个名称的差异仅在于它们各自最初定义时所针对的分组大小。

 


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 和 sgcAES_ECB_Encrypt 正是这些函数在 PKCS#7 填充下的简化形式。sgcPad_Remove 在遇到格式错误的填充时会返回 False 而不是引发异常, 并且对每一种失败情形都走同一条路径,因为区分它们正是填充预言攻击所需要的。请特别留意 padZero:它无法区分填充字节和末尾恰好为零的明文字节, 因此会剥离所有末尾的零字节,导致本应合法地以零结尾的数据被截断返回。

 

AES 硬件加速(AES-NI)

 

在使用 Delphi 构建的 Windows Win32 和 Win64 版本中,sgcCrypto_AES 会在运行时检测 CPU,当 CPU 报告支持 AES-NI、PCLMULQDQ 和 SSSE3 时,单块加密、AES-CTR、AES-GCM(计数器模式和 GHASH)以及 AES-CBC 加密都会使用这些指令运行,它们在设计上就是恒定时间的。没有这些指令时,以及在 Linux64、macOS、iOS 和 Android 上,仍然使用此前那套恒定时间的软件核心。CBC 解密始终使用软件核心。原生 TLS 引擎会自动从中受益。

 

sgcAES_HardwareAvailable 在本构建带有硬件路径且 CPU 支持时返回 True。全局变量 sgcAES_UseHardware,默认值为 True,设为 False 会强制使用软件核心,例如用来对比两者。请在启动时设置一次。

 

var sgcAES_UseHardware: Boolean = True;
function sgcAES_HardwareAvailable: Boolean;

if sgcAES_HardwareAvailable then
  Memo1.Lines.Add('AES-NI in use');

 

AES 密钥包装

 

密钥包装保护的是一个密钥,而不是一条消息:它不需要 IV, 是确定性的,并且会对被包装的密钥进行认证。这是在主密钥下存储数据加密密钥的标准方式, 例如用 HSM 或 KMS 主密钥来包装某个文件专属密钥。RFC 3394 要求被包装的密钥长度必须是 8 字节的整数倍; RFC 5649 为任意长度的密钥增加了填充支持。

 


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 和 Salsa20(原始流密码)

 

这两个是上面 AEAD 构造背后所使用的流密码,这里直接提供给那些指定使用原始密码本身的协议。 二者都不提供认证:几乎在所有场景下,本页上方的 Poly1305 AEAD 版本才是您真正需要的。 Salsa20 是 ChaCha20 的前身,保留它是为了与 NaCl 时代的格式兼容;任何新场景都应选择 ChaCha20。

 


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;

 

遗留:DES 和 NTLM

 

sgcCrypto_Legacy 还提供 ECB 模式下的单重 DES,以及基于 DES 的 NTLM v1 密钥派生。DES 的有效密钥长度只有 56 位, 在现代硬件上数小时内即可暴力破解;它的存在只是为了与按名称指定它的协议互操作 (通常是 NTLM 身份验证)。请勿在任何新场景中选用它;请改用 AES。

 


function sgcDES_EncryptECB(const aKey, aData: TBytes): TBytes;
function sgcDES_DecryptECB(const aKey, aData: TBytes): TBytes;
function sgcDES_NTLM7to8(const aKey7: TBytes): TBytes;