WinZip AE-2 归档加密

WinZip AE-1 和 AE-2 是 .zip 归档内部使用的 AES 加密方案,用于取代原本脆弱的 PKZIP 2.0 流密码。 所有函数都位于 sgcCrypto_Zip_AE2 单元中,且每次只处理一个归档条目;构建 .zip 容器本身不在本单元的范围之内。

 

布局

 

密钥材料通过对密码执行 PBKDF2-HMAC-SHA1、迭代 1000 次得到,一次调用即可同时产生加密密钥、 认证密钥和一个 2 字节的密码校验值。盐值长度和密钥长度都取决于该条目所选用的 AES 强度。

 


function sgcZipAE2_SaltLength(aStrength: TsgcZipAESStrength): Integer;
function sgcZipAE2_KeyLength(aStrength: TsgcZipAESStrength): Integer;
procedure sgcZipAE2_DeriveKeys(const aPassword, aSalt: TBytes; aStrength: TsgcZipAESStrength;
  out aEncKey, aAuthKey, aVerifier: TBytes);

 

加密和解密一个条目

 


function sgcZipAE2_Encrypt(const aPassword, aSalt, aPlain: TBytes; aStrength: TsgcZipAESStrength;
  out aVerifier, aAuthCode: TBytes): TBytes;
function sgcZipAE2_Decrypt(const aPassword, aSalt, aCipher, aVerifier, aAuthCode: TBytes;
  aStrength: TsgcZipAESStrength; out aPlain: TBytes): Boolean;

var
  oSalt, oCipher, oVerifier, oAuth, oPlain: TBytes;
begin
  oSalt := sgcRandomBytes(sgcZipAE2_SaltLength(zasAES256));
  oCipher := sgcZipAE2_Encrypt(oPassword, oSalt, oPlain, zasAES256, oVerifier, oAuth);
  if sgcZipAE2_Decrypt(oPassword, oSalt, oCipher, oVerifier, oAuth, zasAES256, oPlain) then
    { correct password, untampered data };
end;

 

2 字节的校验值能立即识别出大多数错误密码,而 aAuthCode 携带的 10 字节 HMAC-SHA1 认证码则会捕获其余的错误密码,同时也能检测对加密内容的任何篡改。

 

AE-2 不能保护的内容

 

该认证码只覆盖单个条目的文件内容本身。它不覆盖文件名、时间戳或归档结构,因此一个无法读取内容的攻击者 仍然可以在不被察觉的情况下重命名、重新排序或删除归档中的条目。它同样无法弥补弱密码带来的问题: 其密钥派生只使用了 1000 次 PBKDF2 迭代,远少于Argon2id 或 scrypt 所使用的次数,这是因为该格式的诞生早于这些算法。一个 AE-2 归档的强度,取决于为它所选择的密码强度。

 

扩展字段

 

一个 .zip 条目通过将其压缩方法设置为 99,并添加一个小的扩展字段(头部 ID 为 0x9901)来标记自己为 AE 加密条目, 该扩展字段记录了底层实际使用的压缩方法、AE 版本(1 或 2)以及 AES 强度, 这些正是读取方在尝试输入密码之前所需要知道的信息。

 


function sgcZipAE2_BuildExtraField(aStrength: TsgcZipAESStrength; aCompressionMethod: Word;
  aVersion: TsgcZipAEVersion = zavAE2): TBytes;
function sgcZipAE2_ParseExtraField(const aData: TBytes; out aStrength: TsgcZipAESStrength;
  out aCompressionMethod: Word; out aVersion: TsgcZipAEVersion): Boolean;

 

另外还有两个便捷函数,用于打包和解包本地文件头之后紧跟的盐值/校验值/密文/认证码这一整套布局, 供那些需要组装或读取完整条目(而不仅仅是其密码学部分)的调用方使用:

 


function sgcZipAE2_PackEntry(const aSalt, aVerifier, aCipher, aAuthCode: TBytes): TBytes;
function sgcZipAE2_UnpackEntry(const aEntry: TBytes; aStrength: TsgcZipAESStrength;
  out aSalt, aVerifier, aCipher, aAuthCode: TBytes): Boolean;