Skip to content

feat(keys): recover all three keys statically from bitwig.jar - #1

Open
zezic wants to merge 2 commits into
blakebratcher:mainfrom
zezic:docs/static-key-extraction
Open

zezic wants to merge 2 commits into
blakebratcher:mainfrom
zezic:docs/static-key-extraction

Conversation

@zezic

@zezic zezic commented Sep 18, 2026

Copy link
Copy Markdown

The Dag, nitro-image and nitro-std keys are ordinary byte[] literals in bitwig.jar. They are built by bytecode -- newarray byte followed by one bastore per element -- so the bytes sit one every four, interleaved with opcodes, and never appear as a contiguous run. Searching the jars for them finds nothing in any encoding, which is what the docs concluded from; reading the arrays out of the bytecode finds all three. This supersedes the 0.1.1 removal of --from-jar: a static route does exist, it simply is not a contiguous byte string.

examples/extract_keys_from_jar.py scans every class for byte[] literals of 48+ bytes -- the cipher factory rejects anything shorter with "Key too short" -- and verifies each candidate against your own installed archives before reporting it. A 6.1 jar holds five such arrays across its 31,476 classes: the three keys, one in the Skia shader filesystem, and one unrelated table. Only the 281 classes that contain a newarray byte opcode are walked, which no candidate can hide from, because the pattern being matched starts with those two bytes; the whole scan takes about 0.8s. Nothing prints as hex without --hex, and no key material is committed.

One trap if you unpack the jar to check this by hand: Bitwig's obfuscated names differ only by case, so Aa.class and aA.class are two classes, and unzip on a case-insensitive filesystem silently collapses them to 17,290 of the 31,476.
The script reads the archive directly and is not affected.

Verified on 5.1.9 and 6.1, which carry byte-identical values for all three keys: 517/517 nitro-image members decompile on 6.1, 121/121 nitro-std members decrypt to valid Nitro source, and factory 0004 document metadata parses.

A 5.1.9 nitro-image decrypts correctly but does not decompile, because the nitrobin container gained a field between the two builds. The image check therefore looks for the member's own name in the plaintext rather than decompiling, so a format gap is not read as a bad key.

nitro-std is the same Dag cipher as nitro-image under a 96-byte key with a 192-byte IV, not a runtime PRNG, so it decrypts offline as well. The live-JVM controllers are unchanged and stay documented as the live route; only the claim that they are the only route is removed.

The Dag, nitro-image and nitro-std keys are ordinary byte[] literals in
bitwig.jar. They are built by bytecode -- newarray byte followed by one
bastore per element -- so the bytes sit one every four, interleaved with
opcodes, and never appear as a contiguous run. Searching the jars for
them finds nothing in any encoding, which is what the docs concluded
from; reading the arrays out of the bytecode finds all three.

examples/extract_keys_from_jar.py scans every class for byte[] literals
of 48+ bytes -- the cipher factory rejects anything shorter with "Key
too short" -- and verifies each candidate against your own installed
archives before reporting it. A 6.1 jar holds five such arrays across
~17,000 classes: the three keys, one in the Skia shader filesystem, and
one unrelated table. Nothing prints as hex without --hex, and no key
material is committed.

Verified on 5.1.9 and 6.1, which carry byte-identical values for all
three keys: 517/517 nitro-image members decompile on 6.1, 121/121
nitro-std members decrypt to valid Nitro source, and factory 0004
document metadata parses.

A 5.1.9 nitro-image decrypts correctly but does not decompile, because
the nitrobin container gained a field between the two builds. The
image check therefore looks for the member's own name in the plaintext
rather than decompiling, so a format gap is not read as a bad key.

nitro-std is the same Dag cipher as nitro-image under a 96-byte key with
a 192-byte IV, not a runtime PRNG, so it decrypts offline as well. The
live-JVM controllers are unchanged and stay documented as the live
route; only the claim that they are the only route is removed.
Scanning the jar took 3.1s, nearly all of it a Python loop over the bytes
of all 31,476 classes. A class with no `newarray byte` in it cannot hold
an array literal, and that check is a substring search rather than a
loop, so it runs first: 281 classes walked instead of 31,476, and the
whole scan drops to 0.8s.

No candidate can hide behind it. The pattern the walk looks for begins
with those two bytes by definition, so the filter has no false negatives.
Output is byte-identical on 5.1.9 and 6.1: the same five arrays, the same
three keys, the same two unidentified.

Every class is still looked at rather than narrowing to the packages the
keys occupy today. The three sit in three different places, and a package
filter would quietly stop finding them the release one moves.

Also corrects the class count. The earlier figure of ~17,000 came from
unpacking the jar to disk on a case-insensitive filesystem, which
silently collapses Bitwig's obfuscated names: `Aa.class` and `aA.class`
are two classes and become one file. The jar holds 31,476; unzip on macOS
leaves 17,290 of them. The survey itself was read from the archive rather
than from the unpacked tree, so its result was never affected - five
arrays is still five arrays - but the number quoted beside it was wrong,
and a reader unpacking the jar to check would have hit the same trap.
That is now written down where it bites.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant