AACS: cutover — lookup-free lib; DriveCredentials for the handshake
The library no longer loads a keydb anywhere. The scan path always captures the disc's AACS inputs (MKB, VID, Unit_Key_RO.inf) and resolves NO key; a caller resolves a Key from a key source and applies it via Disc::decrypt_with. - ScanOptions loses keydb_path / unit_key / disable_keydb (and the path search); it now carries only optional DriveCredentials (host certs) for the live-drive AACS handshake. do_handshake_cert uses those instead of loading the keydb. An unlocked / LibreDrive drive takes the OEM Volume-ID path and needs none. - The mux input() path takes caller-resolved unit_keys instead of a keydb_path, and applies them via decrypt_with. - Deleted the now-dead inline resolve_encryption / resolve_encryption_static. All 700+ lib tests pass.
This commit is contained in:
+13
-255
@@ -179,54 +179,33 @@ impl Disc {
|
||||
session: &mut crate::drive::Drive,
|
||||
opts: &ScanOptions,
|
||||
) -> (Option<HandshakeResult>, Option<Error>) {
|
||||
use crate::aacs::{self, KeyDb};
|
||||
use crate::aacs;
|
||||
|
||||
let keydb_path = match opts.resolve_keydb() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
// Host certs come from the caller's DriveCredentials (e.g. the keydb's
|
||||
// host_certs(), sourced app-side) — the library does not load a keydb.
|
||||
// Absent ⇒ no cert auth: an unlocked / LibreDrive drive already returned
|
||||
// a Volume ID via the OEM path before reaching here, so this is the
|
||||
// locked-drive-without-credentials case.
|
||||
let host_certs: &[aacs::HostCert] = match &opts.credentials {
|
||||
Some(c) if !c.host_certs.is_empty() => &c.host_certs,
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
phase = "handshake_no_keydb",
|
||||
"no KEYDB found in search paths; handshake skipped"
|
||||
phase = "handshake_no_credentials",
|
||||
"no drive credentials supplied; cert handshake skipped"
|
||||
);
|
||||
return (None, None);
|
||||
}
|
||||
};
|
||||
let keydb = match KeyDb::load(&keydb_path) {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
phase = "handshake_keydb_load_failed",
|
||||
io_error_kind = ?e.kind(),
|
||||
keydb = %keydb_path.display(),
|
||||
"KEYDB load failed; handshake skipped"
|
||||
);
|
||||
return (
|
||||
None,
|
||||
Some(Error::KeydbLoad {
|
||||
path: keydb_path.display().to_string(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let host_cert_count = keydb.host_certs.len();
|
||||
let host_cert_count = host_certs.len();
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
phase = "handshake_start",
|
||||
host_cert_count,
|
||||
keydb = %keydb_path.display(),
|
||||
"handshake starting"
|
||||
);
|
||||
|
||||
if host_cert_count == 0 {
|
||||
// No host certs in keydb -> cert auth cannot proceed.
|
||||
// Surface as RawReadUnsupported so the caller knows
|
||||
// neither path is available on this configuration.
|
||||
return (None, Some(Error::AacsRawReadUnsupported));
|
||||
}
|
||||
|
||||
// v0.25.7 wedge fix. Pre-0.25.7 this loop fired up to 16 AACS
|
||||
// authenticate attempts back-to-back with no pause. Each attempt
|
||||
// is 5-10 SCSI REPORT_KEY/SEND_KEY exchanges. On a disc whose
|
||||
@@ -246,7 +225,7 @@ impl Disc {
|
||||
const MAX_CERT_ATTEMPTS: usize = 3;
|
||||
const PER_CERT_BACKOFF_MS: u64 = 1000;
|
||||
let mut last_err_code: Option<u16> = None;
|
||||
for (idx, hc) in keydb.host_certs.iter().take(MAX_CERT_ATTEMPTS).enumerate() {
|
||||
for (idx, hc) in host_certs.iter().take(MAX_CERT_ATTEMPTS).enumerate() {
|
||||
if idx > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_millis(PER_CERT_BACKOFF_MS));
|
||||
}
|
||||
@@ -317,227 +296,6 @@ impl Disc {
|
||||
(None, Some(Error::AacsHostCertRejected))
|
||||
}
|
||||
|
||||
/// Resolve disc encryption — AACS 1.0, AACS 2.0, CSS, or none.
|
||||
///
|
||||
/// Reads AACS files from UDF (via SectorSource), resolves keys through
|
||||
/// whatever path works: KEYDB VUK lookup, media key derivation, processing
|
||||
/// keys, device keys. Uses handshake result (volume ID, bus key) if available.
|
||||
pub(super) fn resolve_encryption(
|
||||
udf_fs: &udf::UdfFs,
|
||||
reader: &mut dyn SectorSource,
|
||||
keydb_path: &std::path::Path,
|
||||
handshake: Option<&HandshakeResult>,
|
||||
) -> Result<AacsState> {
|
||||
use crate::aacs::{self, KeyDb};
|
||||
use crate::drm::{DrmContext, DrmProbe, DrmScheme, ResolvedScheme};
|
||||
|
||||
let keydb = KeyDb::load(keydb_path).map_err(|_| Error::KeydbLoad {
|
||||
path: keydb_path.display().to_string(),
|
||||
})?;
|
||||
|
||||
// Read AACS files from disc/image via UDF
|
||||
let uk_ro_data = udf_fs
|
||||
.read_file(reader, "/AACS/Unit_Key_RO.inf")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
|
||||
.map_err(|_| Error::AacsNoKeys)?;
|
||||
|
||||
// Log the disc hash so we can confirm whether it's present in KEYDB
|
||||
// when key resolution fails. The disc hash is SHA-1 of the full
|
||||
// Unit_Key_RO.inf file bytes — same value KEYDB.cfg keys VUK entries by.
|
||||
let dh = crate::aacs::disc_hash(&uk_ro_data);
|
||||
let dh_hex = crate::aacs::disc_hash_hex(&dh);
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
phase = "scan_aacs_disc_hash",
|
||||
disc_hash = %dh_hex,
|
||||
uk_ro_len = uk_ro_data.len(),
|
||||
"disc hash computed (compare with keydb.cfg entries)"
|
||||
);
|
||||
|
||||
let cc_data = udf_fs
|
||||
.read_file(reader, "/AACS/Content000.cer")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/Content001.cer"))
|
||||
.ok();
|
||||
|
||||
let mkb_data = udf_fs
|
||||
.read_file(reader, "/AACS/MKB_RW.inf")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/MKB_RO.inf"))
|
||||
.ok();
|
||||
let mkb_ver = mkb_data.as_deref().and_then(aacs::mkb_version);
|
||||
|
||||
let mkb_first_64_hex = mkb_data
|
||||
.as_deref()
|
||||
.map(|m| {
|
||||
m.iter()
|
||||
.take(64)
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect::<String>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
phase = "scan_aacs_mkb_info",
|
||||
mkb_present = mkb_data.is_some(),
|
||||
mkb_len = mkb_data.as_deref().map(|m| m.len()).unwrap_or(0),
|
||||
mkb_version = ?mkb_ver,
|
||||
mkb_first_64 = %mkb_first_64_hex,
|
||||
keydb_disc_count = keydb.disc_entries.len(),
|
||||
keydb_dk_count = keydb.device_keys.len(),
|
||||
keydb_pk_count = keydb.processing_keys.len(),
|
||||
"AACS resolution inputs"
|
||||
);
|
||||
|
||||
// Use handshake volume ID if available, otherwise zeros
|
||||
// (KEYDB VUK lookup by disc hash works without volume ID;
|
||||
// paths 2/3/4 in `resolve_keys` short-circuit on the zero
|
||||
// sentinel and don't waste cycles trying to derive against
|
||||
// garbage input).
|
||||
let volume_id = handshake.map(|h| h.volume_id).unwrap_or([0u8; 16]);
|
||||
let vid_available = volume_id != [0u8; 16];
|
||||
let read_data_key = handshake.and_then(|h| h.read_data_key);
|
||||
|
||||
// Resolve: tries all available paths — KEYDB VUK, media key, processing key, device key.
|
||||
//
|
||||
// Distinguish "we had every input and still missed" from "we
|
||||
// never had VID so the derivation paths couldn't run." The
|
||||
// former points at a stale keydb / unsupported MKB; the
|
||||
// latter points at a failed handshake upstream. Path 1
|
||||
// (disc-hash lookup) ran without VID and missed -> disc isn't
|
||||
// in the keydb. If the caller has a handshake-failure reason
|
||||
// it overrides this in `scan_with`.
|
||||
let miss_error = if vid_available {
|
||||
Error::AacsMkUnavailable
|
||||
} else {
|
||||
Error::AacsVukNotInKeydb
|
||||
};
|
||||
|
||||
// Build a probe + context and let the dispatcher pick V10 / V20
|
||||
// / V21. CSS is impossible here (this function is only called
|
||||
// when /AACS exists), so we don't populate the DVD probe sector
|
||||
// or a CSS context.
|
||||
let probe = DrmProbe {
|
||||
dvd_sample_sector: None,
|
||||
content_cert: cc_data.as_deref(),
|
||||
mkb: mkb_data.as_deref(),
|
||||
};
|
||||
let scheme = match DrmScheme::detect(&probe) {
|
||||
Some(s) => s,
|
||||
None => return Err(miss_error),
|
||||
};
|
||||
let providers: &[&dyn aacs::KeyProvider] = &[&keydb];
|
||||
let aacs_ctx = aacs::ResolveContext {
|
||||
unit_key_ro: &uk_ro_data,
|
||||
content_cert: cc_data.as_deref(),
|
||||
volume_id: &volume_id,
|
||||
providers,
|
||||
mkb: mkb_data.as_deref(),
|
||||
};
|
||||
let mut ctx = DrmContext {
|
||||
aacs: Some(aacs_ctx),
|
||||
css: None,
|
||||
};
|
||||
let resolved = match scheme.load(&mut ctx) {
|
||||
Some(ResolvedScheme::Aacs(r)) => r,
|
||||
// Resolution against /AACS inputs can only produce AACS
|
||||
// keys. Either the dispatcher returned None (load failed)
|
||||
// or — structurally impossible here — a CSS state. Both
|
||||
// surface as the upstream miss-error.
|
||||
_ => return Err(miss_error),
|
||||
};
|
||||
|
||||
Ok(AacsState {
|
||||
version: match resolved.version {
|
||||
aacs::AacsVersion::V10 => 1,
|
||||
aacs::AacsVersion::V20 | aacs::AacsVersion::V21 => 2,
|
||||
},
|
||||
bus_encryption: resolved.bus_encryption,
|
||||
mkb_version: mkb_ver,
|
||||
disc_hash: aacs::disc_hash_hex(&resolved.disc_hash),
|
||||
key_source: match resolved.key_source {
|
||||
1 => KeyOrigin::DeviceKey,
|
||||
2 => KeyOrigin::ProcessingKey,
|
||||
3 => KeyOrigin::KeyDbDerived,
|
||||
4 => KeyOrigin::KeyDb,
|
||||
5 => KeyOrigin::KeyDbUnitKeys,
|
||||
_ => KeyOrigin::KeyDb,
|
||||
},
|
||||
vuk: resolved.vuk,
|
||||
unit_keys: resolved.unit_keys,
|
||||
read_data_key,
|
||||
volume_id,
|
||||
// Stash the AACS inputs so a later out-of-band `Disc::decrypt_with`
|
||||
// (caller-resolved Key → derive down) can run without re-reading
|
||||
// the disc. The borrows in `aacs_ctx` ended when `scheme.load`
|
||||
// returned, so these buffers are free to move here.
|
||||
uk_ro: uk_ro_data,
|
||||
mkb: mkb_data.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolve encryption from a caller-supplied Unit Key (the external key service
|
||||
/// path). No keydb, no derivation: read `Unit_Key_RO.inf` for the disc
|
||||
/// hash + version/bus-encryption flags, then use `unit_key` directly as
|
||||
/// CPS unit 1's decryption key. The handshake (if any) still supplies the
|
||||
/// volume ID and AACS 2.0 read-data key for bus decryption.
|
||||
pub(super) fn resolve_encryption_static(
|
||||
udf_fs: &udf::UdfFs,
|
||||
reader: &mut dyn SectorSource,
|
||||
unit_key: [u8; 16],
|
||||
handshake: Option<&HandshakeResult>,
|
||||
) -> Result<AacsState> {
|
||||
use crate::aacs;
|
||||
|
||||
let uk_ro_data = udf_fs
|
||||
.read_file(reader, "/AACS/Unit_Key_RO.inf")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
|
||||
.map_err(|_| Error::AacsNoKeys)?;
|
||||
let dh = aacs::disc_hash(&uk_ro_data);
|
||||
|
||||
let cc = udf_fs
|
||||
.read_file(reader, "/AACS/Content000.cer")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/Content001.cer"))
|
||||
.ok()
|
||||
.as_deref()
|
||||
.and_then(aacs::parse_content_cert);
|
||||
let bus_encryption = cc.as_ref().map(|c| c.bus_encryption).unwrap_or(false);
|
||||
let version = match cc.as_ref().map(|c| c.version) {
|
||||
Some(aacs::AacsVersion::V10) => 1,
|
||||
Some(_) => 2,
|
||||
None if bus_encryption => 2,
|
||||
None => 1,
|
||||
};
|
||||
|
||||
let mkb_ver = udf_fs
|
||||
.read_file(reader, "/AACS/MKB_RW.inf")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/MKB_RO.inf"))
|
||||
.ok()
|
||||
.as_deref()
|
||||
.and_then(aacs::mkb_version);
|
||||
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
phase = "scan_aacs_external_uk",
|
||||
disc_hash = %aacs::disc_hash_hex(&dh),
|
||||
version,
|
||||
bus_encryption,
|
||||
"using caller-supplied unit key"
|
||||
);
|
||||
|
||||
Ok(AacsState {
|
||||
version,
|
||||
bus_encryption,
|
||||
mkb_version: mkb_ver,
|
||||
disc_hash: aacs::disc_hash_hex(&dh),
|
||||
key_source: KeyOrigin::ExternalUk,
|
||||
vuk: None,
|
||||
unit_keys: vec![(1, unit_key)],
|
||||
read_data_key: handshake.and_then(|h| h.read_data_key),
|
||||
volume_id: handshake.map(|h| h.volume_id).unwrap_or([0u8; 16]),
|
||||
uk_ro: Vec::new(),
|
||||
mkb: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a keys-free AACS state that carries only the Volume ID (+ version
|
||||
/// metadata), for callers that resolve Unit Keys out-of-band and have
|
||||
/// disabled the local keydb. The VID is on-disc content read during the
|
||||
|
||||
+32
-113
@@ -939,59 +939,31 @@ impl KeyOrigin {
|
||||
|
||||
// ─── Disc scanning ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Standard KEYDB.cfg search locations (compatible with libaacs).
|
||||
const KEYDB_SEARCH_PATHS: &[&str] = &[
|
||||
".config/aacs/KEYDB.cfg", // libaacs standard path
|
||||
".config/freemkv/keydb.cfg", // freemkv download path
|
||||
];
|
||||
const KEYDB_SYSTEM_PATH: &str = "/etc/aacs/KEYDB.cfg";
|
||||
|
||||
/// Options for disc scanning.
|
||||
#[derive(Default)]
|
||||
pub struct ScanOptions {
|
||||
/// Path to KEYDB.cfg for AACS key lookup.
|
||||
/// If None, searches standard locations ($HOME/.config/aacs/ and /etc/aacs/).
|
||||
pub keydb_path: Option<std::path::PathBuf>,
|
||||
/// Caller-supplied Unit Key — an alternative to keydb lookup. When set,
|
||||
/// libfreemkv skips keydb lookup and all derivation and uses this key
|
||||
/// directly to decrypt; it takes precedence over `keydb_path`. The caller
|
||||
/// obtains the key however it likes; libfreemkv stays free of any network
|
||||
/// dependency.
|
||||
pub unit_key: Option<[u8; 16]>,
|
||||
/// Disable KEYDB entirely: skip both the explicit `keydb_path` and the
|
||||
/// standard-location search, so no keydb is loaded for this scan. A caller
|
||||
/// that resolves keys out-of-band (e.g. a remote key service) sets this so
|
||||
/// a keydb that merely happens to sit in a default location does not shadow
|
||||
/// the out-of-band path. `unit_key` still takes precedence over everything.
|
||||
pub disable_keydb: bool,
|
||||
/// AACS host credentials for the live-drive authenticated handshake.
|
||||
///
|
||||
/// Optional and source-agnostic: an unlocked / LibreDrive drive uses the OEM
|
||||
/// Volume-ID path and needs none, and an ISO scan has no handshake at all. The
|
||||
/// caller supplies the host cert(s) from wherever it likes — today the keydb's
|
||||
/// `host_certs()`, tomorrow a cert file or built-in. Decoupled from the key
|
||||
/// source: a locked drive needs the cert to unlock even when the decryption key
|
||||
/// comes from an online service.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct DriveCredentials {
|
||||
/// Host certificate(s) + private key(s) for the SCSI AACS handshake.
|
||||
pub host_certs: Vec<crate::aacs::HostCert>,
|
||||
}
|
||||
|
||||
impl ScanOptions {
|
||||
/// Resolve KEYDB path: explicit path first, then standard locations.
|
||||
/// Returns `None` when `disable_keydb` is set — no keydb is consulted.
|
||||
fn resolve_keydb(&self) -> Option<std::path::PathBuf> {
|
||||
if self.disable_keydb {
|
||||
return None;
|
||||
}
|
||||
if let Some(p) = &self.keydb_path {
|
||||
if p.exists() {
|
||||
return Some(p.clone());
|
||||
}
|
||||
}
|
||||
if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
|
||||
for relative in KEYDB_SEARCH_PATHS {
|
||||
let p = std::path::PathBuf::from(&home).join(relative);
|
||||
if p.exists() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
let p = std::path::PathBuf::from(KEYDB_SYSTEM_PATH);
|
||||
if p.exists() {
|
||||
return Some(p);
|
||||
}
|
||||
None
|
||||
}
|
||||
/// Options for disc scanning.
|
||||
///
|
||||
/// libfreemkv is lookup-free — it resolves no keys. The caller resolves a key
|
||||
/// out-of-band (a key source) and applies it via [`Disc::decrypt_with`]. The
|
||||
/// only scan input is the optional drive credentials for the live-drive
|
||||
/// authenticated handshake.
|
||||
#[derive(Default)]
|
||||
pub struct ScanOptions {
|
||||
/// Host credentials for the live-drive AACS handshake. `None` for an
|
||||
/// unlocked / LibreDrive drive (OEM Volume-ID path) and for ISO scans.
|
||||
pub credentials: Option<DriveCredentials>,
|
||||
}
|
||||
|
||||
/// Quick disc identification — name, format, capacity. No title/stream parsing.
|
||||
@@ -1233,7 +1205,7 @@ impl Disc {
|
||||
capacity: u32,
|
||||
handshake: Option<HandshakeResult>,
|
||||
handshake_error: Option<Error>,
|
||||
opts: &ScanOptions,
|
||||
_opts: &ScanOptions,
|
||||
udf_fs: udf::UdfFs,
|
||||
) -> Result<Self> {
|
||||
// 2. Resolve encryption (AACS, CSS, or none)
|
||||
@@ -1242,69 +1214,16 @@ impl Disc {
|
||||
|
||||
let (aacs, aacs_error) = if !encrypted {
|
||||
(None, None)
|
||||
} else if let Some(unit_key) = opts.unit_key {
|
||||
// Second key source: caller supplied the Unit Key directly
|
||||
// (external Unit Key). Skip keydb entirely.
|
||||
match Self::resolve_encryption_static(&udf_fs, reader, unit_key, handshake.as_ref()) {
|
||||
Ok(state) => (Some(state), None),
|
||||
Err(e) => (None, Some(e)),
|
||||
}
|
||||
} else if opts.disable_keydb {
|
||||
// Keydb disabled: keys are resolved out-of-band. Still capture the
|
||||
// VID (read during the handshake) so the out-of-band path has it;
|
||||
// carry no keys (disc reports "encrypted, no keys" until re-scanned
|
||||
// with a resolved Unit Key).
|
||||
} else {
|
||||
// Lookup-free: capture the disc's AACS inputs (MKB, VID,
|
||||
// Unit_Key_RO.inf) but resolve NO key. The caller resolves a Key
|
||||
// from a key source and applies it via `Disc::decrypt_with`. The
|
||||
// disc reports "encrypted, no keys" until then.
|
||||
match Self::resolve_vid_only(&udf_fs, reader, handshake.as_ref()) {
|
||||
Ok(state) => (Some(state), None),
|
||||
Err(e) => (None, Some(e)),
|
||||
}
|
||||
} else {
|
||||
match opts.resolve_keydb() {
|
||||
Some(keydb_path) => {
|
||||
match Self::resolve_encryption(&udf_fs, reader, &keydb_path, handshake.as_ref())
|
||||
{
|
||||
Ok(state) => (Some(state), None),
|
||||
Err(e) => {
|
||||
// When the handshake itself failed AND resolution
|
||||
// bottomed out at "no keys", surface the upstream
|
||||
// handshake failure — it's more actionable than
|
||||
// the generic AacsNoKeys.
|
||||
let final_err = match (&e, handshake_error.as_ref()) {
|
||||
(
|
||||
Error::AacsNoKeys
|
||||
| Error::AacsVukNotInKeydb
|
||||
| Error::AacsVidUnavailable,
|
||||
Some(_),
|
||||
) => handshake_error.unwrap(),
|
||||
_ => e,
|
||||
};
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
phase = "scan_aacs_resolve_failed",
|
||||
error_code = final_err.code(),
|
||||
keydb = %keydb_path.display(),
|
||||
handshake_ok = handshake.is_some(),
|
||||
"AACS key resolution failed"
|
||||
);
|
||||
(None, Some(final_err))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
tracing::warn!(
|
||||
target: "freemkv::disc",
|
||||
phase = "scan_aacs_no_keydb",
|
||||
"encrypted disc but no KEYDB found in search paths"
|
||||
);
|
||||
// Sentinel path string lets autorip's message switch
|
||||
// distinguish "no keydb found anywhere" from "keydb at
|
||||
// <path> failed to parse".
|
||||
let final_err =
|
||||
handshake_error.unwrap_or_else(|| crate::error::Error::KeydbLoad {
|
||||
path: String::from("<no keydb in search paths>"),
|
||||
});
|
||||
(None, Some(final_err))
|
||||
}
|
||||
// A handshake failure (no VID) is more actionable than the
|
||||
// generic capture error, so surface it when present.
|
||||
Err(e) => (None, Some(handshake_error.unwrap_or(e))),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -171,9 +171,9 @@ pub use decrypt::{DecryptKeys, decrypt_sectors, decrypt_threads, set_decrypt_thr
|
||||
// prefix at the crate root to keep both addressable.
|
||||
pub use disc::{
|
||||
AacsState, AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat, DamageSeverity,
|
||||
Disc, DiscFormat, DiscId, DiscTitle, Extent, FrameRate, HdrFormat, Key, KeyOrigin,
|
||||
LabelPurpose, LabelQualifier, PatchOptions, PatchOutcome, Resolution, SampleRate, ScanOptions,
|
||||
Stream, SubtitleStream, SweepOptions, VideoStream, classify_damage,
|
||||
Disc, DiscFormat, DiscId, DiscTitle, DriveCredentials, Extent, FrameRate, HdrFormat, Key,
|
||||
KeyOrigin, LabelPurpose, LabelQualifier, PatchOptions, PatchOutcome, Resolution, SampleRate,
|
||||
ScanOptions, Stream, SubtitleStream, SweepOptions, VideoStream, classify_damage,
|
||||
};
|
||||
pub use keysource::{DiscInputs, KeySource};
|
||||
|
||||
|
||||
+15
-10
@@ -162,10 +162,11 @@ fn validate_network_addr(addr: &str) -> io::Result<()> {
|
||||
/// Options for opening an input stream.
|
||||
#[derive(Default)]
|
||||
pub struct InputOptions {
|
||||
pub keydb_path: Option<String>,
|
||||
/// Caller-supplied Unit Key (external Unit Key) — the second, mutually
|
||||
/// exclusive key source. Takes precedence over `keydb_path`.
|
||||
pub unit_key: Option<[u8; 16]>,
|
||||
/// Caller-resolved per-CPS-unit AACS keys to apply to the scanned disc
|
||||
/// (`(cps_unit, 16-byte key)`). Empty for an unencrypted disc or when the
|
||||
/// caller has no key. The library does no lookup — a key source resolves
|
||||
/// these and the caller passes them here.
|
||||
pub unit_keys: Vec<(u32, [u8; 16])>,
|
||||
pub title_index: Option<usize>,
|
||||
/// Skip decryption — return raw encrypted bytes.
|
||||
pub raw: bool,
|
||||
@@ -184,11 +185,6 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
||||
}
|
||||
StreamUrl::Iso { ref path } => {
|
||||
validate_file_path(path, "iso")?;
|
||||
let scan_opts = crate::disc::ScanOptions {
|
||||
keydb_path: opts.keydb_path.as_ref().map(Into::into),
|
||||
unit_key: opts.unit_key,
|
||||
..Default::default()
|
||||
};
|
||||
// FileSectorSource is the sole file-backed sector source.
|
||||
// It carries the platform-tuned SEQUENTIAL fadvise hint
|
||||
// (so the kernel readahead window widens) and the periodic
|
||||
@@ -196,8 +192,17 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
||||
// when the mux output is being written to the same disk.
|
||||
let mut reader = crate::io::file_sector_source::FileSectorSource::open(path)?;
|
||||
let capacity = reader.capacity_sectors();
|
||||
let disc = crate::disc::Disc::scan_image(&mut reader, capacity, &scan_opts)
|
||||
let mut disc = crate::disc::Disc::scan_image(
|
||||
&mut reader,
|
||||
capacity,
|
||||
&crate::disc::ScanOptions::default(),
|
||||
)
|
||||
.map_err(|e| -> io::Error { e.into() })?;
|
||||
// Apply the caller-resolved keys (lookup-free); decrypt_keys() then
|
||||
// yields them for the stream below.
|
||||
if !opts.unit_keys.is_empty() {
|
||||
let _ = disc.decrypt_with(crate::disc::Key::Unit(opts.unit_keys.clone()));
|
||||
}
|
||||
if disc.titles.is_empty() {
|
||||
return Err(crate::error::Error::NoStreams.into());
|
||||
}
|
||||
|
||||
+15
-32
@@ -126,31 +126,19 @@ fn disc_title_total_sectors() {
|
||||
// ── ScanOptions tests ──────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn scan_options_default() {
|
||||
fn scan_options_default_has_no_credentials() {
|
||||
// Lookup-free: the only scan input is the optional live-drive credentials.
|
||||
let opts = ScanOptions::default();
|
||||
assert!(opts.keydb_path.is_none());
|
||||
assert!(opts.credentials.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_options_with_keydb() {
|
||||
fn scan_options_with_credentials() {
|
||||
let opts = ScanOptions {
|
||||
keydb_path: Some(("/tmp/KEYDB.cfg").into()),
|
||||
..Default::default()
|
||||
credentials: Some(libfreemkv::DriveCredentials::default()),
|
||||
};
|
||||
assert_eq!(
|
||||
opts.keydb_path.as_ref().unwrap().to_str().unwrap(),
|
||||
"/tmp/KEYDB.cfg"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_options_with_keydb_pathbuf() {
|
||||
let path = std::path::PathBuf::from("/home/user/.config/aacs/KEYDB.cfg");
|
||||
let opts = ScanOptions {
|
||||
keydb_path: Some(path.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(opts.keydb_path.unwrap(), path);
|
||||
assert!(opts.credentials.is_some());
|
||||
assert!(opts.credentials.unwrap().host_certs.is_empty());
|
||||
}
|
||||
|
||||
// ── detect_format integration tests ───────────────────────────────────────
|
||||
@@ -511,21 +499,16 @@ fn batch_count_max_batch_sizes() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_encryption_no_keydb() {
|
||||
// A UDF image with /AACS directory but no keydb path -> aacs is None
|
||||
fn scan_encrypted_resolves_no_keys() {
|
||||
// A UDF image with an /AACS directory: the lookup-free scan detects
|
||||
// encryption and captures inputs, but resolves NO key on its own — a
|
||||
// caller applies one later via Disc::decrypt_with.
|
||||
let mut reader = MockSectorReader::new();
|
||||
build_udf_with_aacs_dir(&mut reader);
|
||||
|
||||
// No keydb configured and no standard keydb on the system
|
||||
let opts = ScanOptions {
|
||||
keydb_path: Some(("/nonexistent/path/KEYDB.cfg").into()),
|
||||
..Default::default()
|
||||
};
|
||||
let disc = Disc::scan_image(&mut reader, 1000, &opts).unwrap();
|
||||
let disc = Disc::scan_image(&mut reader, 1000, &ScanOptions::default()).unwrap();
|
||||
|
||||
// The disc detects encryption but can't resolve keys without a keydb
|
||||
assert!(
|
||||
disc.aacs.is_none(),
|
||||
"aacs should be None when keydb is unavailable"
|
||||
);
|
||||
// No unit keys without an external key (the mock has no Unit_Key_RO.inf to
|
||||
// capture, so the keyless state isn't even built) — either way, no keys.
|
||||
assert!(matches!(disc.decrypt_keys(), libfreemkv::DecryptKeys::None));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user