parse_device_key parsed node/uv/shift via from_str_radix(trim_start_matches("0x")),
a CASE-SENSITIVE strip: an uppercase 0X prefix was not removed, from_str_radix
failed, and the WHOLE device key was dropped. Now uses the canonical
libfreemkv::hex::parse_hex_u16/u32/u8. Red->green regression test added.
Base per-CPS-unit keys and the AACS 2.1 forensic index set are now two explicit
trait operations instead of one overloaded get_uk whose return count was implicit.
keydb serves unit keys (forensic opts out via the default); online implements both;
MultiSource drives both in order. Teed up for 1.4.5. Local WIP baseline.
The key service now always returns {"UK":[...]} - an array of one for a
plain movie sample, or all 32 index-ordered variant keys for a forensic
sample. OnlineSource::query accepts both the legacy string form and the
array form, emitting one UnitKey per element (index = array position).
Re-export MIN_SAMPLE_UNITS so callers size their samples correctly.
The KEYDB-gated test that feeds real key material into libfreemkv's AACS crypto
now composes decrypt_unit + is_clean (the removed decrypt_unit_try_keys wrapper's
behaviour), unchanged semantics.
leak-guard bans the .invalid TLD (grouped with private .internal/.local/etc);
example.test is the RFC 6761 reserved test TLD — equally unreachable, not on
the ban list — so the leak-guard CI check passes.
- to_keydb_cfg emits the sibling `| HC2 |` line (AACS 2.0 host cert),
the inverse of parse_host_cert_v2, so a round-trip never silently
drops v2 host certs.
- save-validation mirrors the parser's real disc-entry rule exactly: a
`0x` line counts as an entry only if it also contains ` = `, so
validating + persisting content that parses to zero usable entries
(e.g. a stray `0xDEADBEEF` line) can no longer succeed.
- disc-entry title is kept verbatim (parens and all) on parse, matching
the emit path so it round-trips.
libfreemkv deleted the aacs::boil veneer, so switch the resolve path to the
raw primitives: derive_media_key_from_{pk,dk}, derive_vuk, decrypt_unit_key
from aacs::derive, and the newtypes from aacs::types. Add a local uks_from_vuk
helper composing decrypt_unit_key. No behaviour change; fmt/clippy/test green
on Rust 1.86.
libfreemkv's AACS work changes mk_from_dk from (dk, mkb, vid) to (dk, mkb) —
the VID enters at the VUK step, not the MK step. Adopt the 2-arg call so the DK
pool is tried unconditionally (no longer gated on a VID being present), matching
libfreemkv. Without this keysources fails to compile against the new libfreemkv.
(Equivalent to the stranded b80d4cb; applied directly since that branch had
diverged surrounding context.)
The online source dropped any MKB over 10 MiB while libfreemkv's reader
captures up to 64 MiB — an MKB in that band was silently un-forwardable
(no key, no surfaced cause). Match the cap and log when it is exceeded.
unit_keys_from now loads all stored unit keys AND the VUK/MK-derived ones, deduping by key value instead of first-hit, so a disc with partial stored UKs plus a VUK yields every declared CPS unit key. Plus keydb.cfg format round-trip (to_keydb_cfg inverse of parse).
Found via a parse->serialize->parse->serialize byte-identical check over the full
182,535-disc OEM keydb:
- Title was reduced to a parenthesised substring, truncating real titles
("Lawrence of Arabia (Restored Version) - Disc 2 - 4K Ultra HD" -> "Restored
Version"). Keep the title VERBATIM (faithful copy); display prettification is a
separate layer.
- A disc titled exactly "M" (`= M | M | 0x...`) had its title eaten as the M
field tag, dropping the real media key. Field scan now skips parts[0] (always
the title), so a title that is a tag letter (M/I/V/U/D) can't shadow a field.
- Add to_keydb_cfg_is_idempotent_on_real_keydb (KEYDB_PATH-gated): proves parse
is lossless on its own output and the serializer is deterministic.
Central round-trippable codec: parse + to_keydb_cfg in one place.
Emits HC, DK, PK, then disc entries sorted by hash. Round-trip test
(parse -> serialize -> re-parse) covers HC/DK/PK/disc. disc_id -> vid
names the field for what it is (the I-field volume ID).
Make the parser public — parsing a keydb is not secret (freemkv uses it, and so
do tools that build a per-disc registry from it, e.g. a Volume-ID index). Purely
additive: a private module in this public crate becomes nameable; no behaviour
change, nothing removed.
Move the keydb save/validation/atomic-write path out of libfreemkv onto
KeydbSource. New KeydbSource::save(bytes) validates + decompresses (zip /
gz / plain, decompressed-size capped) and crash-safely writes to the
source's OWN path (sibling-temp + fsync + rename + parent-dir fsync) —
not a hardcoded default, so the caller chooses the destination.
KeydbSource::update(fetch, url) calls an INJECTED fetch closure then
save, keeping this crate transport-agnostic on the update path (the app
supplies its own TLS / SSRF-guarded transport). UpdateResult moves here
and is re-exported. Add flate2 + zip (already in the resolved graph via
libfreemkv) for decompression; no new HTTP stack.
unit_keys_from now resolves the Media Key in order: stored per-disc MK
-> keydb Processing Key pool (mk_from_pk vs this disc's own MKB) ->
device-key pool (mk_from_dk), then MK+VID -> VUK -> UK. MK/VUK entries
still honored directly; cross-disc MK-pool brute stays retired. Fixes the
factually-wrong justifying comment + adds PK-pool KATs.
Move the encrypted sample reader and the key-resolution loop into libfreemkv
(they read the disc and validate keys — decryption mechanism, not lookup). A
key source now only looks a key up and hands it back.
A keydb can hand out a per-disc terminal Key::Unit (a UK entry keyed on
disc_hash). Unlike a derived key (Device/Processing/Media/Volume), a
terminal UK is applied as-is by Disc::decrypt_with: it is NOT re-derived
through the MKB-verified AACS resolver, so a UK entry whose hash matches
the disc but whose key bytes are wrong is only disproved by descrambling
real ciphertext.
KeydbSource inherited the default needs_samples() == false, so on the
autorip auto-resume / mux-worker path (which samples units only when some
source reports needs_samples()) a keydb-only resolve ran with empty
samples and committed a wrong UK as success, muxing undecryptable video
while reporting done. The CLI was unaffected because it always samples.
Override needs_samples() to true on KeydbSource so every consumer samples
encrypted units before resolving and a wrong keydb UK is rejected on all
paths. Regression test asserts the override.
- KeydbSource implements KeySource::host_certs(), delegating to the
inherent host_certs() — surfaces the | HC | / | HC2 | certs already
parsed from keydb.cfg by libfreemkv's parser, so the OEM cert route
collects them across the keysource layer. No new parsing.
- OnlineSource::host_certs() is a no-op stub: returns empty with zero
network access (no client fetch, no server endpoint). Online host-cert
serving is deferred. TODO(owner) marker left in place.
Tests: trait host_certs returns the keydb HC row; empty when keydb
missing; online host_certs is an empty no-op without network.
OnlineSource::query() now includes the disc's volume_label (UDF/ISO volume id)
as a plain-text `title` field in the /decode POST, so the key service can build
a disc_hash → title catalog from real rips. Depends on libfreemkv 0.30.
Each source implements next_key (a cursor over its candidates) instead of
returning them all at once. The keydb hands its per-disc candidates out
UK-first (UK > VK > MK > DK) so a stale/wrong VUK never pre-empts a good UK in
the same entry; online and mapfile are one-shot. MultiSource composes sources
in the caller's chosen order and resolve_and_apply drives the
next_key -> decrypt_with loop, stopping at the first key that decrypts.
read_sample_units moves here so the CLI and autorip share one content sampler.
OnlineSource: the remote key-service client (moved out of autorip), posting the
disc's Unit_Key_RO.inf + MKB + Volume ID + encrypted content samples to the
service and returning the resolved unit key as a terminal Key::Unit candidate.
Kept out of libfreemkv so the library stays network-free. Source-internal
failures (unreachable / status / parse) are logged and surface as "no
candidate" so the next source is tried.
MapfileSource: reads a rip mapfile's persisted unit keys (the resume / deferred-
mux fast path) and offers them as a Key::Unit candidate. Keyed by mapfile path.
DiscInputs gained an app-populated `samples` field for sources that validate
against ciphertext server-side (OnlineSource); local sources ignore it.
The published key-source layer for libfreemkv. libfreemkv does no lookup; it
is handed a Key and derives down. This crate provides the KeySource impls that
do the lookup and hand a Key in. Applications choose and order the sources.
This first cut ships:
- KeydbSource: parses a local keydb.cfg and enumerates its material as ordered
candidate keys (per-disc VUK/unit/media first, then the universal device-key,
processing-key, and media-key pools). It does no derivation — the library
walks the MKB and verifies media keys. Candidate ordering lets the library
try each path a keydb can satisfy.
- resolve_first: tries each source's candidates in order and returns the first
the caller's validator accepts (validate-before-return), so a stale entry
falls through to the next source.
OnlineSource (remote key service) and MapfileSource (cached unit key) land with
the application wiring, where the sample-read and mapfile paths already live.