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
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
-52
@@ -685,8 +685,7 @@ impl Disc {
|
|||||||
/// - At minimum batch + still failing: retries once, then skips + zero-fills
|
/// - At minimum batch + still failing: retries once, then skips + zero-fills
|
||||||
pub struct ContentReader<'a> {
|
pub struct ContentReader<'a> {
|
||||||
session: &'a mut Drive,
|
session: &'a mut Drive,
|
||||||
aacs: Option<&'a AacsState>,
|
decrypt_keys: crate::decrypt::DecryptKeys,
|
||||||
css: Option<&'a crate::css::CssState>,
|
|
||||||
extents: Vec<Extent>,
|
extents: Vec<Extent>,
|
||||||
current_extent: usize,
|
current_extent: usize,
|
||||||
current_offset: u32,
|
current_offset: u32,
|
||||||
@@ -709,6 +708,23 @@ pub struct ContentReader<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Disc {
|
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
|
/// Open a title for reading. Decryption is automatic -- if the disc
|
||||||
/// is encrypted and keys were found during scan(), content is decrypted
|
/// is encrypted and keys were found during scan(), content is decrypted
|
||||||
/// on the fly. Unencrypted discs pass through unchanged.
|
/// on the fly. Unencrypted discs pass through unchanged.
|
||||||
@@ -730,10 +746,22 @@ impl Disc {
|
|||||||
// Detect kernel max transfer size for this device
|
// Detect kernel max transfer size for this device
|
||||||
let max_batch = detect_max_batch_sectors(session.device_path());
|
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 {
|
Ok(ContentReader {
|
||||||
session,
|
session,
|
||||||
aacs: self.aacs.as_ref(),
|
decrypt_keys,
|
||||||
css: self.css.as_ref(),
|
|
||||||
extents: title.extents.clone(),
|
extents: title.extents.clone(),
|
||||||
current_extent: 0,
|
current_extent: 0,
|
||||||
current_offset: 0,
|
current_offset: 0,
|
||||||
@@ -838,63 +866,25 @@ impl<'a> ContentReader<'a> {
|
|||||||
|
|
||||||
// Decrypt all units in the buffer in-place
|
// Decrypt all units in the buffer in-place
|
||||||
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
||||||
if let Some(aacs) = &self.aacs {
|
let total_bytes = self.buf_len * unit_len;
|
||||||
// AACS unit decryption (BD/UHD)
|
crate::decrypt::decrypt_sectors(
|
||||||
let uk = aacs
|
&mut self.read_buf[..total_bytes],
|
||||||
.unit_keys
|
&self.decrypt_keys,
|
||||||
.get(self.unit_key_idx)
|
self.unit_key_idx,
|
||||||
.map(|(_, k)| *k)
|
);
|
||||||
.ok_or(Error::AacsDataKey)?;
|
self.buf_pos = self.buf_len;
|
||||||
let rdk = aacs.read_data_key.as_ref();
|
Ok(Some(&self.read_buf[..total_bytes]))
|
||||||
|
|
||||||
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]))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt a single aligned unit in-place if needed.
|
/// Decrypt a single aligned unit in-place if needed.
|
||||||
fn decrypt_unit(&self, unit: &mut [u8]) {
|
fn decrypt_unit(&self, unit: &mut [u8]) {
|
||||||
if let Some(aacs) = &self.aacs {
|
crate::decrypt::decrypt_sectors(unit, &self.decrypt_keys, self.unit_key_idx);
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read sectors via standard READ(10) 0x00.
|
/// Read sectors via standard READ(10) 0x00.
|
||||||
/// calibration primers. Standard reads are faster on most drives.
|
/// calibration primers. Standard reads are faster on most drives.
|
||||||
fn read_sectors(&mut self, lba: u32, count: u16) -> Result<()> {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-22
@@ -317,27 +317,8 @@ impl Drive {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_disc(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
/// Read sectors from the disc. Raw SCSI READ(10).
|
||||||
let cdb = [
|
pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||||
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<usize> {
|
|
||||||
let cdb = [
|
let cdb = [
|
||||||
crate::scsi::SCSI_READ_10,
|
crate::scsi::SCSI_READ_10,
|
||||||
0x00,
|
0x00,
|
||||||
@@ -424,7 +405,7 @@ impl Drive {
|
|||||||
|
|
||||||
impl SectorReader for Drive {
|
impl SectorReader for Drive {
|
||||||
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||||
self.read_disc(lba, count, buf)
|
self.read(lba, count, buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
pub mod aacs;
|
pub mod aacs;
|
||||||
pub(crate) mod clpi;
|
pub(crate) mod clpi;
|
||||||
pub mod css;
|
pub mod css;
|
||||||
|
pub mod decrypt;
|
||||||
pub mod disc;
|
pub mod disc;
|
||||||
pub mod drive;
|
pub mod drive;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@@ -107,6 +108,7 @@ pub use mux::NetworkStream;
|
|||||||
pub use mux::NullStream;
|
pub use mux::NullStream;
|
||||||
pub use mux::StdioStream;
|
pub use mux::StdioStream;
|
||||||
pub use mux::{open_input, open_output, parse_url, InputOptions, StreamUrl};
|
pub use mux::{open_input, open_output, parse_url, InputOptions, StreamUrl};
|
||||||
|
pub use decrypt::{DecryptKeys, decrypt_sectors};
|
||||||
pub use scsi::ScsiTransport;
|
pub use scsi::ScsiTransport;
|
||||||
pub use sector::SectorReader;
|
pub use sector::SectorReader;
|
||||||
pub use speed::DriveSpeed;
|
pub use speed::DriveSpeed;
|
||||||
|
|||||||
+9
-42
@@ -17,13 +17,6 @@ use crate::error::Error;
|
|||||||
use crate::speed::DriveSpeed;
|
use crate::speed::DriveSpeed;
|
||||||
use std::io::{self, Read, Write};
|
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.
|
/// Options for opening a disc stream.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DiscOptions {
|
pub struct DiscOptions {
|
||||||
@@ -54,8 +47,7 @@ pub struct DiscStream {
|
|||||||
current_offset: u32,
|
current_offset: u32,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
content_format: ContentFormat,
|
content_format: ContentFormat,
|
||||||
aacs: Option<AacsDecrypt>,
|
decrypt_keys: crate::decrypt::DecryptKeys,
|
||||||
css: Option<crate::css::CssState>,
|
|
||||||
unit_key_idx: usize,
|
unit_key_idx: usize,
|
||||||
read_buf: Vec<u8>,
|
read_buf: Vec<u8>,
|
||||||
/// Current batch size in sectors (adapts on errors)
|
/// Current batch size in sectors (adapts on errors)
|
||||||
@@ -99,11 +91,7 @@ impl DiscStream {
|
|||||||
let disc_title = disc.titles[title_index].clone();
|
let disc_title = disc.titles[title_index].clone();
|
||||||
let extents = disc_title.extents.clone();
|
let extents = disc_title.extents.clone();
|
||||||
let content_format = disc_title.content_format;
|
let content_format = disc_title.content_format;
|
||||||
let aacs = disc.aacs.as_ref().map(|a| AacsDecrypt {
|
let decrypt_keys = disc.decrypt_keys();
|
||||||
unit_keys: a.unit_keys.clone(),
|
|
||||||
read_data_key: a.read_data_key,
|
|
||||||
});
|
|
||||||
let css = disc.css.clone();
|
|
||||||
|
|
||||||
let max_batch = detect_max_batch_sectors(session.device_path());
|
let max_batch = detect_max_batch_sectors(session.device_path());
|
||||||
|
|
||||||
@@ -118,8 +106,7 @@ impl DiscStream {
|
|||||||
current_extent: 0,
|
current_extent: 0,
|
||||||
current_offset: 0,
|
current_offset: 0,
|
||||||
content_format,
|
content_format,
|
||||||
aacs,
|
decrypt_keys,
|
||||||
css,
|
|
||||||
unit_key_idx: 0,
|
unit_key_idx: 0,
|
||||||
read_buf: Vec::with_capacity(max_batch as usize * 2048),
|
read_buf: Vec::with_capacity(max_batch as usize * 2048),
|
||||||
batch_sectors: max_batch,
|
batch_sectors: max_batch,
|
||||||
@@ -137,7 +124,7 @@ impl DiscStream {
|
|||||||
|
|
||||||
/// Read sectors from the drive into `self.read_buf`.
|
/// Read sectors from the drive into `self.read_buf`.
|
||||||
fn read_sectors(&mut self, lba: u32, count: u16) -> Result<(), Error> {
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,32 +235,12 @@ impl DiscStream {
|
|||||||
/// Decrypt the contents of `self.read_buf` in-place and copy the
|
/// Decrypt the contents of `self.read_buf` in-place and copy the
|
||||||
/// decrypted data into `self.batch_buf`.
|
/// decrypted data into `self.batch_buf`.
|
||||||
fn decrypt_and_buffer(&mut self) {
|
fn decrypt_and_buffer(&mut self) {
|
||||||
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
|
||||||
let total_bytes = self.read_buf.len();
|
let total_bytes = self.read_buf.len();
|
||||||
|
crate::decrypt::decrypt_sectors(
|
||||||
if let Some(ref aacs) = self.aacs {
|
&mut self.read_buf[..total_bytes],
|
||||||
let uk = aacs
|
&self.decrypt_keys,
|
||||||
.unit_keys
|
self.unit_key_idx,
|
||||||
.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
|
|
||||||
|
|
||||||
// Swap buffers instead of copying — the old batch_buf becomes
|
// Swap buffers instead of copying — the old batch_buf becomes
|
||||||
// read_buf and will be overwritten on the next read.
|
// read_buf and will be overwritten on the next read.
|
||||||
|
|||||||
Reference in New Issue
Block a user