Files
craft-libs/crates/offcrypto/README.md
Dylan M. Taylor 100768b90d Add craft-offcrypto: encrypted OOXML packages and protection password hashes (#931)
Moved from WordCraft's crates/docx/src/crypt.rs (07f97737): agile and
standard decryption, agile encryption, the ISO and legacy protection
password hashes. The compound file is read and written with craft-cfb
and the descriptor parsed with craft-xml.

From GridCraft's crates/xlsx/src/crypto.rs (5cf52c5f): check(), named
unsupported schemes, rights-managed files, the password key encryptor
chosen by its URI, and independent key vectors.
2026-11-11 18:43:46 -05:00

3.4 KiB

craft-offcrypto

Clean-room Office document cryptography: password-encrypted OOXML packages and the password hashes of document and sheet protection. No unsafe. No third-party implementation was consulted; some expected keys in the tests were computed with msoffcrypto-tool and Python's hashlib as independent checks.

Document Used for
[MS-OFFCRYPTO] Office Document Cryptography Structure (Microsoft Open Specifications) §2.1 data spaces; §2.3.2, §2.3.3, §2.3.4.5-§2.3.4.9 standard encryption; §2.3.4.10-§2.3.4.15 agile encryption; §2.4.2.4 the ISO write-protection hash
ECMA-376 Part 1 §17.15.1.29, §18.2.29; Part 4 §14.8.1 where protection hashes are stored; the legacy 32-bit password key
[MS-CFB] the compound file, through craft-cfb

Encrypted packages

A password-protected .docx, .xlsx or .pptx is a compound file with an EncryptionInfo stream and an EncryptedPackage stream. The crate turns that file into the zip package and back. It does not look inside the package: craft-opc reads what decrypt returns.

if craft_offcrypto::is_encrypted(&bytes) {
    craft_offcrypto::check(&bytes)?;                       // Unsupported("RC4"), ... before asking
    let package = craft_offcrypto::decrypt(&bytes, Some(password))?;   // WrongPassword, Malformed
}
let file = craft_offcrypto::encrypt(&package, password)?;
Read Written
Agile (Office 2010 and later): AES-128/192/256 CBC in 4096-byte segments, SHA-1/256/384/512 yes; the HMAC over the encrypted stream is required and checked AES-256, SHA-512, 100,000 spins, with the data-spaces streams Office expects
Standard (Office 2007): AES-128/192/256 ECB, SHA-1, 50,000 spins yes for tests only (encrypt_standard)
RC4, extensible, certificate-only, rights management refused by name (Error::Unsupported) no

decrypt(bytes, None) returns Error::PasswordRequired once it knows the file is one it can decrypt; check is that question on its own.

Protection passwords

protection::hash_password makes the algorithmName / hashValue / saltValue / spinCount values of a new protection password; protection::check_password_hash and check_legacy_password_hash (the cryptAlgorithmSid form, through legacy_key) check a password against stored values. Which attributes hold them is the format's business. A hash that cannot be checked is PasswordCheck::Unsupported, never a match.

Hostile input

Stream reads are capped (EncryptionInfo at 1 MiB, the package at the file's size), the XML descriptor is parsed with craft-xml under the same cap, every length is checked before slicing, a stated package size larger than the data is damage and never drives an allocation, and spin counts above 10,000,000 are refused. Verifier and integrity values are compared without stopping at the first difference. Passwords, keys and derived hashes are wiped when dropped (zeroize).

Origin

Moved from WordCraft's crates/docx/src/crypt.rs (at 07f97737), the fullest of three copies: it reads both schemes, writes agile encryption and has the protection hashes. From GridCraft's crates/xlsx/src/crypto.rs (at 5cf52c5f) come check, the naming of unsupported schemes (Error::Unsupported), rights-managed files, choosing the password key encryptor by its URI, and the independent key vectors in the tests. DeckCraft's crates/pptx/src/cfb.rs only detected encrypted files, which is_encrypted does.