Git · dadoum.zone
Repositories
Help
Report an Issue
apple-crates
Code
Commits
Branches
Tags
Search
Tree:
198bf39
Branches
Tags
main
apple-crates
fairplay
src
lib.rs
Add FairPlay crate reimplementing fairplay-decrypt and fix APS
Dadoum
commited
198bf39
at 2026-10-25 21:12:14
lib.rs
Blame
History
Raw
//! FairPlay page-key derivation and in-place decryption of selected regions. //! //! The caller owns archive traversal, executable parsing, selecting the protected //! region and clearing its encryption flag after successful decryption. Page //! indices refer to the original complete executable, including any FAT prefix. //! See [`Decryptor`] and [`PageRecords`] for the integration boundary. #![doc = include_str!("../README.md")] mod crypto; mod metadata; use aes::Aes128; use aes::cipher::{BlockCipherEncrypt, KeyInit}; use zeroize::Zeroizing; pub use metadata::{PageRecord, PageRecords, SinfFields}; /// Number of bytes independently encrypted with each SUPF page record. pub const PAGE_SIZE: usize = 4096; /// Errors detected before a region is modified. #[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum Error { #[error("invalid SINF: {0}")] InvalidSinf(&'static str), #[error("credential does not decode this SINF private record")] InvalidCredential, #[error("invalid SUPF: {0}")] InvalidSupf(&'static str), #[error("SUPF does not contain CPU {cpu_type:#x}, subtype {cpu_subtype:#x}")] ArchitectureNotFound { cpu_type: u32, cpu_subtype: u32 }, #[error("encrypted region must contain a nonzero whole number of 4096-byte pages")] InvalidRegionLength, #[error("need {required} SUPF page records, found {available}")] InsufficientRecords { required: usize, available: usize }, #[error("absolute page indices exceed the 32-bit key derivation range")] PageIndexOverflow, } /// A cached AES key schedule shared by all pages using the same SINF. /// /// Construct from a saved 16-byte credential and SINF, or from an already /// recovered working key. No device calls, filesystem access or executable /// parsing are performed. This type intentionally does not expose keys in Debug. pub struct Decryptor { cipher: Aes128, } impl Decryptor { /// Decode the SINF private record and cache the page-derivation key schedule. pub fn from_sinf(credential: &[u8; 16], sinf: &[u8]) -> Result<Self, Error> { Self::from_sinf_fields(credential, SinfFields::parse(sinf)?) } /// Accept fields already extracted by the caller's SINF parser. pub fn from_sinf_fields(credential: &[u8; 16], fields: SinfFields<'_>) -> Result<Self, Error> { let work = Zeroizing::new(crypto::derive_work_key(credential, fields)?); Ok(Self::from_work_key(&work)) } /// Use the 16-byte working key returned by the recovered SINF algorithm. /// /// This is the masked working key, not the credential or a page AES key. pub fn from_work_key(work: &[u8; 16]) -> Self { let mut key = Zeroizing::new(*work); for (byte, mask) in key.iter_mut().zip(crypto::WORK_MASK) { *byte ^= mask; } Self { cipher: Aes128::new((&*key).into()), } } /// Derive a page AES key from its wrapped SUPF key and absolute page index. pub fn derive_page_key(&self, wrapped: &[u8; 16], absolute_page_index: u32) -> [u8; 16] { let offset = (u64::from(absolute_page_index) << 12).to_be_bytes(); // The recovered context masks cancel exactly: // 3e4a45b664eba141 ^ 2d43fbf6ed1b8833 ^ 1309be4089f02972 == 0. let mut blocks = [aes::cipher::Block::<Aes128>::default(); 3]; blocks[0][..8].copy_from_slice(&wrapped[8..]); blocks[0][8..].copy_from_slice(&offset); blocks[1].copy_from_slice(wrapped); blocks[2][..8].copy_from_slice(&offset); blocks[2][8..].copy_from_slice(&wrapped[..8]); self.cipher.encrypt_blocks(&mut blocks); let mut state = [0; 16]; for block in blocks { for (byte, input) in state.iter_mut().zip(block) { *byte ^= input; } state = crypto::finish(&state); } state } /// Decrypt one complete page in place; CBC restarts with the record's IV. /// /// `absolute_page_index` is `original_file_offset / PAGE_SIZE`, not the /// page's position within a sliced region or within a FAT architecture. pub fn decrypt_page_in_place( &self, page: &mut [u8; PAGE_SIZE], absolute_page_index: u32, record: PageRecord<'_>, ) { let key = Zeroizing::new(self.derive_page_key(record.wrapped_key(), absolute_page_index)); crypto::decrypt_cbc(&key, record.iv(), page); } /// Decrypt a caller-selected protected region without allocating a copy. /// /// `records` must start at the record for the first supplied page. For a /// subregion use [`PageRecords::slice`]. Extra records are allowed. Every /// size and index check completes before any byte is modified. Returns the /// number of decrypted pages; headers and encryption flags are caller-owned. pub fn decrypt_region_in_place( &self, region: &mut [u8], first_page_index: u32, records: PageRecords<'_>, ) -> Result<usize, Error> { if region.is_empty() || !region.len().is_multiple_of(PAGE_SIZE) { return Err(Error::InvalidRegionLength); } let count = region.len() / PAGE_SIZE; if records.len() < count { return Err(Error::InsufficientRecords { required: count, available: records.len(), }); } let last = u32::try_from(count - 1).map_err(|_| Error::PageIndexOverflow)?; first_page_index .checked_add(last) .ok_or(Error::PageIndexOverflow)?; for (index, page) in region.chunks_exact_mut(PAGE_SIZE).enumerate() { // Sizes and availability are validated above for the whole region. self.decrypt_page_in_place( page.try_into().expect("complete page"), first_page_index + index as u32, records.get(index).expect("validated record count"), ); } Ok(count) } } /// Existing marker retained for compatibility with the original crate stub. pub trait FairPlayProxy {}