Skip to main content

ravenrs_sheet/xlsx/
crypto.rs

1//! Password-encrypted workbooks. Excel wraps the ordinary OOXML package in a compound file with
2//! two streams: `EncryptionInfo` (how the key comes from the password, and a verifier that tells
3//! a wrong password) and `EncryptedPackage` (the package, encrypted with AES). Agile encryption
4//! (Excel 2010 onward) and Standard encryption (Excel 2007) are read; RC4, Extensible and
5//! certificate-only encryption are reported as unsupported.
6//!
7//! Clean-room from Microsoft's public [MS-OFFCRYPTO] specification. Every read is
8//! bounds-checked, and the costly inputs (spin count, stated sizes) are capped.
9
10use aes::cipher::{BlockCipherDecrypt, KeyInit};
11use sha2::Digest;
12
13use crate::xlsx::cfb::Compound;
14use crate::xlsx::xml::El;
15
16/// Spin counts above this are refused (the cap sheet-protection hashes use too).
17const MAX_SPIN_COUNT: u32 = 10_000_000;
18/// Standard encryption always iterates this many times.
19const STANDARD_SPIN_COUNT: u32 = 50_000;
20/// Agile packages are encrypted in segments of this many bytes, each with its own IV.
21pub(crate) const SEGMENT: usize = 4096;
22/// AES block size.
23const BLOCK: usize = 16;
24
25/// Agile block keys ([MS-OFFCRYPTO] 2.3.4.13): one per value derived from the password.
26pub(crate) const BLOCK_VERIFIER_INPUT: [u8; 8] = [0xFE, 0xA7, 0xD2, 0x76, 0x3B, 0x4B, 0x9E, 0x79];
27pub(crate) const BLOCK_VERIFIER_HASH: [u8; 8] = [0xD7, 0xAA, 0x0F, 0x6D, 0x30, 0x61, 0x34, 0x4E];
28pub(crate) const BLOCK_KEY: [u8; 8] = [0x14, 0x6E, 0x0B, 0xE7, 0xAB, 0xAC, 0xD0, 0xD6];
29/// The two dataIntegrity block keys ([MS-OFFCRYPTO] 2.3.4.14), one per HMAC field.
30pub(crate) const BLOCK_HMAC_KEY: [u8; 8] = [0x5F, 0xB2, 0xAD, 0x01, 0x0C, 0xB9, 0xE1, 0xF6];
31pub(crate) const BLOCK_HMAC_VALUE: [u8; 8] = [0xA0, 0x67, 0x7F, 0x02, 0xB2, 0x2C, 0x84, 0x33];
32
33/// The password key encryptor's URI (the other kind is certificate-based).
34pub(crate) const PASSWORD_ENCRYPTOR: &str = "http://schemas.microsoft.com/office/2006/keyEncryptor/password";
35
36/// Standard encryption header flags ([MS-OFFCRYPTO] 2.3.1).
37pub(crate) const F_CRYPTOAPI: u32 = 0x04;
38const F_EXTERNAL: u32 = 0x10;
39pub(crate) const F_AES: u32 = 0x20;
40
41/// Why an encrypted workbook couldn't be decrypted.
42#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
43pub enum DecryptError {
44    #[error("the password is incorrect")]
45    WrongPassword,
46    /// The scheme, e.g. "RC4" or "Extensible", to complete "uses {0} encryption".
47    #[error("{0} encryption isn't supported")]
48    Unsupported(String),
49    #[error("the encrypted file is damaged ({0})")]
50    Malformed(String),
51}
52
53fn malformed(m: impl Into<String>) -> DecryptError {
54    DecryptError::Malformed(m.into())
55}
56
57/// The plain OOXML package (a zip) inside an encrypted workbook.
58pub fn decrypt(bytes: &[u8], password: &str) -> Result<Vec<u8>, DecryptError> {
59    let (info, package) = streams(bytes)?;
60    decrypt_streams(&info, &package, password)
61}
62
63/// Whether the right password could decrypt this encrypted workbook, checked without one. An
64/// error names a scheme no password opens (RC4, rights management…) or damage, so callers can
65/// say so instead of asking for a password first.
66pub fn check_encryption(bytes: &[u8]) -> Result<(), DecryptError> {
67    let (info, _) = streams(bytes)?;
68    match scheme(&info)? {
69        Scheme::Agile(xml) => agile_descriptor(xml).map(|_| ()),
70        Scheme::Standard(info) => standard_header(info).map(|_| ()),
71    }
72}
73
74/// The `EncryptionInfo` and `EncryptedPackage` streams. Rights management (IRM) keeps the
75/// package but describes its encryption elsewhere, with no `EncryptionInfo`.
76fn streams(bytes: &[u8]) -> Result<(Vec<u8>, Vec<u8>), DecryptError> {
77    let cf = Compound::open(bytes).map_err(malformed)?;
78    let info = cf.root_stream("EncryptionInfo").map_err(malformed)?;
79    let package = cf.root_stream("EncryptedPackage").map_err(malformed)?.ok_or_else(|| malformed("no EncryptedPackage stream"))?;
80    let info = info.ok_or_else(|| DecryptError::Unsupported("rights-management".into()))?;
81    Ok((info, package))
82}
83
84/// Decrypts from the two streams' contents.
85pub(crate) fn decrypt_streams(info: &[u8], package: &[u8], password: &str) -> Result<Vec<u8>, DecryptError> {
86    match scheme(info)? {
87        Scheme::Agile(xml) => agile(xml, package, password),
88        Scheme::Standard(info) => standard(info, package, password),
89    }
90}
91
92/// The two schemes read, with the part of `EncryptionInfo` each one parses.
93enum Scheme<'a> {
94    /// The XML descriptor after the version and reserved field.
95    Agile(&'a [u8]),
96    /// Everything after the version.
97    Standard(&'a [u8]),
98}
99
100fn scheme(info: &[u8]) -> Result<Scheme<'_>, DecryptError> {
101    let short = || malformed("truncated EncryptionInfo");
102    let major = u16_at(info, 0).ok_or_else(short)?;
103    let minor = u16_at(info, 2).ok_or_else(short)?;
104    match (major, minor) {
105        (4, 4) => Ok(Scheme::Agile(info.get(8..).ok_or_else(short)?)),
106        (2..=4, 2) => Ok(Scheme::Standard(info.get(4..).ok_or_else(short)?)),
107        (3 | 4, 3) => Err(DecryptError::Unsupported("Extensible".into())),
108        _ => Err(DecryptError::Unsupported(format!("version {major}.{minor}"))),
109    }
110}
111
112fn u16_at(b: &[u8], at: usize) -> Option<u16> {
113    let s = b.get(at..at.checked_add(2)?)?;
114    Some(u16::from_le_bytes([*s.first()?, *s.get(1)?]))
115}
116
117fn u32_at(b: &[u8], at: usize) -> Option<u32> {
118    let s = b.get(at..at.checked_add(4)?)?;
119    Some(u32::from_le_bytes([*s.first()?, *s.get(1)?, *s.get(2)?, *s.get(3)?]))
120}
121
122/// The plaintext size an `EncryptedPackage` states, and the ciphertext after it. A size larger
123/// than the ciphertext is damage, so it never drives an allocation.
124fn package_parts(package: &[u8]) -> Result<(usize, &[u8]), DecryptError> {
125    let head: [u8; 8] = package.get(..8).and_then(|h| h.try_into().ok()).ok_or_else(|| malformed("truncated EncryptedPackage"))?;
126    let data = package.get(8..).unwrap_or_default();
127    let size = usize::try_from(u64::from_le_bytes(head)).unwrap_or(usize::MAX);
128    if size > data.len() {
129        return Err(malformed("the package is shorter than its stated size"));
130    }
131    Ok((size, data))
132}
133
134// ---- hashing -------------------------------------------------------------------------------
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub(crate) enum Hash {
138    Sha1,
139    Sha256,
140    Sha384,
141    Sha512,
142}
143
144impl Hash {
145    fn from_name(name: &str) -> Result<Hash, DecryptError> {
146        Ok(match name {
147            "SHA1" | "SHA-1" => Hash::Sha1,
148            "SHA256" | "SHA-256" => Hash::Sha256,
149            "SHA384" | "SHA-384" => Hash::Sha384,
150            "SHA512" | "SHA-512" => Hash::Sha512,
151            other => return Err(DecryptError::Unsupported(format!("{other} hash"))),
152        })
153    }
154
155    /// The digest of the concatenated `parts`.
156    pub(crate) fn digest(self, parts: &[&[u8]]) -> Vec<u8> {
157        fn run<D: Digest>(parts: &[&[u8]]) -> Vec<u8> {
158            let mut d = D::new();
159            for p in parts {
160                d.update(p);
161            }
162            d.finalize().to_vec()
163        }
164        match self {
165            Hash::Sha1 => run::<sha1::Sha1>(parts),
166            Hash::Sha256 => run::<sha2::Sha256>(parts),
167            Hash::Sha384 => run::<sha2::Sha384>(parts),
168            Hash::Sha512 => run::<sha2::Sha512>(parts),
169        }
170    }
171
172    /// The digest's length in bytes.
173    fn digest_len(self) -> usize {
174        match self {
175            Hash::Sha1 => 20,
176            Hash::Sha256 => 32,
177            Hash::Sha384 => 48,
178            Hash::Sha512 => 64,
179        }
180    }
181
182    /// The bytes the algorithm's compression function takes: 128 for SHA-384/512, else 64.
183    fn block_size(self) -> usize {
184        match self {
185            Hash::Sha384 | Hash::Sha512 => 128,
186            Hash::Sha1 | Hash::Sha256 => 64,
187        }
188    }
189}
190
191/// H0 = H(salt ‖ UTF-16LE(password)), then Hn = H(LE32(n - 1) ‖ Hn-1) for `spin` rounds.
192pub(crate) fn iterated_hash(hash: Hash, password: &str, salt: &[u8], spin: u32) -> Vec<u8> {
193    let pw: Vec<u8> = password.encode_utf16().flat_map(u16::to_le_bytes).collect();
194    let mut h = hash.digest(&[salt, &pw]);
195    for i in 0..spin {
196        h = hash.digest(&[&i.to_le_bytes(), &h]);
197    }
198    h
199}
200
201/// H(a ‖ b), truncated or padded with 0x36 to `len`: Agile's derived keys and IVs.
202pub(crate) fn derive(hash: Hash, a: &[u8], b: &[u8], len: usize) -> Vec<u8> {
203    let mut k = hash.digest(&[a, b]);
204    k.resize(len, 0x36);
205    k
206}
207
208/// HMAC-SHA-`hash` ([RFC2104]), as Agile's dataIntegrity uses.
209pub(crate) fn hmac(hash: Hash, key: &[u8], message: &[u8]) -> Vec<u8> {
210    let bs = hash.block_size();
211    let mut k = key.to_vec();
212    if k.len() > bs {
213        k = hash.digest(&[&k]);
214    }
215    k.resize(bs, 0);
216    let pad = |x: u8| k.iter().map(|b| b ^ x).collect::<Vec<u8>>();
217    let inner = hash.digest(&[&pad(0x36), message]);
218    hash.digest(&[&pad(0x5C), &inner])
219}
220
221/// The Standard encryption key ([MS-OFFCRYPTO] 2.3.4.7): SHA-1 iterated 50,000 times, the
222/// block-0 hash, then the 0x36 / 0x5C expansion, truncated to `key_len`.
223pub(crate) fn standard_key(password: &str, salt: &[u8], key_len: usize) -> Vec<u8> {
224    let h = iterated_hash(Hash::Sha1, password, salt, STANDARD_SPIN_COUNT);
225    let h = Hash::Sha1.digest(&[&h, &0u32.to_le_bytes()]);
226    let expand = |pad: u8| {
227        let mut buf = [pad; 64];
228        for (b, x) in buf.iter_mut().zip(&h) {
229            *b ^= x;
230        }
231        Hash::Sha1.digest(&[&buf])
232    };
233    let mut key = expand(0x36);
234    key.extend(expand(0x5C));
235    key.truncate(key_len);
236    key
237}
238
239// ---- AES -----------------------------------------------------------------------------------
240
241enum Aes {
242    A128(aes::Aes128),
243    A192(aes::Aes192),
244    A256(aes::Aes256),
245}
246
247impl Aes {
248    fn new(key: &[u8]) -> Result<Aes, DecryptError> {
249        let bad = |_| malformed("bad AES key");
250        Ok(match key.len() {
251            16 => Aes::A128(aes::Aes128::new_from_slice(key).map_err(bad)?),
252            24 => Aes::A192(aes::Aes192::new_from_slice(key).map_err(bad)?),
253            32 => Aes::A256(aes::Aes256::new_from_slice(key).map_err(bad)?),
254            n => return Err(malformed(format!("a {n}-byte AES key"))),
255        })
256    }
257
258    fn decrypt_block(&self, block: &mut [u8; BLOCK]) {
259        let b = aes::Block::cast_from_core_mut(block);
260        match self {
261            Aes::A128(c) => c.decrypt_block(b),
262            Aes::A192(c) => c.decrypt_block(b),
263            Aes::A256(c) => c.decrypt_block(b),
264        }
265    }
266
267    /// ECB: each block on its own. `data` must be whole blocks.
268    fn decrypt_ecb(&self, data: &mut [u8]) -> Result<(), DecryptError> {
269        let (blocks, rest) = data.as_chunks_mut::<BLOCK>();
270        if !rest.is_empty() {
271            return Err(malformed("ciphertext isn't whole AES blocks"));
272        }
273        blocks.iter_mut().for_each(|b| self.decrypt_block(b));
274        Ok(())
275    }
276
277    /// CBC: each block XORed with the previous ciphertext block (the IV for the first).
278    fn decrypt_cbc(&self, iv: &[u8], data: &mut [u8]) -> Result<(), DecryptError> {
279        let mut prev: [u8; BLOCK] = iv.try_into().map_err(|_| malformed("an IV that isn't one AES block"))?;
280        let (blocks, rest) = data.as_chunks_mut::<BLOCK>();
281        if !rest.is_empty() {
282            return Err(malformed("ciphertext isn't whole AES blocks"));
283        }
284        for b in blocks {
285            let cipher = *b;
286            self.decrypt_block(b);
287            b.iter_mut().zip(prev).for_each(|(x, p)| *x ^= p);
288            prev = cipher;
289        }
290        Ok(())
291    }
292}
293
294// ---- Standard encryption -------------------------------------------------------------------
295
296/// What a Standard `EncryptionInfo` holds: the key size and the password verifier.
297struct StandardHeader<'a> {
298    key_len: usize,
299    salt: &'a [u8],
300    verifier: &'a [u8],
301    verifier_hash: &'a [u8],
302}
303
304/// `info` starts after the version: flags, header size, header, verifier.
305fn standard_header(info: &[u8]) -> Result<StandardHeader<'_>, DecryptError> {
306    let short = || malformed("truncated Standard encryption header");
307    let flags = u32_at(info, 0).ok_or_else(short)?;
308    if flags & F_EXTERNAL != 0 {
309        return Err(DecryptError::Unsupported("external".into()));
310    }
311    if flags & F_AES == 0 || flags & F_CRYPTOAPI == 0 {
312        return Err(DecryptError::Unsupported("RC4".into()));
313    }
314    let header_size = usize::try_from(u32_at(info, 4).ok_or_else(short)?).map_err(|_| short())?;
315    let header = info.get(8..8usize.checked_add(header_size).ok_or_else(short)?).ok_or_else(short)?;
316    let alg_id = u32_at(header, 8).ok_or_else(short)?;
317    let alg_hash = u32_at(header, 12).ok_or_else(short)?;
318    let key_bits = u32_at(header, 16).ok_or_else(short)?;
319    let key_len = match (alg_id, key_bits) {
320        (0x660E | 0, 128) => 16,
321        (0x660F, 192) => 24,
322        (0x6610, 256) => 32,
323        (0x660E..=0x6610, _) => return Err(malformed(format!("AES with a {key_bits}-bit key"))),
324        _ => return Err(DecryptError::Unsupported(format!("cipher 0x{alg_id:04X}"))),
325    };
326    if alg_hash != 0x8004 && alg_hash != 0 {
327        return Err(DecryptError::Unsupported(format!("hash 0x{alg_hash:04X}")));
328    }
329    // Verifier: salt size, salt, encrypted verifier, verifier hash size, encrypted hash.
330    let v = info.get(8 + header_size..).ok_or_else(short)?;
331    if u32_at(v, 0) != Some(16) {
332        return Err(malformed("a Standard salt that isn't 16 bytes"));
333    }
334    let salt = v.get(4..20).ok_or_else(short)?;
335    let verifier = v.get(20..36).ok_or_else(short)?;
336    let verifier_hash = v.get(40..72).ok_or_else(short)?;
337    Ok(StandardHeader { key_len, salt, verifier, verifier_hash })
338}
339
340fn standard(info: &[u8], package: &[u8], password: &str) -> Result<Vec<u8>, DecryptError> {
341    let h = standard_header(info)?;
342    let (mut verifier, mut verifier_hash) = (h.verifier.to_vec(), h.verifier_hash.to_vec());
343    let aes = Aes::new(&standard_key(password, h.salt, h.key_len))?;
344    aes.decrypt_ecb(&mut verifier)?;
345    aes.decrypt_ecb(&mut verifier_hash)?;
346    if verifier_hash.get(..20) != Some(Hash::Sha1.digest(&[&verifier]).as_slice()) {
347        return Err(DecryptError::WrongPassword);
348    }
349    let (size, data) = package_parts(package)?;
350    let mut out = data.get(..size.div_ceil(BLOCK) * BLOCK).ok_or_else(|| malformed("the package isn't whole AES blocks"))?.to_vec();
351    aes.decrypt_ecb(&mut out)?;
352    out.truncate(size);
353    Ok(out)
354}
355
356// ---- Agile encryption ----------------------------------------------------------------------
357
358/// The cipher parameters shared by `keyData` and a password `encryptedKey` element.
359pub(crate) struct Params {
360    pub(crate) hash: Hash,
361    pub(crate) salt: Vec<u8>,
362    pub(crate) key_len: usize,
363}
364
365impl Params {
366    fn read(e: &El) -> Result<Params, DecryptError> {
367        let cipher = e.attr("cipherAlgorithm").unwrap_or("AES");
368        if cipher != "AES" {
369            return Err(DecryptError::Unsupported(cipher.into()));
370        }
371        let chaining = e.attr("cipherChaining").unwrap_or("ChainingModeCBC");
372        if chaining != "ChainingModeCBC" {
373            return Err(DecryptError::Unsupported(format!("AES {chaining}")));
374        }
375        if e.attr_u32("blockSize") != Some(16) {
376            return Err(malformed("an AES block size that isn't 16"));
377        }
378        let key_len = match e.attr_u32("keyBits") {
379            Some(128) => 16,
380            Some(192) => 24,
381            Some(256) => 32,
382            other => return Err(malformed(format!("AES with keyBits {}", other.unwrap_or(0)))),
383        };
384        let hash = Hash::from_name(e.attr("hashAlgorithm").unwrap_or("SHA1"))?;
385        let salt = base64(e.attr("saltValue").ok_or_else(|| malformed("no saltValue"))?)?;
386        if salt.is_empty() || salt.len() > 64 {
387            return Err(malformed("a salt that isn't 1 to 64 bytes"));
388        }
389        Ok(Params { hash, salt, key_len })
390    }
391
392    /// An IV: H(salt ‖ block key) for a block key, else the salt, sized to one AES block.
393    pub(crate) fn iv(&self, block_key: Option<&[u8]>) -> Vec<u8> {
394        match block_key {
395            Some(k) => derive(self.hash, &self.salt, k, BLOCK),
396            None => {
397                let mut iv = self.salt.clone();
398                iv.resize(BLOCK, 0x36);
399                iv
400            }
401        }
402    }
403}
404
405/// What an Agile descriptor holds: the package's cipher, the password key encryptor with its
406/// three encrypted values (verifier input, verifier hash, package key), and the package's
407/// integrity pair, which Excel always writes and the reader requires.
408struct AgileDescriptor {
409    key_data: Params,
410    key: Params,
411    spin: u32,
412    verifier_input: Vec<u8>,
413    verifier_hash: Vec<u8>,
414    key_value: Vec<u8>,
415    hmac_key: Vec<u8>,
416    hmac_value: Vec<u8>,
417}
418
419/// `xml` is the XML encryption descriptor after the 8-byte version and reserved field.
420fn agile_descriptor(xml: &[u8]) -> Result<AgileDescriptor, DecryptError> {
421    let root = crate::xlsx::xml::parse(xml).map_err(|e| malformed(e.to_string()))?;
422    let key_data = Params::read(root.child("keyData").ok_or_else(|| malformed("no keyData"))?)?;
423    let encryptor = root
424        .child("keyEncryptors")
425        .and_then(|k| k.kids("keyEncryptor").find(|e| e.attr("uri") == Some(PASSWORD_ENCRYPTOR)))
426        .ok_or_else(|| DecryptError::Unsupported("certificate".into()))?
427        .child("encryptedKey")
428        .ok_or_else(|| malformed("no encryptedKey"))?;
429    let key = Params::read(encryptor)?;
430    let spin = encryptor.attr_u32("spinCount").ok_or_else(|| malformed("no spinCount"))?;
431    if spin > MAX_SPIN_COUNT {
432        return Err(malformed(format!("a spin count of {spin}, more than the {MAX_SPIN_COUNT} allowed")));
433    }
434    let field = |name: &str| base64(encryptor.attr(name).ok_or_else(|| malformed(format!("no {name}")))?);
435    // A descriptor without dataIntegrity can't prove its ciphertext is the one the password
436    // protects: the verifier only authenticates the key, not the package ([MS-OFFCRYPTO] 2.3.4.14).
437    let integrity = root.child("dataIntegrity").ok_or_else(|| malformed("no dataIntegrity"))?;
438    let hmac_field = |attr: &str| {
439        let v = base64(integrity.attr(attr).ok_or_else(|| malformed(format!("no {attr}")))?)?;
440        if v.is_empty() || v.len() % BLOCK != 0 {
441            return Err(malformed(format!("a whole-block {attr}")));
442        }
443        Ok(v)
444    };
445    Ok(AgileDescriptor {
446        key_data,
447        key,
448        spin,
449        verifier_input: field("encryptedVerifierHashInput")?,
450        verifier_hash: field("encryptedVerifierHashValue")?,
451        key_value: field("encryptedKeyValue")?,
452        hmac_key: hmac_field("encryptedHmacKey")?,
453        hmac_value: hmac_field("encryptedHmacValue")?,
454    })
455}
456
457/// Decrypts an Agile package ([MS-OFFCRYPTO] 2.3.4.10): checks the password against the
458/// verifier, unwraps the package key, verifies the dataIntegrity HMAC, then decrypts the segments.
459fn agile(xml: &[u8], package: &[u8], password: &str) -> Result<Vec<u8>, DecryptError> {
460    let descriptor = agile_descriptor(xml)?;
461    let AgileDescriptor { key_data, key, spin, verifier_input, verifier_hash, key_value, hmac_key, hmac_value } = descriptor;
462    let h = iterated_hash(key.hash, password, &key.salt, spin);
463    let iv = key.iv(None);
464    let field = |data: &[u8], block_key: &[u8]| -> Result<Vec<u8>, DecryptError> {
465        let mut data = data.to_vec();
466        Aes::new(&derive(key.hash, &h, block_key, key.key_len))?.decrypt_cbc(&iv, &mut data)?;
467        Ok(data)
468    };
469    let input = field(&verifier_input, &BLOCK_VERIFIER_INPUT)?;
470    let expected = field(&verifier_hash, &BLOCK_VERIFIER_HASH)?;
471    let input = input.get(..key.salt.len()).unwrap_or(&input);
472    let actual = key.hash.digest(&[input]);
473    if expected.get(..actual.len()) != Some(actual.as_slice()) {
474        return Err(DecryptError::WrongPassword);
475    }
476    let secret = field(&key_value, &BLOCK_KEY)?;
477    let aes = Aes::new(secret.get(..key_data.key_len).ok_or_else(|| malformed("a short encryptedKeyValue"))?)?;
478    // Package integrity, [MS-OFFCRYPTO] 2.3.4.14: both HMAC fields are encrypted with the
479    // package key (the verifier above only authenticates the password), computed over the whole
480    // EncryptedPackage stream, StreamSize field included. The specification keys the HMAC with a
481    // random salt-size array, but Excel's key is as long as the hash (64 bytes for SHA-512), so
482    // both lengths are tried.
483    let mut mac_field = hmac_key;
484    aes.decrypt_cbc(&key_data.iv(Some(&BLOCK_HMAC_KEY)), &mut mac_field)?;
485    let mut declared = hmac_value;
486    aes.decrypt_cbc(&key_data.iv(Some(&BLOCK_HMAC_VALUE)), &mut declared)?;
487    let verified = [key_data.hash.digest_len(), key_data.salt.len()].into_iter().any(|len| {
488        let Some(key) = mac_field.get(..len) else { return false };
489        let mac = hmac(key_data.hash, key, package);
490        declared.get(..mac.len()) == Some(mac.as_slice())
491    });
492    if !verified {
493        return Err(malformed("the decrypted data failed its integrity check"));
494    }
495    let (size, data) = package_parts(package)?;
496    let mut out = Vec::with_capacity(size);
497    for (i, segment) in data.chunks(SEGMENT).enumerate() {
498        if out.len() >= size {
499            break;
500        }
501        let index = u32::try_from(i).map_err(|_| malformed("too many segments"))?;
502        let start = out.len();
503        out.extend_from_slice(segment);
504        let block = out.get_mut(start..).unwrap_or_default();
505        aes.decrypt_cbc(&key_data.iv(Some(&index.to_le_bytes())), block)?;
506    }
507    out.truncate(size);
508    Ok(out)
509}
510
511// ---- base64 --------------------------------------------------------------------------------
512
513/// Standard base64 (whitespace ignored), as Agile's XML uses for every binary value.
514fn base64(s: &str) -> Result<Vec<u8>, DecryptError> {
515    let mut out = Vec::with_capacity(s.len() / 4 * 3);
516    let (mut buf, mut bits) = (0u32, 0u32);
517    for c in s.bytes().filter(|c| !c.is_ascii_whitespace() && *c != b'=') {
518        let v = match c {
519            b'A'..=b'Z' => c - b'A',
520            b'a'..=b'z' => c - b'a' + 26,
521            b'0'..=b'9' => c - b'0' + 52,
522            b'+' => 62,
523            b'/' => 63,
524            _ => return Err(malformed("invalid base64")),
525        };
526        buf = (buf << 6) | u32::from(v);
527        bits += 6;
528        if bits >= 8 {
529            bits -= 8;
530            out.push((buf >> bits) as u8);
531            buf &= (1 << bits) - 1;
532        }
533    }
534    Ok(out)
535}
536
537#[cfg(test)]
538mod tests;