Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
198bf39
Branches
Tags
main
apple-crates
fairplay
src
crypto.rs
Add FairPlay crate reimplementing fairplay-decrypt and fix APS
Dadoum
commited
198bf39
at 2026-10-25 21:12:14
crypto.rs
Blame
History
Raw
use aes::cipher::{BlockModeDecrypt, KeyIvInit, block_padding::NoPadding}; use md5::{Digest, Md5}; use zeroize::Zeroizing; use crate::{Error, SinfFields}; pub(crate) const WORK_MASK: [u8; 16] = [ 0xc7, 0x62, 0x61, 0x10, 0xce, 0xcd, 0x15, 0x96, 0x3d, 0x85, 0x0f, 0x66, 0x05, 0xf1, 0x1c, 0x4b, ]; const ROUND_WORDS: [u32; 4] = [ 0x6ee6b74f ^ 0x06b2cb61, 0x78b72abf ^ 0x06b2cb61, 0xd227c8ea ^ 0x06b2cb61, 0x0c0081bc ^ 0x06b2cb61, ]; const FINAL_WORDS: [u32; 4] = [0x5b7198c7, 0xeb5de22c, 0x0f8aa8fa, 0xf6e1bb3f]; /// The caller guarantees complete AES blocks; no padding is added or removed. pub(crate) fn decrypt_cbc(key: &[u8; 16], iv: &[u8; 16], data: &mut [u8]) { cbc::Decryptor::<aes::Aes128>::new(key.into(), iv.into()) .decrypt_padded::<NoPadding>(data) .expect("validated whole AES blocks"); } pub(crate) fn derive_work_key( credential: &[u8; 16], fields: SinfFields<'_>, ) -> Result<[u8; 16], Error> { let private = match fields.private.len() { 448 => fields.private, 440 if fields.private[432..] == [0; 8] => &fields.private[..432], 440 => return Err(Error::InvalidSinf("private record trailer is not zero")), _ => { return Err(Error::InvalidSinf( "private record must be 440 or 448 bytes", )); } }; let iviv: &[u8; 16] = fields .iviv .try_into() .map_err(|_| Error::InvalidSinf("iviv must contain 16 bytes"))?; let name_end = fields .name .iter() .position(|byte| *byte == 0) .unwrap_or(fields.name.len()); let mut hash = Md5::new(); hash.update(&fields.name[..name_end]); hash.update(iviv); let iv: [u8; 16] = hash.finalize().into(); let mut key = Zeroizing::new(*credential); for byte in key.iter_mut() { *byte ^= b'0'; } let mut clear = Zeroizing::new(private.to_vec()); decrypt_cbc(&key, &iv, &mut clear); if &clear[..4] != b"itun" || &clear[20..24] != b"key " || &clear[44..48] != b"iviv" { return Err(Error::InvalidCredential); } Ok(std::array::from_fn(|index| { clear[24 + index] ^ WORK_MASK[index] })) } fn square_words(sums: [u32; 4]) -> [u32; 4] { // Each sum has at most 17 bits. Multiplication wraps, but its carry // correction is XORed back in; omitting it changes the recovered network. std::array::from_fn(|i| sums[i].wrapping_mul(sums[i]) ^ (sums[i] >> 16) ^ ROUND_WORDS[i]) } /// Four corrected-square rounds, folded 64-bit squares, then feedforward. /// Byte order is big-endian halfwords; packed round words are low half first. pub(crate) fn finish(input: &[u8; 16]) -> [u8; 16] { let initial: [u32; 8] = std::array::from_fn(|i| u32::from(u16::from_be_bytes([input[2 * i], input[2 * i + 1]]))); let [a, b, c, d] = square_words([ initial[0] + initial[1], initial[2] + initial[3], initial[4] + initial[5], initial[6] + initial[7], ]); let [a, b, c, d] = square_words([ (b & 65535) + (a >> 16), (a & 65535) + (b >> 16), (c & 65535) + (d >> 16), (d & 65535) + (c >> 16), ]); let [a, b, c, d] = square_words([ (b & 65535) + (a & 65535), (c & 65535) + (a >> 16), (d & 65535) + (b >> 16), (d >> 16) + (c >> 16), ]); let [a, b, c, d] = square_words([ (a >> 16) + (b & 65535), (c & 65535) + (a & 65535), (d >> 16) + (b >> 16), (d & 65535) + (c >> 16), ]); // Wrap before squaring; retain all 64 bits of each final square. let sums = [ a.wrapping_add(b), ((a & 65535) + (c & 65535)).wrapping_add(((b & 65535) + (d & 65535)) << 16), ((a & 65535) + (c >> 16)).wrapping_add(((b >> 16) + (d >> 16)) << 16), c.wrapping_add(d), ]; let mut output = [0; 16]; for (i, sum) in sums.into_iter().enumerate() { let square = u64::from(sum) * u64::from(sum); let folded = square as u32 ^ (square >> 32) as u32 ^ FINAL_WORDS[i]; let low = (initial[2 * i] as u16 ^ folded as u16).to_be_bytes(); let high = (initial[2 * i + 1] as u16 ^ (folded >> 16) as u16).to_be_bytes(); output[4 * i..4 * i + 2].copy_from_slice(&low); output[4 * i + 2..4 * i + 4].copy_from_slice(&high); } output }