From 07082625627821b4f9cf72234ebc7de1754adf13 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:08:58 +0000 Subject: [PATCH] Add decrypt module, merge to one drive.read(), Disc::decrypt_keys() - New decrypt.rs: DecryptKeys enum (AACS/CSS/None) + decrypt_sectors() - Single drive.read() replaces read_disc/read_content (same SCSI READ(10)) - ContentReader and DiscStream use decrypt_sectors() (no duplicated crypto) - Disc::decrypt_keys() exposes resolved keys for disc-to-ISO --- src/decrypt.rs | 64 +++++++++++++++++++++++++++++++++ src/disc/mod.rs | 94 ++++++++++++++++++++++-------------------------- src/drive/mod.rs | 25 ++----------- src/lib.rs | 2 ++ src/mux/disc.rs | 51 +++++--------------------- 5 files changed, 120 insertions(+), 116 deletions(-) create mode 100644 src/decrypt.rs diff --git a/src/decrypt.rs b/src/decrypt.rs new file mode 100644 index 0000000..da7c4e0 --- /dev/null +++ b/src/decrypt.rs @@ -0,0 +1,64 @@ +//! Decrypt-on-read layer. +//! +//! Decrypts sectors in-place using resolved keys from disc scanning. +//! Handles AACS 1.0, AACS 2.0, and CSS transparently. +//! The caller never sees encrypted data unless explicitly bypassed. + +use crate::aacs; +use crate::css; + +/// Resolved decryption state from disc scanning. +/// Passed to `decrypt_sectors()` — the caller doesn't need to know +/// which encryption scheme is in use. +pub enum DecryptKeys { + /// No encryption on this disc. + None, + /// AACS (Blu-ray / UHD). Unit keys + optional read data key. + Aacs { + unit_keys: Vec<(u32, [u8; 16])>, + read_data_key: Option<[u8; 16]>, + }, + /// CSS (DVD). Title key for sector descrambling. + Css { + title_key: [u8; 5], + }, +} + +impl DecryptKeys { + /// True if there are keys to decrypt with. + pub fn is_encrypted(&self) -> bool { + !matches!(self, DecryptKeys::None) + } +} + +/// Decrypt a buffer of sectors in-place. +/// +/// For AACS: processes in 6144-byte aligned units (3 sectors). +/// For CSS: processes per 2048-byte sector. +/// For None: no-op. +/// +/// `unit_key_idx` selects which AACS unit key to use (0 for most discs). +pub fn decrypt_sectors(buf: &mut [u8], keys: &DecryptKeys, unit_key_idx: usize) { + match keys { + DecryptKeys::None => {} + DecryptKeys::Aacs { unit_keys, read_data_key } => { + let uk = unit_keys + .get(unit_key_idx) + .map(|(_, k)| *k) + .unwrap_or([0u8; 16]); + let rdk = read_data_key.as_ref(); + let unit_len = aacs::ALIGNED_UNIT_LEN; + + for chunk in buf.chunks_mut(unit_len) { + if chunk.len() == unit_len && aacs::is_unit_encrypted(chunk) { + aacs::decrypt_unit_full(chunk, &uk, rdk); + } + } + } + DecryptKeys::Css { title_key } => { + for chunk in buf.chunks_mut(2048) { + css::lfsr::descramble_sector(title_key, chunk); + } + } + } +} diff --git a/src/disc/mod.rs b/src/disc/mod.rs index ba58b91..459f794 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -685,8 +685,7 @@ impl Disc { /// - At minimum batch + still failing: retries once, then skips + zero-fills pub struct ContentReader<'a> { session: &'a mut Drive, - aacs: Option<&'a AacsState>, - css: Option<&'a crate::css::CssState>, + decrypt_keys: crate::decrypt::DecryptKeys, extents: Vec, current_extent: usize, current_offset: u32, @@ -709,6 +708,23 @@ pub struct ContentReader<'a> { } impl Disc { + /// Get the resolved decryption keys for this disc. + /// Used by disc-to-ISO and other full-disc operations. + pub fn decrypt_keys(&self) -> crate::decrypt::DecryptKeys { + if let Some(ref aacs) = self.aacs { + crate::decrypt::DecryptKeys::Aacs { + unit_keys: aacs.unit_keys.clone(), + read_data_key: aacs.read_data_key, + } + } else if let Some(ref css) = self.css { + crate::decrypt::DecryptKeys::Css { + title_key: css.title_key, + } + } else { + crate::decrypt::DecryptKeys::None + } + } + /// Open a title for reading. Decryption is automatic -- if the disc /// is encrypted and keys were found during scan(), content is decrypted /// on the fly. Unencrypted discs pass through unchanged. @@ -730,10 +746,22 @@ impl Disc { // Detect kernel max transfer size for this device let max_batch = detect_max_batch_sectors(session.device_path()); + let decrypt_keys = if let Some(ref aacs) = self.aacs { + crate::decrypt::DecryptKeys::Aacs { + unit_keys: aacs.unit_keys.clone(), + read_data_key: aacs.read_data_key, + } + } else if let Some(ref css) = self.css { + crate::decrypt::DecryptKeys::Css { + title_key: css.title_key, + } + } else { + crate::decrypt::DecryptKeys::None + }; + Ok(ContentReader { session, - aacs: self.aacs.as_ref(), - css: self.css.as_ref(), + decrypt_keys, extents: title.extents.clone(), current_extent: 0, current_offset: 0, @@ -838,63 +866,25 @@ impl<'a> ContentReader<'a> { // Decrypt all units in the buffer in-place let unit_len = crate::aacs::ALIGNED_UNIT_LEN; - if let Some(aacs) = &self.aacs { - // AACS unit decryption (BD/UHD) - let uk = aacs - .unit_keys - .get(self.unit_key_idx) - .map(|(_, k)| *k) - .ok_or(Error::AacsDataKey)?; - let rdk = aacs.read_data_key.as_ref(); - - for i in 0..self.buf_len { - let start = i * unit_len; - let end = start + unit_len; - let unit = &mut self.read_buf[start..end]; - if crate::aacs::is_unit_encrypted(unit) { - crate::aacs::decrypt_unit_full(unit, &uk, rdk); - } - } - - let total_bytes = self.buf_len * unit_len; - self.buf_pos = self.buf_len; - Ok(Some(&self.read_buf[..total_bytes])) - } else if let Some(css) = &self.css { - // CSS per-sector descrambling (DVD) - let total_bytes = self.buf_len * unit_len; - for chunk in self.read_buf[..total_bytes].chunks_mut(2048) { - crate::css::lfsr::descramble_sector(&css.title_key, chunk); - } - - self.buf_pos = self.buf_len; - Ok(Some(&self.read_buf[..total_bytes])) - } else { - // No encryption - let total_bytes = self.buf_len * unit_len; - self.buf_pos = self.buf_len; - Ok(Some(&self.read_buf[..total_bytes])) - } + let total_bytes = self.buf_len * unit_len; + crate::decrypt::decrypt_sectors( + &mut self.read_buf[..total_bytes], + &self.decrypt_keys, + self.unit_key_idx, + ); + self.buf_pos = self.buf_len; + Ok(Some(&self.read_buf[..total_bytes])) } /// Decrypt a single aligned unit in-place if needed. fn decrypt_unit(&self, unit: &mut [u8]) { - if let Some(aacs) = &self.aacs { - if crate::aacs::is_unit_encrypted(unit) { - let uk = aacs - .unit_keys - .get(self.unit_key_idx) - .map(|(_, k)| *k) - .unwrap_or([0u8; 16]); - - crate::aacs::decrypt_unit_full(unit, &uk, aacs.read_data_key.as_ref()); - } - } + crate::decrypt::decrypt_sectors(unit, &self.decrypt_keys, self.unit_key_idx); } /// Read sectors via standard READ(10) 0x00. /// calibration primers. Standard reads are faster on most drives. fn read_sectors(&mut self, lba: u32, count: u16) -> Result<()> { - self.session.read_content(lba, count, &mut self.read_buf)?; + self.session.read(lba, count, &mut self.read_buf)?; Ok(()) } diff --git a/src/drive/mod.rs b/src/drive/mod.rs index 2970855..9889fb5 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -317,27 +317,8 @@ impl Drive { } } - pub fn read_disc(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { - let cdb = [ - crate::scsi::SCSI_READ_10, - 0x00, - (lba >> 24) as u8, - (lba >> 16) as u8, - (lba >> 8) as u8, - lba as u8, - 0x00, - (count >> 8) as u8, - count as u8, - 0x00, - ]; - let result = - self.scsi - .as_mut() - .execute(&cdb, crate::scsi::DataDirection::FromDevice, buf, 5_000)?; - Ok(result.bytes_transferred) - } - - pub fn read_content(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { + /// Read sectors from the disc. Raw SCSI READ(10). + pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { let cdb = [ crate::scsi::SCSI_READ_10, 0x00, @@ -424,7 +405,7 @@ impl Drive { impl SectorReader for Drive { fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { - self.read_disc(lba, count, buf) + self.read(lba, count, buf) } } diff --git a/src/lib.rs b/src/lib.rs index 6258dc0..be689d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,6 +68,7 @@ pub mod aacs; pub(crate) mod clpi; pub mod css; +pub mod decrypt; pub mod disc; pub mod drive; pub mod error; @@ -107,6 +108,7 @@ pub use mux::NetworkStream; pub use mux::NullStream; pub use mux::StdioStream; pub use mux::{open_input, open_output, parse_url, InputOptions, StreamUrl}; +pub use decrypt::{DecryptKeys, decrypt_sectors}; pub use scsi::ScsiTransport; pub use sector::SectorReader; pub use speed::DriveSpeed; diff --git a/src/mux/disc.rs b/src/mux/disc.rs index 7c69940..3eb7c2d 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -17,13 +17,6 @@ use crate::error::Error; use crate::speed::DriveSpeed; use std::io::{self, Read, Write}; -/// AACS decryption parameters needed at read time. -/// Extracted from `AacsState` so we don't need `Clone` on the full struct. -struct AacsDecrypt { - unit_keys: Vec<(u32, [u8; 16])>, - read_data_key: Option<[u8; 16]>, -} - /// Options for opening a disc stream. #[derive(Default)] pub struct DiscOptions { @@ -54,8 +47,7 @@ pub struct DiscStream { current_offset: u32, #[allow(dead_code)] content_format: ContentFormat, - aacs: Option, - css: Option, + decrypt_keys: crate::decrypt::DecryptKeys, unit_key_idx: usize, read_buf: Vec, /// Current batch size in sectors (adapts on errors) @@ -99,11 +91,7 @@ impl DiscStream { let disc_title = disc.titles[title_index].clone(); let extents = disc_title.extents.clone(); let content_format = disc_title.content_format; - let aacs = disc.aacs.as_ref().map(|a| AacsDecrypt { - unit_keys: a.unit_keys.clone(), - read_data_key: a.read_data_key, - }); - let css = disc.css.clone(); + let decrypt_keys = disc.decrypt_keys(); let max_batch = detect_max_batch_sectors(session.device_path()); @@ -118,8 +106,7 @@ impl DiscStream { current_extent: 0, current_offset: 0, content_format, - aacs, - css, + decrypt_keys, unit_key_idx: 0, read_buf: Vec::with_capacity(max_batch as usize * 2048), batch_sectors: max_batch, @@ -137,7 +124,7 @@ impl DiscStream { /// Read sectors from the drive into `self.read_buf`. fn read_sectors(&mut self, lba: u32, count: u16) -> Result<(), Error> { - self.session.read_content(lba, count, &mut self.read_buf)?; + self.session.read(lba, count, &mut self.read_buf)?; Ok(()) } @@ -248,32 +235,12 @@ impl DiscStream { /// Decrypt the contents of `self.read_buf` in-place and copy the /// decrypted data into `self.batch_buf`. fn decrypt_and_buffer(&mut self) { - let unit_len = crate::aacs::ALIGNED_UNIT_LEN; let total_bytes = self.read_buf.len(); - - if let Some(ref aacs) = self.aacs { - let uk = aacs - .unit_keys - .get(self.unit_key_idx) - .map(|(_, k)| *k) - .unwrap_or([0u8; 16]); - let rdk = aacs.read_data_key.as_ref(); - - let num_units = total_bytes / unit_len; - for i in 0..num_units { - let start = i * unit_len; - let end = start + unit_len; - let unit = &mut self.read_buf[start..end]; - if crate::aacs::is_unit_encrypted(unit) { - crate::aacs::decrypt_unit_full(unit, &uk, rdk); - } - } - } else if let Some(ref css) = self.css { - for chunk in self.read_buf[..total_bytes].chunks_mut(2048) { - crate::css::lfsr::descramble_sector(&css.title_key, chunk); - } - } - // No encryption: read_buf is already plaintext + crate::decrypt::decrypt_sectors( + &mut self.read_buf[..total_bytes], + &self.decrypt_keys, + self.unit_key_idx, + ); // Swap buffers instead of copying — the old batch_buf becomes // read_buf and will be overwritten on the next read.