flowchart LR
A["🔑 Password\n(UTF-16LE, ≤255 chars)"] --> B["H₀ = H(salt ‖ pw)"]
B --> C["Spin Loop\nspinCount iterations\nHₙ = H(LE32(i) ‖ Hₙ₋₁)\n⚠️ iterator PREPENDED"]
C --> D["Hfinal = H(Hₙ ‖ blockKey)"]
D --> E{"len vs keyBits/8"}
E -->|"shorter"| F["Pad with 0x36"]
E -->|"longer"| G["Truncate"]
E -->|"equal"| H["Use as-is"]
F --> I["🔐 AES Key"]
G --> I
H --> I
flowchart LR
subgraph EI["EncryptionInfo Stream"]
direction TB
HDR["Binary Header (8 bytes)\nvMajor=0x0004 vMinor=0x0004\nReserved=0x00000040"]
XML["XML Descriptor\n(UTF-8, no BOM)"]
HDR --> XML
end
subgraph XMLE["XML Elements"]
direction TB
KD["keyData\nsaltValue, cipherAlgorithm\nhashAlgorithm, keyBits"]
DI["dataIntegrity\nencryptedHmacKey\nencryptedHmacValue"]
KE["keyEncryptors"]
KD --> DI --> KE
end
subgraph ENCS["Key Encryptors"]
direction TB
PKE["🔑 PasswordKeyEncryptor\nspinCount, saltValue\nencryptedVerifierHashInput\nencryptedVerifierHashValue\nencryptedKeyValue"]
CKE["🔒 CertificateKeyEncryptor\nX509Certificate\nencryptedKeyValue\ncertVerifier"]
end
subgraph EP["EncryptedPackage Stream"]
direction TB
SS["StreamSize (8 bytes, u64 LE)"]
ED["Encrypted OPC/ZIP\n4096-byte CBC segments\nper-segment IV"]
SS --> ED
end
XML --> KD
KE --> PKE
KE --> CKE
What your code needs from the <p:encryptedKey> element in the \EncryptionInfo XML:
Field
XML Attribute
Type
Example (Office 2013)
Salt
saltValue
base64 → 16 bytes
Nqj8Y...==
Spin count
spinCount
decimal integer
100000
Hash algorithm
hashAlgorithm
string
SHA512
Cipher algorithm
cipherAlgorithm
string
AES
Chaining mode
cipherChaining
string
ChainingModeCBC
Key bits
keyBits
decimal integer
256
Block size
blockSize
decimal integer
16
Hash size
hashSize
decimal integer
64
Encrypted verifier
encryptedVerifierHashInput
base64 → 16 bytes
Encrypted verifier hash
encryptedVerifierHashValue
base64 → 32-64 bytes
Encrypted key
encryptedKeyValue
base64 → 16-32 bytes
(for document decrypt)
Also from the <keyData> element (for document decryption after password verified):
Field
XML Attribute
Type
Purpose
Key data salt
saltValue
base64
Segment IV generation
Key data key bits
keyBits
decimal
Intermediate key length
HMAC key
encryptedHmacKey (on <dataIntegrity>)
base64
Integrity verification
HMAC value
encryptedHmacValue (on <dataIntegrity>)
base64
Integrity verification
Python function signature this maps to:
defverify_agile_password(password:str,salt:bytes,# from p:encryptedKey.saltValuespin_count:int,# from p:encryptedKey.spinCounthash_algo:str,# from p:encryptedKey.hashAlgorithmkey_bits:int,# from p:encryptedKey.keyBitsblock_size:int,# from p:encryptedKey.blockSizeencrypted_verifier:bytes,# from p:encryptedKey.encryptedVerifierHashInputencrypted_verifier_hash:bytes,# from p:encryptedKey.encryptedVerifierHashValuecipher_algo:str="AES",chaining:str="ChainingModeCBC",)->bool:...
CT_KeyEncryptor — wraps one key encryptor; uri attribute selects the sub-schema; at least one MUST be present; intermediate key MUST be the same across all encryptors
CT_KeyEncryptors — sequence of CT_KeyEncryptor elements; exactly one MUST exist
HMAC of the base64-decoded X509Certificate, using the Encryption.keyData hash algorithm, keyed with the intermediate key
If the intermediate key is reset, all CertificateKeyEncryptor elements MUST also be reset
The certVerifier MUST match the value calculated using the current intermediate key; if it does not match, the CertificateKeyEncryptor MUST be discarded [MS-OFFCRYPTO, SS2.3.4.10]
Number of bytes of unencrypted data (not including this field)
EncryptedData
8
variable
bytes
Encrypted payload, encrypted per SS2.3.4.15
Actual stream size may be larger than StreamSize due to block-size padding of the final segment
Data Spaces Infrastructure [MS-OFFCRYPTO, SS2.3.4.1–2.3.4.3]¶
When agile encryption is used, EncryptionName in \0x06DataSpaces\TransformInfo\StrongEncryptionTransform\0x06Primary MUST be a null string (0x00000000) [MS-OFFCRYPTO, SS2.3.4.3]
If algorithms in EncryptionTransformInfo differ from \EncryptionInfo, the \EncryptionInfo stream is authoritative [MS-OFFCRYPTO, SS2.3.4.3]
FUNCTION DeriveEncryptionKey
PARAMETERS password, salt, spinCount, blockKey, keyBits, hashAlgorithm
RETURNS encryption key as byte array
// Step 1: Initial hash
SET H = Hash(hashAlgorithm, salt + UTF16LE(password))
// Step 2: Iterated hash -- iterator PREPENDED
FOR iterator FROM 0 TO (spinCount - 1)
SET iteratorBytes = UInt32ToLittleEndianBytes(iterator) // 4 bytes
SET H = Hash(hashAlgorithm, iteratorBytes + H)
END FOR
// Step 3: Final hash with block key
SET Hfinal = Hash(hashAlgorithm, H + blockKey)
// Step 4: Key sizing
SET cbRequiredKeyLength = keyBits / 8
IF Length(Hfinal) < cbRequiredKeyLength
// Pad with 0x36
SET Hfinal = Hfinal + Repeat(0x36, cbRequiredKeyLength - Length(Hfinal))
ELSE IF Length(Hfinal) > cbRequiredKeyLength
// Truncate
SET Hfinal = Hfinal[0 .. cbRequiredKeyLength - 1]
END IF
RETURN Hfinal
END FUNCTION
Critical implementation notes:
- Iterator is an unsigned 32-bit integer, little-endian encoded (4 bytes), PREPENDED to the previous hash
- Iterator starts at 0x00000000, last value = spinCount - 1
- ⚠️ Contrast with ISO Write Protection Hash: agile PREPENDS the iterator (LE32(i) ‖ Hₙ₋₁); ISO APPENDS it (Hₙ₋₁ ‖ LE32(i))
- The padding byte for short keys is 0x36 (not 0x00) [MS-OFFCRYPTO, SS2.3.4.11]
FUNCTION GenerateIV
PARAMETERS keySalt, blockKey, blockSize, hashAlgorithm
RETURNS initialization vector as byte array
// Step 1: Compute base IV
IF blockKey is provided
SET IV = Hash(hashAlgorithm, keySalt + blockKey)
ELSE
SET IV = keySalt
END IF
// Step 2: Pad or truncate to blockSize
IF Length(IV) < blockSize
SET IV = IV + Repeat(0x36, blockSize - Length(IV))
ELSE IF Length(IV) > blockSize
SET IV = IV[0 .. blockSize - 1]
END IF
RETURN IV
END FUNCTION
keySalt = binary form of the relevant saltValue attribute
blockKey = purpose-specific constant (or absent → IV = keySalt before pad/truncate)
Padding byte is 0x36; result MUST be exactly blockSize bytes
FUNCTION GenerateEncryptedVerifierHashInput
PARAMETERS password, salt, spinCount, hashAlgorithm, cipherAlgorithm,
cipherChaining, keyBits, blockSize, saltSize
RETURNS base64-encoded encrypted verifier hash input
// Step 1: Generate random verifier
SET verifier = RandomBytes(saltSize)
// Step 2: Derive encryption key using verifierHashInput block key
SET blockKey = {0xFE, 0xA7, 0xD2, 0x76, 0x3B, 0x4B, 0x9E, 0x79}
SET key = DeriveEncryptionKey(password, salt, spinCount, blockKey, keyBits, hashAlgorithm)
// Step 3: Generate IV from PasswordKeyEncryptor.saltValue (no separate blockKey for IV)
SET iv = GenerateIV(salt, null, blockSize, hashAlgorithm)
// Note: IV = salt, padded/truncated to blockSize
// Step 4: Pad verifier to blockSize multiple
IF (Length(verifier) MOD blockSize) != 0
SET verifier = verifier + Repeat(0x00, blockSize - (Length(verifier) MOD blockSize))
END IF
// Step 5: Encrypt
SET encrypted = Encrypt(cipherAlgorithm, cipherChaining, key, iv, verifier)
RETURN Base64Encode(encrypted)
END FUNCTION
FUNCTION GenerateEncryptedVerifierHashValue
PARAMETERS password, salt, spinCount, hashAlgorithm, cipherAlgorithm,
cipherChaining, keyBits, blockSize, verifier
RETURNS base64-encoded encrypted verifier hash value
// Step 1: Hash the random verifier from encryptedVerifierHashInput step 1
SET verifierHash = Hash(hashAlgorithm, verifier)
// Step 2: Derive encryption key using verifierHashValue block key
SET blockKey = {0xD7, 0xAA, 0x0F, 0x6D, 0x30, 0x61, 0x34, 0x4E}
SET key = DeriveEncryptionKey(password, salt, spinCount, blockKey, keyBits, hashAlgorithm)
// Step 3: Generate IV from PasswordKeyEncryptor.saltValue
SET iv = GenerateIV(salt, null, blockSize, hashAlgorithm)
// Step 4: Pad hash to blockSize multiple
IF (Length(verifierHash) MOD blockSize) != 0
SET verifierHash = verifierHash + Repeat(0x00,
blockSize - (Length(verifierHash) MOD blockSize))
END IF
// Step 5: Encrypt
SET encrypted = Encrypt(cipherAlgorithm, cipherChaining, key, iv, verifierHash)
RETURN Base64Encode(encrypted)
END FUNCTION
FUNCTION RecoverIntermediateKey
PARAMETERS password, passwordKeyEncryptor
RETURNS intermediate key as byte array
SET salt = Base64Decode(passwordKeyEncryptor.saltValue)
SET spinCount = passwordKeyEncryptor.spinCount
SET hashAlg = passwordKeyEncryptor.hashAlgorithm
SET cipherAlg = passwordKeyEncryptor.cipherAlgorithm
SET chaining = passwordKeyEncryptor.cipherChaining
SET keyBits = passwordKeyEncryptor.keyBits
SET blockSize = passwordKeyEncryptor.blockSize
// Derive key for encryptedKeyValue
SET blockKey3 = {0x14, 0x6E, 0x0B, 0xE7, 0xAB, 0xAC, 0xD0, 0xD6}
SET key3 = DeriveEncryptionKey(password, salt, spinCount, blockKey3, keyBits, hashAlg)
// Generate IV
SET iv = GenerateIV(salt, null, blockSize, hashAlg)
// Decrypt intermediate key
SET encKey = Base64Decode(passwordKeyEncryptor.encryptedKeyValue)
SET intermediateKey = Decrypt(cipherAlg, chaining, key3, iv, encKey)
// Truncate to KeyData.keyBits / 8 bytes
SET intermediateKey = intermediateKey[0 .. (keyDataKeyBits / 8) - 1]
RETURN intermediateKey
END FUNCTION
Verifies encrypted data has not been tampered with
Uses HMAC [RFC2104] keyed with a randomly generated salt
The entire\EncryptedPackage stream (including the 8-byte StreamSize field) MUST be the HMAC message
FUNCTION GenerateDataIntegrity
PARAMETERS intermediateKey, keyData, encryptedPackageStream
RETURNS (encryptedHmacKey, encryptedHmacValue) as base64 strings
SET keyDataSalt = Base64Decode(keyData.saltValue)
SET blockSize = keyData.blockSize
SET cipherAlg = keyData.cipherAlgorithm
SET chaining = keyData.cipherChaining
SET hashAlg = keyData.hashAlgorithm
// Generate random HMAC salt
SET hmacSalt = RandomBytes(keyData.saltSize)
// Encrypt the HMAC salt
SET blockKey1 = {0x5F, 0xB2, 0xAD, 0x01, 0x0C, 0xB9, 0xE1, 0xF6}
SET iv1 = GenerateIV(keyDataSalt, blockKey1, blockSize, hashAlg)
IF (Length(hmacSalt) MOD blockSize) != 0
SET paddedSalt = hmacSalt + Repeat(0x00, blockSize - (Length(hmacSalt) MOD blockSize))
ELSE
SET paddedSalt = hmacSalt
END IF
SET encHmacKey = Encrypt(cipherAlg, chaining, intermediateKey, iv1, paddedSalt)
SET encryptedHmacKey = Base64Encode(encHmacKey)
// Compute HMAC of the entire EncryptedPackage stream (including StreamSize field)
SET hmacValue = HMAC(hashAlg, hmacSalt, encryptedPackageStream)
// Encrypt the HMAC value
SET blockKey2 = {0xA0, 0x67, 0x7F, 0x02, 0xB2, 0x2C, 0x84, 0x33}
SET iv2 = GenerateIV(keyDataSalt, blockKey2, blockSize, hashAlg)
IF (Length(hmacValue) MOD blockSize) != 0
SET paddedHmac = hmacValue + Repeat(0x00, blockSize - (Length(hmacValue) MOD blockSize))
ELSE
SET paddedHmac = hmacValue
END IF
SET encHmacValue = Encrypt(cipherAlg, chaining, intermediateKey, iv2, paddedHmac)
SET encryptedHmacValue = Base64Encode(encHmacValue)
RETURN (encryptedHmacKey, encryptedHmacValue)
END FUNCTION
FUNCTION EncryptPackage
PARAMETERS plaintextPackage, intermediateKey, keyData
RETURNS encrypted package stream (with StreamSize header)
SET keyDataSalt = Base64Decode(keyData.saltValue)
SET blockSize = keyData.blockSize
SET cipherAlg = keyData.cipherAlgorithm
SET chaining = keyData.cipherChaining
SET hashAlg = keyData.hashAlgorithm
SET segmentSize = 4096
WRITE UInt64ToLittleEndianBytes(Length(plaintextPackage)) TO output
SET segmentIndex = 0
SET offset = 0
WHILE offset < Length(plaintextPackage)
SET remaining = Length(plaintextPackage) - offset
IF remaining >= segmentSize
SET segment = plaintextPackage[offset .. offset + segmentSize - 1]
ELSE
SET segment = plaintextPackage[offset .. offset + remaining - 1]
IF (Length(segment) MOD blockSize) != 0
SET segment = segment + Repeat(anyByte,
blockSize - (Length(segment) MOD blockSize))
END IF
END IF
SET segmentBlockKey = UInt32ToLittleEndianBytes(segmentIndex)
SET iv = GenerateIV(keyDataSalt, segmentBlockKey, blockSize, hashAlg)
SET encryptedSegment = Encrypt(cipherAlg, chaining, intermediateKey, iv, segment)
WRITE encryptedSegment TO output
SET offset = offset + segmentSize
SET segmentIndex = segmentIndex + 1
END WHILE
RETURN output
END FUNCTION
FUNCTION DecryptPackage
PARAMETERS encryptedPackageStream, intermediateKey, keyData
RETURNS decrypted package as byte array
SET keyDataSalt = Base64Decode(keyData.saltValue)
SET blockSize = keyData.blockSize
SET cipherAlg = keyData.cipherAlgorithm
SET chaining = keyData.cipherChaining
SET hashAlg = keyData.hashAlgorithm
SET segmentSize = 4096
SET streamSize = ReadUInt64LittleEndian(encryptedPackageStream[0..7])
SET encryptedData = encryptedPackageStream[8..]
SET segmentIndex = 0
SET offset = 0
SET decrypted = empty byte array
SET totalDecrypted = 0
WHILE offset < Length(encryptedData) AND totalDecrypted < streamSize
SET remaining = streamSize - totalDecrypted
IF remaining >= segmentSize
SET encSegSize = segmentSize
ELSE
SET encSegSize = remaining
IF (encSegSize MOD blockSize) != 0
SET encSegSize = encSegSize + (blockSize - (encSegSize MOD blockSize))
END IF
END IF
SET encSegment = encryptedData[offset .. offset + encSegSize - 1]
SET segmentBlockKey = UInt32ToLittleEndianBytes(segmentIndex)
SET iv = GenerateIV(keyDataSalt, segmentBlockKey, blockSize, hashAlg)
SET decSegment = Decrypt(cipherAlg, chaining, intermediateKey, iv, encSegment)
SET bytesNeeded = Min(segmentSize, streamSize - totalDecrypted)
APPEND decSegment[0 .. bytesNeeded - 1] TO decrypted
SET totalDecrypted = totalDecrypted + bytesNeeded
SET offset = offset + encSegSize
SET segmentIndex = segmentIndex + 1
END WHILE
RETURN decrypted
END FUNCTION
FUNCTION DecryptDocument
PARAMETERS encryptionInfoStream, encryptedPackageStream, password
RETURNS decrypted OPC package as byte array, or error
// 1. Parse EncryptionInfo header
SET vMajor = ReadUInt16LE(encryptionInfoStream[0..1])
SET vMinor = ReadUInt16LE(encryptionInfoStream[2..3])
SET reserved = ReadUInt32LE(encryptionInfoStream[4..7])
ASSERT vMajor == 0x0004 AND vMinor == 0x0004 AND reserved == 0x00000040
// 2. Parse XML descriptor
SET xmlDescriptor = ParseXML(encryptionInfoStream[8..])
SET keyData = xmlDescriptor.encryption.keyData
SET dataIntegrity = xmlDescriptor.encryption.dataIntegrity
SET passwordKeyEncryptor = FindPasswordKeyEncryptor(xmlDescriptor)
// 3. Verify password
IF NOT VerifyPassword(password, passwordKeyEncryptor)
RETURN error("incorrect password")
END IF
// 4. Recover intermediate key
SET intermediateKey = RecoverIntermediateKey(password, passwordKeyEncryptor)
// 5. Verify data integrity
IF dataIntegrity is present
IF NOT VerifyDataIntegrity(intermediateKey, keyData, encryptedPackageStream)
RETURN error("data integrity check failed")
END IF
END IF
// 6. Decrypt package
SET decryptedPackage = DecryptPackage(encryptedPackageStream, intermediateKey, keyData)
RETURN decryptedPackage
END FUNCTION
CBC and integrity mandatory: agile encryption requires CBC and corruption detection (HMAC), unlike standard encryption which uses ECB and has no integrity check
Preferred hashes: SHA-2 series preferred; MD2, MD4, MD5 not recommended
Deprecated ciphers: DES not recommended; RC4 MUST NOT be used
Password limit: 255 Unicode code points
RC2 interop: key < 128 bits may cause interop issues across OS versions (<13>)
DES key encoding:keyBits MUST be 64 for 56-bit encryption; decrypted key includes DES parity bits (<14>)
DESX interop: may cause interop issues across OS versions (<15>)
3DES key encoding:keyBits MUST be 192 (168-bit) or 128 (112-bit); decrypted key includes DES parity bits (<16>, <17>)
DataIntegrity element: all Office-encrypted agile documents include it; schema allows absence for non-ECMA-376 applications (<20>)
Contrast with standard: standard encryption lacks CBC and corruption detection; agile addresses both weaknesses