7zFormat.txt and Methods.txt give the method ID and say nothing about its properties, key derivation or padding. Those were
pinned by experiment against 7-Zip 26.00 (7z, used as a black box), written down here before the code that uses them.
A value enters the code only through one of the experiments below. Scripts: testdata/encryption/generate.py makes the
reference archives; the experiments themselves used a small Python script with hashlib and the OpenSSL-backed
cryptography package as an independent implementation of the hypothesis.
What the code does
- Coder properties (experiments 1 to 3): byte 0 holds the key-derivation power in bits 0-5, and bits 6 and 7 are the extra
one of the IV size and salt size. Byte 1 holds the salt size's remaining four bits in its high nibble and the IV size's in its
low nibble. So
saltSize = bit7 + (b1 >> 4)andivSize = bit6 + (b1 & 15). The salt bytes follow, then the IV bytes. The reader requiresivSize = 16and rejects other sizes with a typed error (7-Zip itself refuses them). A writer uses no salt, a 16-byte IV, and so writes0x40 | power,0x0F, IV. - Key (experiment 1): SHA-256 over 2^power repetitions of
salt || password as UTF-16LE || round counter, where the counter is the round number as an 8-byte little-endian integer starting at 0, all fed to one running hash. The 32-byte digest is the AES-256 key. 7-Zip writes power 19. The reader refuses a power aboveSevenZipReaderOptions.MaxKeyDerivationPower(default 24) withCompressionLimitExceptionbefore any hashing, since 2^power rounds is the work a hostile archive asks for. - Cipher (experiment 1): AES-256 in CBC mode without padding, with the 16-byte IV, over the whole packed stream. The packed size is a multiple of 16; the coder's unpack size (in the folder's unpack-size list) is the true length and the rest is padding.
- Padding (experiment 4): the encoder pads the last block with zero bytes.
- Wrong password: there is no verifier. A wrong key decrypts to noise, which fails the CRC or the next coder. The reader
reports that as
ArchivePasswordException(Problem = Incorrect) for folders that have an AES coder, and says in the message that 7z cannot tell it from damage. - Encrypted header (experiment 5): the signature header's next-header record is an EncodedHeader (0x17) whose single folder
is
[AES, LZMA]: the AES coder first, its output bound to the LZMA coder's input; unpack sizes[LZMA packed size, header size].
Experiments
All with password test123 (and, for 6, a Unicode one), source file a.txt of 1200 bytes, 7z a -t7z -mx0 -mhe=off.
- Layout and key derivation. The coder in the archive read
24 06 F1 07 01 12 | 53 0F | 16 bytes: flag byte, ID, property size 18, then53 0Fand 16 random-looking bytes. Hypothesis: power 0x13 (19) from0x53 & 0x3F; 16-byte IV (bit 6 plus nibble 15); no salt; key as above. Decrypting the 1200 packed bytes with that key and IV reproduceda.txtand its CRC-32. Result: confirmed. - A salt goes before the IV. An archive was rebuilt in Python with a 4-byte salt
01 02 03 04(properties: byte 00x40|19, byte 1(4<<4)|15, the salt, the IV), the data re-encrypted with the salted key, and both header CRCs recomputed.7z t -ptest123:Everything is Ok;-pwrong:CRC Failed in encrypted file. Wrong password?. Confirms the nibble layout and the position of the salt. - Extra bits and sizes. Same construction with a 5-byte salt (flag bit 7 set, nibble 4) and a 16-byte salt (bit 7, nibble 15):
both
Everything is Ok. An 8-byte IV (no bit 6, nibble 8, zero-padded to 16 for the cipher):Unsupported Method. Powers 10 and 0:Everything is Ok(the power field means 2^power rounds). - Padding. A 1001-byte file: packed size 1008, unpack size 1001. Decrypting and looking at bytes 1001-1007: all zero.
- Encrypted header.
-mhe=onarchives open only with the password and decode with the construction above. The reader's test vectors intestdata/encryption/vectors/come from these runs. - Password encoding. A password with
ä ö €and a non-BMP character produced an archive whose key matched UTF-16LE of the string (surrogate pairs included) in the key derivation above.
The round trip the other way is part of the test suite: archives from SevenZipWriter pass 7z t -p... and extract byte-exact,
with and without header encryption, and 7z t with a wrong password fails.
Limits and behaviour
- A missing password for a folder names the entry (
ArchivePasswordException.EntryName,Problem = Required); for an encrypted header the name is empty and the exception comes from the first call that needs the header. - Listing an archive whose header is not encrypted works without a password. Links in encrypted folders are listed with an empty target in that case.
- Memory: one 64 KiB buffer per open decryptor; key material is cleared on dispose where the BCL allows (the key array held by
the reader's cache is zeroed when the reader is disposed;
Aescopies the key it is given).