D009 - The crypto chain is unchanged between generations, and that is a finding
Recovering a package's filesystem key takes four steps and three primitives:
entry 0x10, bytes 0x400..0x500 --RSA-2048--> dk3
sha256(entry 0x20's table ROW || dk3) -> key and iv
entry 0x20's data --AES-128-CBC--> wrapped image key
that --RSA-2048--> the filesystem key
Every source for this is previous-generation. It opens all three current-generation packages on the first attempt, each yielding a distinct 32-byte key. So the chain did not change across the generation boundary - worth recording as a positive result, because the container's magic did, and there was no reason to assume one from the other.
The row, not the data
The hash takes the image-key entry's table row - the thirty-two bytes describing it - and not the data that row points at. Two different thirty-two-byte quantities associated with one entry, and choosing wrong produces a key that is exactly as plausible and entirely wrong.
Package::entry_row exists as an accessor of its own for
that reason. A single method returning "the entry's bytes" would make
the mistake natural and invisible.
Padding is checked, and this is where a wrong key hides
Both RSA steps yield a PKCS#1 v1.5 block, and the markers are validated before the payload is taken. Skipping that check does not fail - it produces a key-shaped quantity that decrypts everything to noise, and the symptom appears hundreds of lines later as an unreadable filesystem rather than as the wrong key it is.
The failure is also informative when it happens: a malformed
block almost always means the package is retail rather than fake, so
NotAFakePackage says so instead of reporting a corrupt
file. That is a wall rather than a bug, and the error text says
which.
Dependencies, and why not the obvious one
num-bigint rather than a full RSA crate. What is needed
is one modular exponentiation against a modulus and exponent already in
hand - not key parsing, generation, or signature verification. A crate
doing all four would be a larger surface for a smaller job, and this
code never verifies a signature: it only unwraps blocks somebody else
wrapped.
Status: decided.