Skip to main content

kc/
crypto.rs

1//! Keychain cryptography: master key derivation, the database blob, wrapped
2//! item keys, and item secrets.
3//!
4//! The dtformats specification covers the container but not the encryption, so
5//! the blob layouts here come from Apple's own `ssblob.h`
6//! (`Security/OSX/libsecurityd/lib/ssblob.h`, `CommonBlob`/`DbBlob`/`KeyBlob`)
7//! and the key-unwrap sequence from `securityd`'s `BLOBFORMAT` notes as
8//! implemented by `chainbreaker`. Every value here is checked against keychains
9//! written by macOS in `tests/keychain_crypto.rs`.
10//!
11//! The chain is:
12//!
13//! ```text
14//! password --PBKDF2-SHA1(salt, 1000, 24)--> master key
15//! master key --3DES-CBC(DbBlob.iv)--> DbBlob crypto blob
16//!                                     -> encryption key (24) + signing key (20)
17//! encryption key --unwrap(KeyBlob)--> item key
18//! item key --3DES-CBC(SSGP.iv)--> item secret
19//! ```
20//!
21//! Everything is 3DES (EDE, three-key) in CBC mode. That is what the format
22//! specifies; it is not a choice this code makes.
23
24use cbc::cipher::block_padding::NoPadding;
25use cbc::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
26use des::TdesEde3;
27use hmac::Hmac;
28use sha1::Sha1;
29
30use crate::acl::AclBlob;
31use crate::cssm::{KeyHeader, WrappedKeyFields};
32use crate::error::{Error, Result};
33
34type Decryptor = cbc::Decryptor<TdesEde3>;
35type Encryptor = cbc::Encryptor<TdesEde3>;
36
37/// `CommonBlob::magicNumber`.
38pub const BLOB_MAGIC: u32 = 0xfade_0711;
39
40/// `CommonBlob::version_MacOS_10_0`. Blobs at this version — and only this
41/// version — are signed with [`legacy_hmac_sha1`].
42pub const BLOB_VERSION_MACOS_10_0: u32 = 0x0000_0100;
43
44/// `CommonBlob::version_MacOS_10_1`.
45pub const BLOB_VERSION_MACOS_10_1: u32 = 0x0000_0101;
46
47/// `CommonBlob::version_partition`, written for keychains under
48/// `~/Library/Keychains` since macOS 10.11.4. Signed with real HMAC-SHA1.
49pub const BLOB_VERSION_PARTITION: u32 = 0x0000_0200;
50
51/// The version this code writes for a new keychain, matching what
52/// `security create-keychain` writes outside `~/Library/Keychains`.
53pub const BLOB_VERSION: u32 = BLOB_VERSION_MACOS_10_0;
54
55/// 3DES block size, and so the IV size for the item and database blobs.
56pub const BLOCK_SIZE: usize = 8;
57
58/// 3DES-EDE3 key length.
59pub const KEY_LEN: usize = 24;
60
61/// PBKDF2 salt length in a `DbBlob`.
62pub const SALT_LEN: usize = 20;
63
64/// PBKDF2 iteration count. Fixed by the format, and by today's standards far
65/// too low — see the security notes in `README.md`.
66pub const PBKDF2_ITERATIONS: u32 = 1000;
67
68/// `sizeof(DbBlob)`: the fixed part, before the public ACL.
69pub const DB_BLOB_LEN: usize = 92;
70
71/// `sizeof(KeyBlob)`: the fixed part, before the public ACL.
72pub const KEY_BLOB_LEN: usize = 136;
73
74/// The fixed IV Apple's key wrapping uses for its first pass. From
75/// `libsecurity_keychain`'s `KeyItem.cpp`; not a nonce, and not secret.
76pub const MAGIC_CMS_IV: [u8; 8] = [0x4a, 0xdd, 0xa2, 0x2c, 0x79, 0xe8, 0x21, 0x05];
77
78/// Bytes that mark an item's secure-storage group.
79pub const SSGP_MAGIC: &[u8; 4] = b"ssgp";
80
81/// A secret that is zeroed when dropped.
82pub use crate::secret::SecretBytes;
83
84/// Derive the master key from a keychain password.
85pub fn master_key(password: &[u8], salt: &[u8]) -> SecretBytes {
86    let mut key = [0u8; KEY_LEN];
87    pbkdf2::pbkdf2::<Hmac<Sha1>>(password, salt, PBKDF2_ITERATIONS, &mut key)
88        .expect("PBKDF2 output length is valid");
89    SecretBytes::new(key.as_slice())
90}
91
92/// 3DES-CBC decrypt, then strip and verify the padding.
93///
94/// The padding is the CSSM variant: every byte holds the pad length, which is
95/// 1..=8. A bad pad byte is how a wrong password shows up, so it is reported as
96/// a distinct error rather than as garbage.
97pub fn decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>> {
98    if key.len() != KEY_LEN {
99        return Err(Error::Crypto("3DES key must be 24 bytes"));
100    }
101    if iv.len() != BLOCK_SIZE {
102        return Err(Error::Crypto("3DES IV must be 8 bytes"));
103    }
104    if data.is_empty() || !data.len().is_multiple_of(BLOCK_SIZE) {
105        return Err(Error::Crypto(
106            "ciphertext length is not a multiple of the block size",
107        ));
108    }
109
110    let mut buffer = data.to_vec();
111    Decryptor::new_from_slices(key, iv)
112        .map_err(|_| Error::Crypto("invalid 3DES key or IV"))?
113        .decrypt_padded_mut::<NoPadding>(&mut buffer)
114        .map_err(|_| Error::Crypto("3DES decryption failed"))?;
115
116    let pad = *buffer.last().expect("non-empty") as usize;
117    if pad == 0 || pad > BLOCK_SIZE || pad > buffer.len() {
118        return Err(Error::WrongPassword);
119    }
120    if buffer[buffer.len() - pad..]
121        .iter()
122        .any(|byte| *byte as usize != pad)
123    {
124        return Err(Error::WrongPassword);
125    }
126    buffer.truncate(buffer.len() - pad);
127    Ok(buffer)
128}
129
130/// 3DES-CBC encrypt with the same padding scheme [`decrypt`] expects.
131pub fn encrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>> {
132    if key.len() != KEY_LEN {
133        return Err(Error::Crypto("3DES key must be 24 bytes"));
134    }
135    if iv.len() != BLOCK_SIZE {
136        return Err(Error::Crypto("3DES IV must be 8 bytes"));
137    }
138
139    // A full block of padding is added when the input is already aligned, so
140    // that the length is always recoverable.
141    let pad = BLOCK_SIZE - (data.len() % BLOCK_SIZE);
142    let mut buffer = Vec::with_capacity(data.len() + pad);
143    buffer.extend_from_slice(data);
144    buffer.extend(std::iter::repeat_n(pad as u8, pad));
145
146    let length = buffer.len();
147    Encryptor::new_from_slices(key, iv)
148        .map_err(|_| Error::Crypto("invalid 3DES key or IV"))?
149        .encrypt_padded_mut::<NoPadding>(&mut buffer, length)
150        .map_err(|_| Error::Crypto("3DES encryption failed"))?;
151    Ok(buffer)
152}
153
154/// Sign a blob the way `securityd` signs one at `version`.
155///
156/// `dbcrypto.cpp` picks `CSSM_ALGID_SHA1HMAC_LEGACY` when — and only when — the
157/// blob is at [`BLOB_VERSION_MACOS_10_0`]; every other version, including the
158/// partition version that keychains under `~/Library/Keychains` carry, is signed
159/// with real HMAC-SHA1. Using the wrong one produces a signature `securityd`
160/// rejects, so the version in the blob decides, not a compile-time choice.
161pub fn sign_blob(version: u32, key: &[u8], chunks: &[&[u8]]) -> [u8; 20] {
162    if version == BLOB_VERSION_MACOS_10_0 {
163        return legacy_hmac_sha1(key, chunks);
164    }
165    let mut mac =
166        <Hmac<Sha1> as hmac::Mac>::new_from_slice(key).expect("HMAC takes any key length");
167    for chunk in chunks {
168        hmac::Mac::update(&mut mac, chunk);
169    }
170    hmac::Mac::finalize(mac).into_bytes().into()
171}
172
173/// HMAC-SHA1 as Apple's `CSSM_ALGID_SHA1HMAC_LEGACY` computes it, over a list of
174/// chunks.
175///
176/// Blobs at version [`BLOB_VERSION`] are signed with this rather than with real
177/// HMAC — `securityd/src/dbcrypto.cpp` selects it for
178/// `version_MacOS_10_0` and calls it "BSafe bug compatibility". The bug, in
179/// `libsecurity_cryptkit/lib/HmacSha1Legacy.c`, is that `hmacLegacyUpdate`
180/// computes an entire HMAC on *every* call while leaving the outer hash context
181/// in place, so signing two chunks folds the first chunk's result into the
182/// second's inner hash:
183///
184/// ```text
185/// inner_1 = SHA1(k_ipad || chunk_1)
186/// inner_2 = SHA1(k_opad || inner_1 || k_ipad || chunk_2)
187/// mac     = SHA1(k_opad || inner_2)
188/// ```
189///
190/// With a single chunk it degenerates to standard HMAC-SHA1. The keychain
191/// signers always pass two, so this is not equivalent to HMAC and must be
192/// reproduced exactly. Verified against keychains written by macOS in
193/// `tests/keychain_crypto.rs`.
194pub fn legacy_hmac_sha1(key: &[u8], chunks: &[&[u8]]) -> [u8; 20] {
195    use sha1::Digest as _;
196
197    // The key is XORed into a full 64-byte block; bytes past the key are the pad
198    // byte itself, which is what `0x00 ^ pad` comes to.
199    let mut k_ipad = [0x36u8; 64];
200    let mut k_opad = [0x5cu8; 64];
201    for (index, byte) in key.iter().take(64).enumerate() {
202        k_ipad[index] = byte ^ 0x36;
203        k_opad[index] = byte ^ 0x5c;
204    }
205
206    let mut context = Sha1::new();
207    for chunk in chunks {
208        context.update(k_ipad);
209        context.update(chunk);
210        let inner = context.finalize_reset();
211        // The reference implementation reinitializes here and then starts the
212        // outer hash, but never finalizes it before the next chunk.
213        context.update(k_opad);
214        context.update(inner);
215    }
216    context.finalize().into()
217}
218
219/// The fixed part of a `DbBlob`, the record that makes a keychain unlockable.
220#[derive(Debug, Clone)]
221pub struct DbBlob {
222    pub version: u32,
223    /// Offset of the encrypted region; also the end of the public ACL.
224    pub start_crypto_blob: u32,
225    pub total_length: u32,
226    pub random_signature: [u8; 16],
227    pub sequence: u32,
228    pub idle_timeout: u32,
229    pub lock_on_sleep: bool,
230    pub salt: [u8; SALT_LEN],
231    pub iv: [u8; BLOCK_SIZE],
232    /// Legacy HMAC-SHA1 of the blob, keyed by the signing key. securityd
233    /// refuses to unlock a keychain whose signature does not match, so this is
234    /// computed on write; see [`Self::sign`].
235    pub blob_signature: [u8; 20],
236    /// Public ACL, between the fixed part and the encrypted region. This one
237    /// does not follow the item-ACL layout, so it is carried as bytes.
238    pub public_acl: Vec<u8>,
239    /// The encrypted region: the database's own keys.
240    pub crypto_blob: Vec<u8>,
241}
242
243impl DbBlob {
244    pub fn parse(data: &[u8]) -> Result<Self> {
245        if data.len() < DB_BLOB_LEN {
246            return Err(Error::format(
247                "database blob is shorter than its fixed header",
248            ));
249        }
250        let magic = be32(data, 0);
251        if magic != BLOB_MAGIC {
252            return Err(Error::format(format!(
253                "database blob magic is 0x{magic:08x}, expected 0x{BLOB_MAGIC:08x}"
254            )));
255        }
256
257        let start_crypto_blob = be32(data, 8);
258        let total_length = be32(data, 12);
259        if (start_crypto_blob as usize) < DB_BLOB_LEN
260            || total_length < start_crypto_blob
261            || total_length as usize > data.len()
262        {
263            return Err(Error::format("database blob offsets are inconsistent"));
264        }
265
266        Ok(Self {
267            version: be32(data, 4),
268            start_crypto_blob,
269            total_length,
270            random_signature: data[16..32].try_into().expect("16 bytes"),
271            sequence: be32(data, 32),
272            idle_timeout: be32(data, 36),
273            lock_on_sleep: data[40] != 0,
274            salt: data[44..64].try_into().expect("20 bytes"),
275            iv: data[64..72].try_into().expect("8 bytes"),
276            blob_signature: data[72..92].try_into().expect("20 bytes"),
277            public_acl: data[DB_BLOB_LEN..start_crypto_blob as usize].to_vec(),
278            crypto_blob: data[start_crypto_blob as usize..total_length as usize].to_vec(),
279        })
280    }
281
282    pub fn to_bytes(&self) -> Vec<u8> {
283        let start_crypto_blob = (DB_BLOB_LEN + self.public_acl.len()) as u32;
284        let total_length = start_crypto_blob + self.crypto_blob.len() as u32;
285
286        let mut out = Vec::with_capacity(total_length as usize);
287        out.extend_from_slice(&BLOB_MAGIC.to_be_bytes());
288        out.extend_from_slice(&self.version.to_be_bytes());
289        out.extend_from_slice(&start_crypto_blob.to_be_bytes());
290        out.extend_from_slice(&total_length.to_be_bytes());
291        out.extend_from_slice(&self.random_signature);
292        out.extend_from_slice(&self.sequence.to_be_bytes());
293        out.extend_from_slice(&self.idle_timeout.to_be_bytes());
294        // DBParameters is { uint32 idleTimeout; uint8 lockOnSleep; }, which the
295        // compiler pads to 8 bytes.
296        out.push(u8::from(self.lock_on_sleep));
297        out.extend_from_slice(&[0, 0, 0]);
298        out.extend_from_slice(&self.salt);
299        out.extend_from_slice(&self.iv);
300        out.extend_from_slice(&self.blob_signature);
301        out.extend_from_slice(&self.public_acl);
302        out.extend_from_slice(&self.crypto_blob);
303        out
304    }
305
306    /// Unlock: derive the master key and open the crypto blob.
307    pub fn unlock(&self, password: &[u8]) -> Result<DbKeys> {
308        let master = master_key(password, &self.salt);
309        let plain = decrypt(master.as_slice(), &self.iv, &self.crypto_blob)?;
310        DbKeys::parse(&plain)
311    }
312
313    /// Build the crypto blob for a set of database keys, and sign the result.
314    pub fn seal(&mut self, password: &[u8], keys: &DbKeys) -> Result<()> {
315        let master = master_key(password, &self.salt);
316        self.crypto_blob = encrypt(master.as_slice(), &self.iv, &keys.to_bytes())?;
317        self.start_crypto_blob = (DB_BLOB_LEN + self.public_acl.len()) as u32;
318        self.total_length = self.start_crypto_blob + self.crypto_blob.len() as u32;
319        self.sign(keys.signing_key.as_slice());
320        Ok(())
321    }
322
323    /// Offset of `blobSignature` within the blob: where the first signed chunk
324    /// ends.
325    const SIGNATURE_OFFSET: usize = 72;
326
327    /// The two chunks securityd signs: everything before the signature field,
328    /// then the public ACL and crypto blob (skipping the signature itself and
329    /// the fields between).
330    fn signed_chunks(bytes: &[u8]) -> [&[u8]; 2] {
331        [&bytes[..Self::SIGNATURE_OFFSET], &bytes[DB_BLOB_LEN..]]
332    }
333
334    /// Recompute `blob_signature`. Call after any change to the blob.
335    pub fn sign(&mut self, signing_key: &[u8]) {
336        self.blob_signature = [0u8; 20];
337        let bytes = self.to_bytes();
338        self.blob_signature = sign_blob(self.version, signing_key, &Self::signed_chunks(&bytes));
339    }
340
341    /// True when the stored signature matches the blob's contents.
342    pub fn verify(&self, signing_key: &[u8]) -> bool {
343        let bytes = self.to_bytes();
344        sign_blob(self.version, signing_key, &Self::signed_chunks(&bytes)) == self.blob_signature
345    }
346}
347
348/// `DbBlob::PrivateBlob`: the keys that protect everything in the keychain.
349#[derive(Debug, Clone)]
350pub struct DbKeys {
351    /// Wraps the per-item keys.
352    pub encryption_key: SecretBytes,
353    /// Signs blobs. Kept so a re-serialized keychain preserves it.
354    pub signing_key: SecretBytes,
355    /// Private ACL, which follows the keys to the end of the blob. Opaque.
356    pub private_acl: Vec<u8>,
357}
358
359impl DbKeys {
360    pub fn parse(plain: &[u8]) -> Result<Self> {
361        if plain.len() < KEY_LEN + 20 {
362            return Err(Error::Crypto("database key blob is too short"));
363        }
364        Ok(Self {
365            encryption_key: SecretBytes::new(&plain[..KEY_LEN]),
366            signing_key: SecretBytes::new(&plain[KEY_LEN..KEY_LEN + 20]),
367            private_acl: plain[KEY_LEN + 20..].to_vec(),
368        })
369    }
370
371    pub fn to_bytes(&self) -> Vec<u8> {
372        let mut out = Vec::with_capacity(KEY_LEN + 20 + self.private_acl.len());
373        out.extend_from_slice(self.encryption_key.as_slice());
374        out.extend_from_slice(self.signing_key.as_slice());
375        out.extend_from_slice(&self.private_acl);
376        out
377    }
378}
379
380/// The fixed part of a `KeyBlob`: one wrapped key, as stored in a key record.
381#[derive(Debug, Clone)]
382pub struct KeyBlob {
383    pub version: u32,
384    pub start_crypto_blob: u32,
385    pub total_length: u32,
386    pub iv: [u8; BLOCK_SIZE],
387    /// The key's CSSM header.
388    pub header: KeyHeader,
389    /// How the key that follows was wrapped.
390    pub wrapped: WrappedKeyFields,
391    pub blob_signature: [u8; 20],
392    /// The item ACL. Parsed when it follows the layout in [`AclBlob`], and kept
393    /// as bytes when it does not, so an unfamiliar policy still round-trips.
394    pub public_acl: PublicAcl,
395    pub crypto_blob: Vec<u8>,
396}
397
398/// A public ACL: understood, or preserved as-is.
399#[derive(Debug, Clone, PartialEq, Eq)]
400pub enum PublicAcl {
401    Parsed(AclBlob),
402    Raw(Vec<u8>),
403}
404
405impl PublicAcl {
406    pub fn parse(data: &[u8]) -> Self {
407        match AclBlob::parse(data) {
408            Ok(blob) if blob.to_bytes() == data => Self::Parsed(blob),
409            // Anything this does not fully understand is carried unchanged
410            // rather than reformatted into something subtly different.
411            _ => Self::Raw(data.to_vec()),
412        }
413    }
414
415    pub fn to_bytes(&self) -> Vec<u8> {
416        match self {
417            Self::Parsed(blob) => blob.to_bytes(),
418            Self::Raw(bytes) => bytes.clone(),
419        }
420    }
421
422    pub fn len(&self) -> usize {
423        match self {
424            Self::Parsed(blob) => blob.encoded_len(),
425            Self::Raw(bytes) => bytes.len(),
426        }
427    }
428
429    pub fn is_empty(&self) -> bool {
430        self.len() == 0
431    }
432
433    /// The item name the ACL names, when it was understood.
434    pub fn item_name(&self) -> Option<&str> {
435        match self {
436            Self::Parsed(blob) => blob.item_name(),
437            Self::Raw(_) => None,
438        }
439    }
440
441    /// Applications the ACL restricts decryption to. Empty means either "any
442    /// application" or an ACL this build did not parse.
443    pub fn trusted_paths(&self) -> Vec<&str> {
444        match self {
445            Self::Parsed(blob) => blob.trusted_paths(),
446            Self::Raw(_) => Vec::new(),
447        }
448    }
449}
450
451impl KeyBlob {
452    pub fn parse(data: &[u8]) -> Result<Self> {
453        if data.len() < KEY_BLOB_LEN {
454            return Err(Error::format("key blob is shorter than its fixed header"));
455        }
456        let magic = be32(data, 0);
457        if magic != BLOB_MAGIC {
458            return Err(Error::format(format!(
459                "key blob magic is 0x{magic:08x}, expected 0x{BLOB_MAGIC:08x}"
460            )));
461        }
462
463        let start_crypto_blob = be32(data, 8);
464        let total_length = be32(data, 12);
465        if (start_crypto_blob as usize) < KEY_BLOB_LEN
466            || total_length < start_crypto_blob
467            || total_length as usize > data.len()
468        {
469            return Err(Error::format("key blob offsets are inconsistent"));
470        }
471
472        Ok(Self {
473            version: be32(data, 4),
474            start_crypto_blob,
475            total_length,
476            iv: data[16..24].try_into().expect("8 bytes"),
477            header: KeyHeader::parse(&data[24..100])?,
478            wrapped: WrappedKeyFields::parse(&data[100..116])?,
479            blob_signature: data[116..136].try_into().expect("20 bytes"),
480            public_acl: PublicAcl::parse(&data[KEY_BLOB_LEN..start_crypto_blob as usize]),
481            crypto_blob: data[start_crypto_blob as usize..total_length as usize].to_vec(),
482        })
483    }
484
485    pub fn to_bytes(&self) -> Vec<u8> {
486        let acl = self.public_acl.to_bytes();
487        let start_crypto_blob = (KEY_BLOB_LEN + acl.len()) as u32;
488        let total_length = start_crypto_blob + self.crypto_blob.len() as u32;
489
490        let mut out = Vec::with_capacity(total_length as usize);
491        out.extend_from_slice(&BLOB_MAGIC.to_be_bytes());
492        out.extend_from_slice(&self.version.to_be_bytes());
493        out.extend_from_slice(&start_crypto_blob.to_be_bytes());
494        out.extend_from_slice(&total_length.to_be_bytes());
495        out.extend_from_slice(&self.iv);
496        self.header.write(&mut out);
497        self.wrapped.write(&mut out);
498        out.extend_from_slice(&self.blob_signature);
499        out.extend_from_slice(&acl);
500        out.extend_from_slice(&self.crypto_blob);
501        out
502    }
503
504    /// Recover the item key this blob wraps.
505    pub fn unwrap_key(&self, encryption_key: &[u8]) -> Result<SecretBytes> {
506        unwrap_key(encryption_key, &self.iv, &self.crypto_blob)
507    }
508
509    /// Offset of `blobSignature` within the blob.
510    const SIGNATURE_OFFSET: usize = 116;
511
512    fn signed_chunks(bytes: &[u8]) -> [&[u8]; 2] {
513        [&bytes[..Self::SIGNATURE_OFFSET], &bytes[KEY_BLOB_LEN..]]
514    }
515
516    /// Recompute `blob_signature`, the same way securityd does for a key blob.
517    /// A key blob carries the database's version, so the algorithm follows it.
518    pub fn sign(&mut self, signing_key: &[u8]) {
519        self.blob_signature = [0u8; 20];
520        let bytes = self.to_bytes();
521        self.blob_signature = sign_blob(self.version, signing_key, &Self::signed_chunks(&bytes));
522    }
523
524    pub fn verify(&self, signing_key: &[u8]) -> bool {
525        let bytes = self.to_bytes();
526        sign_blob(self.version, signing_key, &Self::signed_chunks(&bytes)) == self.blob_signature
527    }
528}
529
530/// Apple's custom key wrapping (`CSSM_KEYBLOB_WRAPPED_FORMAT_APPLE_CUSTOM`).
531///
532/// The wrapped form is
533///
534/// ```text
535/// outer      = 3DES-CBC(db key, MAGIC_CMS_IV, reverse(iv || inner))
536/// inner      = 3DES-CBC(db key, iv, descriptive_data_length || key)
537/// ```
538///
539/// The IV travels inside the wrapped blob as well as in the key blob's header,
540/// and the whole buffer is byte-reversed between the two passes. The descriptive
541/// data is the key's private ACL, which is empty for a keychain item and for an
542/// imported identity, so its length is zero.
543///
544/// The same scheme wraps both a 24-byte item key and a private key's PKCS#8
545/// `PrivateKeyInfo`; only the payload length differs. Use [`unwrap_blob`] and
546/// [`wrap_blob`] when the length is not 24.
547///
548/// Verified both ways against keychains written by macOS: every item key in them
549/// unwraps, and re-wrapping the same key reproduces their byte layout.
550pub fn unwrap_key(encryption_key: &[u8], iv: &[u8], wrapped: &[u8]) -> Result<SecretBytes> {
551    let material = unwrap_blob(encryption_key, iv, wrapped)?;
552    if material.as_slice().len() != KEY_LEN {
553        return Err(Error::Crypto("unwrapped key is not 24 bytes"));
554    }
555    Ok(material)
556}
557
558/// Unwrap key material of any length: a 24-byte item key, or a private key's
559/// PKCS#8 `PrivateKeyInfo`, which is what a private-key record holds.
560pub fn unwrap_blob(encryption_key: &[u8], iv: &[u8], wrapped: &[u8]) -> Result<SecretBytes> {
561    let mut outer = decrypt(encryption_key, &MAGIC_CMS_IV, wrapped)?;
562    if outer.len() < BLOCK_SIZE + 2 * BLOCK_SIZE {
563        return Err(Error::Crypto("wrapped key is too short"));
564    }
565    outer.reverse();
566
567    let (embedded_iv, inner_ciphertext) = outer.split_at(BLOCK_SIZE);
568    // The header IV is what securityd uses; the embedded copy should agree.
569    let iv = if embedded_iv == iv { embedded_iv } else { iv };
570
571    let inner = decrypt(encryption_key, iv, inner_ciphertext)?;
572    if inner.len() < 4 {
573        return Err(Error::Crypto(
574            "unwrapped key has no descriptive-data length",
575        ));
576    }
577    let descriptive_len = u32::from_be_bytes([inner[0], inner[1], inner[2], inner[3]]) as usize;
578    let key_at = 4 + descriptive_len;
579    if inner.len() <= key_at {
580        return Err(Error::Crypto("unwrapped key is too short"));
581    }
582    // Everything after the descriptive data is the key: 24 bytes for an item
583    // key, a PKCS#8 PrivateKeyInfo for an identity's private key.
584    Ok(SecretBytes::new(&inner[key_at..]))
585}
586
587/// Apple's custom key wrapping, forwards. Inverse of [`unwrap_key`].
588pub fn wrap_key(encryption_key: &[u8], iv: &[u8], key: &[u8]) -> Result<Vec<u8>> {
589    if key.len() != KEY_LEN {
590        return Err(Error::Crypto("item key must be 24 bytes"));
591    }
592    wrap_blob(encryption_key, iv, key)
593}
594
595/// Wrap key material of any length. Inverse of [`unwrap_blob`].
596pub fn wrap_blob(encryption_key: &[u8], iv: &[u8], key: &[u8]) -> Result<Vec<u8>> {
597    if iv.len() != BLOCK_SIZE {
598        return Err(Error::Crypto("3DES IV must be 8 bytes"));
599    }
600
601    // No descriptive data: a keychain item's or identity's key has no private
602    // ACL. macOS writes a zero length here too.
603    let mut inner = Vec::with_capacity(4 + key.len());
604    inner.extend_from_slice(&0u32.to_be_bytes());
605    inner.extend_from_slice(key);
606
607    let mut buffer = iv.to_vec();
608    buffer.extend_from_slice(&encrypt(encryption_key, iv, &inner)?);
609    buffer.reverse();
610    encrypt(encryption_key, &MAGIC_CMS_IV, &buffer)
611}
612
613/// An item's encrypted secret, as stored in the record's key data.
614#[derive(Debug, Clone)]
615pub struct Ssgp {
616    /// `ssgp` plus a 16-byte label: together, the key record's `Label`.
617    pub label: [u8; 20],
618    pub iv: [u8; BLOCK_SIZE],
619    pub ciphertext: Vec<u8>,
620}
621
622/// Length of the fixed part of an SSGP blob.
623pub const SSGP_HEADER_LEN: usize = 28;
624
625impl Ssgp {
626    pub fn parse(data: &[u8]) -> Result<Self> {
627        if data.len() <= SSGP_HEADER_LEN {
628            return Err(Error::format("item has no secure-storage payload"));
629        }
630        if &data[..4] != SSGP_MAGIC {
631            return Err(Error::format("item payload is not a secure-storage group"));
632        }
633        Ok(Self {
634            label: data[..20].try_into().expect("20 bytes"),
635            iv: data[20..28].try_into().expect("8 bytes"),
636            ciphertext: data[SSGP_HEADER_LEN..].to_vec(),
637        })
638    }
639
640    pub fn to_bytes(&self) -> Vec<u8> {
641        let mut out = Vec::with_capacity(SSGP_HEADER_LEN + self.ciphertext.len());
642        out.extend_from_slice(&self.label);
643        out.extend_from_slice(&self.iv);
644        out.extend_from_slice(&self.ciphertext);
645        out
646    }
647
648    /// Build a payload for `secret` under `item_key`.
649    pub fn seal(
650        label: [u8; 20],
651        iv: [u8; BLOCK_SIZE],
652        item_key: &[u8],
653        secret: &[u8],
654    ) -> Result<Self> {
655        Ok(Self {
656            label,
657            iv,
658            ciphertext: encrypt(item_key, &iv, secret)?,
659        })
660    }
661
662    pub fn open(&self, item_key: &[u8]) -> Result<SecretBytes> {
663        Ok(SecretBytes::new(decrypt(
664            item_key,
665            &self.iv,
666            &self.ciphertext,
667        )?))
668    }
669
670    /// The label as a hex string, for messages.
671    pub fn label_hex(&self) -> String {
672        hex::encode(&self.label[4..])
673    }
674}
675
676fn be32(data: &[u8], at: usize) -> u32 {
677    u32::from_be_bytes([data[at], data[at + 1], data[at + 2], data[at + 3]])
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683
684    /// RFC 6070 test vector 3, which anchors the PBKDF2-HMAC-SHA1 primitive to
685    /// a published value independent of this code.
686    #[test]
687    fn pbkdf2_matches_rfc_6070() {
688        let mut out = [0u8; 20];
689        pbkdf2::pbkdf2::<Hmac<Sha1>>(b"password", b"salt", 4096, &mut out).unwrap();
690        assert_eq!(hex::encode(out), "4b007901b765489abead49d926f721d065a429c1");
691    }
692
693    /// And this pins the parameters the keychain format uses: 1000 iterations,
694    /// 24 bytes of output. Cross-checked against Python's `hashlib.pbkdf2_hmac`.
695    #[test]
696    fn master_key_uses_the_formats_parameters() {
697        let key = master_key(b"password", b"salt");
698        assert_eq!(key.as_slice().len(), KEY_LEN);
699        assert_eq!(
700            hex::encode(key.as_slice()),
701            "6e88be8bad7eae9d9e10aa061224034fed48d03fcbad968b"
702        );
703    }
704
705    #[test]
706    fn encrypt_then_decrypt_round_trips_with_cssm_padding() {
707        let key = [0x11u8; KEY_LEN];
708        let iv = [0x22u8; BLOCK_SIZE];
709        for plaintext in [
710            b"".to_vec(),
711            b"a".to_vec(),
712            b"12345678".to_vec(),  // exactly one block
713            b"123456789".to_vec(), // spills into a second
714            vec![0xffu8; 64],
715        ] {
716            let sealed = encrypt(&key, &iv, &plaintext).unwrap();
717            assert_eq!(sealed.len() % BLOCK_SIZE, 0);
718            assert!(sealed.len() > plaintext.len(), "padding is always added");
719            assert_eq!(decrypt(&key, &iv, &sealed).unwrap(), plaintext);
720        }
721    }
722
723    #[test]
724    fn a_wrong_key_is_reported_as_a_wrong_password() {
725        let iv = [0x22u8; BLOCK_SIZE];
726        let sealed = encrypt(&[0x11u8; KEY_LEN], &iv, b"secret data here").unwrap();
727        // Padding almost never validates under the wrong key.
728        assert!(matches!(
729            decrypt(&[0x33u8; KEY_LEN], &iv, &sealed),
730            Err(Error::WrongPassword) | Err(Error::Crypto(_))
731        ));
732    }
733
734    #[test]
735    fn decrypt_rejects_malformed_inputs() {
736        let key = [0x11u8; KEY_LEN];
737        let iv = [0x22u8; BLOCK_SIZE];
738        assert!(decrypt(&key, &iv, b"").is_err());
739        assert!(
740            decrypt(&key, &iv, b"1234567").is_err(),
741            "not a block multiple"
742        );
743        assert!(decrypt(&key[..8], &iv, b"12345678").is_err(), "short key");
744        assert!(decrypt(&key, &iv[..4], b"12345678").is_err(), "short IV");
745    }
746
747    #[test]
748    fn key_wrapping_round_trips() {
749        let encryption_key = [0x5au8; KEY_LEN];
750        let iv = [0x77u8; BLOCK_SIZE];
751        let item_key = [0xa5u8; KEY_LEN];
752
753        let wrapped = wrap_key(&encryption_key, &iv, &item_key).unwrap();
754        let unwrapped = unwrap_key(&encryption_key, &iv, &wrapped).unwrap();
755        assert_eq!(unwrapped.as_slice(), item_key);
756    }
757
758    #[test]
759    fn wrapped_key_has_the_layout_macos_writes() {
760        let encryption_key = [0x5au8; KEY_LEN];
761        let iv = [0x77u8; BLOCK_SIZE];
762        let wrapped = wrap_key(&encryption_key, &iv, &[0xa5u8; KEY_LEN]).unwrap();
763
764        // 8-byte IV plus a 32-byte inner ciphertext, padded to 48 by the outer
765        // pass. macOS writes exactly this length for an item key.
766        assert_eq!(wrapped.len(), 48);
767
768        let mut outer = decrypt(&encryption_key, &MAGIC_CMS_IV, &wrapped).unwrap();
769        assert_eq!(outer.len(), 40);
770        outer.reverse();
771        assert_eq!(
772            &outer[..BLOCK_SIZE],
773            &iv,
774            "the IV is carried inside the blob"
775        );
776
777        let inner = decrypt(&encryption_key, &iv, &outer[BLOCK_SIZE..]).unwrap();
778        assert_eq!(&inner[..4], &[0, 0, 0, 0], "descriptive data is empty");
779        assert_eq!(&inner[4..], &[0xa5u8; KEY_LEN]);
780    }
781
782    #[test]
783    fn wrapping_rejects_a_wrong_length_key() {
784        assert!(wrap_key(&[0u8; KEY_LEN], &[0u8; BLOCK_SIZE], &[0u8; 16]).is_err());
785    }
786
787    #[test]
788    fn db_blob_round_trips_through_bytes() {
789        let mut blob = DbBlob {
790            version: BLOB_VERSION,
791            start_crypto_blob: 0,
792            total_length: 0,
793            random_signature: [7u8; 16],
794            sequence: 3,
795            idle_timeout: 300,
796            lock_on_sleep: true,
797            salt: [9u8; SALT_LEN],
798            iv: [1u8; BLOCK_SIZE],
799            blob_signature: [4u8; 20],
800            public_acl: vec![0xaa; 28],
801            crypto_blob: Vec::new(),
802        };
803        let keys = DbKeys {
804            encryption_key: SecretBytes::new(vec![0x13; KEY_LEN]),
805            signing_key: SecretBytes::new(vec![0x14; 20]),
806            private_acl: Vec::new(),
807        };
808        blob.seal(b"open sesame", &keys).unwrap();
809
810        let bytes = blob.to_bytes();
811        assert_eq!(bytes.len(), blob.total_length as usize);
812        let parsed = DbBlob::parse(&bytes).unwrap();
813        assert_eq!(parsed.start_crypto_blob as usize, DB_BLOB_LEN + 28);
814        assert_eq!(parsed.public_acl, blob.public_acl);
815        assert_eq!(parsed.salt, blob.salt);
816        assert!(parsed.lock_on_sleep);
817        assert_eq!(parsed.idle_timeout, 300);
818
819        let opened = parsed.unlock(b"open sesame").unwrap();
820        assert_eq!(
821            opened.encryption_key.as_slice(),
822            keys.encryption_key.as_slice()
823        );
824        assert_eq!(opened.signing_key.as_slice(), keys.signing_key.as_slice());
825
826        assert!(matches!(parsed.unlock(b"wrong"), Err(Error::WrongPassword)));
827    }
828
829    /// A `DbBlob` that `security` wrote into `~/Library/Keychains`, which is why
830    /// it is at the partition version. Its signature verifies only with real
831    /// HMAC-SHA1; signing it the legacy way is what `securityd` would reject.
832    #[test]
833    fn a_partition_version_blob_is_signed_with_real_hmac() {
834        let bytes = hex::decode(concat!(
835            "fade07110000020000000078000000a81c5679b6a9edaa0a4753ce619fdd64b9",
836            "000000000000012c01000000ec9c4b45174e6421a2723869afb9eecc31155aa0",
837            "623dcb2aa7157013ffda409579631311d74c1b6ef9a5d1f5a79963e700000000",
838            "00000001000000010000000000000001000000000100000040692291de02d260",
839            "c740c2ba7fc7d6802b6a6b073de5ceb83da1d10afcecbd18315f365d7853670e",
840            "ab5456adbb219664",
841        ))
842        .unwrap();
843
844        let blob = DbBlob::parse(&bytes).unwrap();
845        assert_eq!(blob.version, BLOB_VERSION_PARTITION);
846
847        let keys = blob.unlock(b"probepw").expect("unlock");
848        assert!(
849            blob.verify(keys.signing_key.as_slice()),
850            "partition blob signature"
851        );
852
853        // The legacy algorithm gives a different answer, so the version really is
854        // what selects it.
855        let chunks = [
856            &bytes[..72],
857            &bytes[DB_BLOB_LEN..blob.total_length as usize],
858        ];
859        assert_ne!(
860            legacy_hmac_sha1(keys.signing_key.as_slice(), &chunks),
861            blob.blob_signature
862        );
863        assert_eq!(
864            sign_blob(BLOB_VERSION_PARTITION, keys.signing_key.as_slice(), &chunks),
865            blob.blob_signature
866        );
867    }
868
869    #[test]
870    fn the_signature_algorithm_follows_the_blob_version() {
871        let key = [0x11u8; 20];
872        let chunks: [&[u8]; 2] = [b"first", b"second"];
873
874        // Only 0x100 gets the legacy variant.
875        assert_eq!(
876            sign_blob(BLOB_VERSION_MACOS_10_0, &key, &chunks),
877            legacy_hmac_sha1(&key, &chunks)
878        );
879        for version in [BLOB_VERSION_MACOS_10_1, BLOB_VERSION_PARTITION] {
880            assert_ne!(
881                sign_blob(version, &key, &chunks),
882                legacy_hmac_sha1(&key, &chunks)
883            );
884        }
885        // And the non-legacy path is plain HMAC over the concatenation.
886        let mut expected = <Hmac<Sha1> as hmac::Mac>::new_from_slice(&key).unwrap();
887        hmac::Mac::update(&mut expected, b"firstsecond");
888        assert_eq!(
889            sign_blob(BLOB_VERSION_PARTITION, &key, &chunks).to_vec(),
890            hmac::Mac::finalize(expected).into_bytes().to_vec()
891        );
892    }
893
894    #[test]
895    fn db_blob_rejects_bad_magic_and_offsets() {
896        let mut bytes = vec![0u8; DB_BLOB_LEN];
897        assert!(DbBlob::parse(&bytes).is_err(), "zero magic");
898
899        bytes[..4].copy_from_slice(&BLOB_MAGIC.to_be_bytes());
900        // start_crypto_blob inside the fixed header is nonsense.
901        bytes[8..12].copy_from_slice(&4u32.to_be_bytes());
902        assert!(DbBlob::parse(&bytes).is_err());
903
904        assert!(DbBlob::parse(&bytes[..10]).is_err(), "truncated");
905    }
906
907    #[test]
908    fn ssgp_round_trips_and_carries_its_label() {
909        let mut label = [0u8; 20];
910        label[..4].copy_from_slice(SSGP_MAGIC);
911        label[4..].copy_from_slice(&[0xab; 16]);
912        let item_key = [0x3cu8; KEY_LEN];
913
914        let ssgp = Ssgp::seal(label, [0x0fu8; BLOCK_SIZE], &item_key, b"hunter2").unwrap();
915        let bytes = ssgp.to_bytes();
916        assert_eq!(bytes.len(), SSGP_HEADER_LEN + ssgp.ciphertext.len());
917
918        let parsed = Ssgp::parse(&bytes).unwrap();
919        assert_eq!(parsed.label, label);
920        assert_eq!(parsed.label_hex(), "ab".repeat(16));
921        assert_eq!(parsed.open(&item_key).unwrap().as_slice(), b"hunter2");
922    }
923
924    #[test]
925    fn ssgp_rejects_payloads_that_are_not_secure_storage() {
926        assert!(Ssgp::parse(b"short").is_err());
927        let mut data = vec![0u8; 40];
928        data[..4].copy_from_slice(b"nope");
929        assert!(Ssgp::parse(&data).is_err());
930    }
931}