Crikey! — CRI + key.
Recovers the decryption key of a CRI USM video without being told it, then decrypts the clip and hands it to ffmpeg. One small exe, nothing to install or configure.
A few days ago I was looking at the statistical fingerprints of VP9 and wondered whether they were enough to identify a key on their own. Reading through WannaCRI turned that hunch into something concrete: given how USM encrypts video, brute-forcing the key is not only possible, it is cheap. So I built a prototype — and sat on it. If a tool like this drew attention the vendor might simply change the scheme, and a lot of people depend on the current one continuing to work.
Then I found UsmDiviner, which got to essentially the same result months earlier and is already public. The cat is out of the bag, and what I have is small, fast and pleasant to use. Here it is.
Drop your clips into a USM folder next to the exe and double-click it.
crikey ── CRI USM key recovery
──────────────────────────────────────────────────────────
████████████████████████████────────────── 1234/1907
38 files/s · elapsed 00:32 · eta 00:18 · 20 threads
1180 cracked 0 from Key.json 42 unverified 2 unreadable
──────────────────────────────────────────────────────────
✓ AbyssCommenVideo.usm ············ 23782827783468063 cracked
✓ Abyss_Intro.usm ················· 53073351375900521 cracked
✓ Gacha_Miyabi.usm ················ 9980390386313553 cracked
? BabelTowerLayerStart.usm ········ no key verified
From a terminal, if you prefer:
crikey.exe every .usm in the USM folder next to the exe
crikey.exe video.usm [more...] just those files
crikey.exe video.usm --key N use a key you already have, and say whether it holds up
crikey.exe --keys-only recover keys, skip the video conversion
Run it with no USM folder and it prints its own manual instead.
ffmpeg.exe next to the exe is used if present, otherwise PATH. If the game's
Key.json is sitting there too, keys it already knows are used directly and only the
rest get cracked — field names in it are found by shape rather than by name, so the
remangling that comes with game updates does not matter.
Results land in output/: the video, plus Key.json with plain { "file", "key" }
pairs. Clips it could not settle are listed in output/failed.json and nothing is
written for them. A wrong key still "decrypts" — it produces a video that plays and
looks like static — so a visible gap beats a file you would have to notice was broken.
The cipher builds a 32-byte mask from the key, then walks the video applying mask byte 1 to stream byte 1, byte 2 to byte 2, and so on, wrapping every 32 bytes. Its feedback stays inside each of those 32 lanes.
That is the whole game. Because the lanes never mix, each mask byte can be guessed on its own — 32 × 256 tries instead of 2^64. Only 7 bytes of the key reach the mask, each feeding several mask bytes, so all 32 lanes vote on the same 7 unknowns. The votes come from the fact that a correctly decrypted VP9 frame does not look like noise: its last byte is the arithmetic coder's flush byte, which is wildly lopsided, and frames often carry a superframe index whose sub-frame sizes must add up to the frame length exactly — arithmetic a wrong guess cannot pass by luck.
Then the part that matters most: the winner is checked, not assumed. Every frame is parsed as VP9. A key one lane off still decrypts, it just corrupts one byte in 32, so across a whole clip it always trips something. If nothing survives, the clip is declined.
One detail worth knowing: USM keys are 56 bits, not 64
The key schedule reads bytes 0 through 6 and stops. The top byte of a 64-bit USM key has
no effect on decryption at all, so two keys differing only there are the same key.
This is why one clip can appear to have two valid keys — 16097468554912682580 and
28625084454752852, for instance, are equal mod 2^56. Compare USM keys with
& 0xFFFFFFFFFFFFFF.
The other seven bytes are all live: across 1794 clips where this tool and the game's own table agree, all seven match every time. A byte the cipher ignored would match about once in 256.
How much it gets: 1864 of 1905 clips — about 98% — in roughly 15 seconds. The 41 it gives up on are short ones. A clip yields about one usable byte of evidence per frame, so 30 frames cannot pin down 7 key bytes. Length decides this, not content.
How much of that is right: every claimed key that could be checked against an
independent reference held up. 1794 match the game's own key table exactly. 23 disagree
with it — and wherever that was adjudicated with a real decoder, the table was the
stale one: its key gave Invalid frame marker while the recovered key decoded clean.
The other 47 are clips the table has no key for at all, so there is nothing to compare
against; they passed the same full-clip check as everything else.
Those numbers are not padded because the tool would rather answer "I don't know" than guess. Measured on one game's library, though — the method rests on the shape of VP9 frames rather than anything game-specific, but nobody has run it at this scale elsewhere.
- One exe, one dependency. No Python, no virtualenv, no
pip install. - ~1900 clips, 7 GB, in about 15 seconds on 20 threads.
- Double-click it. Nothing to configure, and it will not write a file it is unsure about.
- VP9 only. Sofdec2 also ships Sofdec.Prime, H.264 and AV1. Those decrypt fine once a key is known — the cipher does not care what the payload means — but none of the evidence above exists in them, so they cannot be cracked here.
- Video only, really. This started as a proof of concept and stayed one. Audio is decrypted but only dumped raw, alpha and subtitles are ignored, and there is no pipeline for turning any of it into finished files.
For the parts this does not do, use UsmDiviner. It got here first and covers more ground.
- WannaCRI — the USM container format and the decryption itself, ported from it 1:1.
- UsmDiviner — got to the same core result months earlier and independently: that the cipher's lanes are separable, that the search decomposes the same way, and that a USM key is really 56 bits. Finding it is the reason this one is public at all. I tried two of its ideas here — its bigram statistics and its paired sub-frame header table — and measured both to be redundant against my own corpus, so neither is in the code; that is a property of my test data, not a judgement of theirs.
- Claude — took my shit code and made it dramatically smaller and faster.
cargo build --release