Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,39 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Static key extraction from `bitwig.jar`**, via
`examples/extract_keys_from_jar.py`. All three keys — Dag, `nitro-image` and
`nitro-std` — are `byte[]` literals in the jar, built by bytecode
(`newarray byte` plus one `bastore` per element) rather than stored as
contiguous bytes, which is why searching the jars for them finds nothing in
any encoding. The script reads the arrays out of the bytecode and verifies
each candidate against your own installed archives before reporting it;
nothing is printed as hex unless you pass `--hex`. Every class is looked at,
but only the 281 of 31,476 that contain a `newarray byte` opcode are walked,
which no candidate can hide from: the pattern being matched starts with those
two bytes. Ships no keys. Verified on Bitwig 5.1.9 and 6.1, which carry
byte-identical values for all three: 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. Note that this supersedes the 0.1.1 removal
of `--from-jar`: a static route does exist, it was simply not a contiguous
byte string.

### Changed

- **`docs/KEY_EXTRACTION.md` and the README no longer say static recovery is
impossible.** That conclusion came from searching for each key as a
contiguous byte string, which does fail. The live-JVM controllers are
unchanged and stay documented as the live route.
- `nitro-decrypt-std`'s docstring no longer describes `nitro-std` as wrapped in
a runtime PRNG cipher that cannot be reproduced offline. It uses the same Dag
cipher as `nitro-image`, under a 96-byte key with a 192-byte IV. The command
itself is unchanged.
- Recorded that a 5.1.9 `nitro-image` decrypts correctly under this key but
does not decompile: the nitrobin container gained a field between 5.1.9 and
6.1. That is a format-version gap in the decompiler, not a key failure.

## [0.2.0] - 2026-08-12

### Added
Expand Down
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,12 @@ A few things that surfaced while reverse-engineering the format:
prove Bitwig's loader would accept a *modified* module. That is a separate,
live, untested question. See
[docs/NITRO_LOAD_MECHANISM.md](docs/NITRO_LOAD_MECHANISM.md).
- **Key extraction runs against your own install, not this repo.** Both keys are
materialized at runtime — neither appears anywhere in Bitwig's jars, in any
encoding — so there is no static or offline key recovery. Recovery goes
through a small controller extension this project bundles: you install it, add
it once in Bitwig, and it dumps the key for the CLI to pick up
(`nitro-extract-keys --install-controller`, then `--live`). That live step
loads inside your own licensed Bitwig and is yours to run and confirm; the Dag
key (for `0004` documents) is entered manually. See
- **Key extraction runs against your own install, not this repo.** All three
keys are static `byte[]` literals in `bitwig.jar`, built by bytecode rather
than stored as contiguous bytes, which is why a `grep` over the jars finds
nothing in any encoding. `examples/extract_keys_from_jar.py` reads them out
and verifies each against your own archives before reporting it. The bundled
controller extension still works and stays documented as the live route. See
[docs/KEY_EXTRACTION.md](docs/KEY_EXTRACTION.md).
- **Repacked-image loader acceptance is unproven.** Repacking the archive is
byte-exact offline, but whether Bitwig loads your repack is one restart-gated
Expand Down
134 changes: 106 additions & 28 deletions docs/KEY_EXTRACTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@ If you do not have a licensed Bitwig install, this toolchain can still parse,
serialize, edit, and pretty-print `.nitrobin` bytes you already have in
plaintext; it just cannot decrypt anything for you.

## The two keys
## The three keys

There are two independent keys, for two independent cipher surfaces.
There are three independent keys, for three independent cipher surfaces.

**Both keys are materialized at runtime.** Neither the nitro-image key nor the
Dag key is stored as static data. Neither appears in `bitwig.jar`, `libs.jar`,
or `lwjgl.jar` in any form — not raw, not hex, not base64 — nor in the native
binaries; this was checked directly. A plain disassembly or a `grep` over the
jars will not hand you either key. **Static jar recovery is impossible.** The
only proven way to recover a key is to read it out of a running Bitwig JVM,
which is what the bundled controller extension does (see
[Running nitro-extract-keys](#running-nitro-extract-keys)).
**All three are static `byte[]` literals in `bitwig.jar`, and a script in this
repository recovers them.** See
[Recovering the keys from the jar](#recovering-the-keys-from-the-jar). The
live-JVM controllers still work and are still documented below, but they are no
longer the only route.

Earlier revisions of this document said the opposite: that the keys are
materialized at runtime and static recovery is impossible. That conclusion came
from searching the jars for the key as a contiguous byte string, which does
fail — in every encoding. The arrays are built by *bytecode*: a `newarray byte`
followed by one `bastore` per element, so the key bytes are interleaved with
opcodes, one byte every four. No `grep` finds that. Reading the array out of
the bytecode finds all three.

### 1. The nitro-image key

Expand All @@ -40,12 +45,17 @@ examined). A sibling entry carries the *same* key value with `iv_size_uEK == 0`,
so select by the IV-size field (198), not by position in the chain — the order
is not guaranteed stable across releases.

**This key is not in the jar.** The transform *classes* are defined in
`bitwig.jar`, but their key bytes are populated at runtime; the 99-byte value
does not appear anywhere in `bitwig.jar`, `libs.jar`, or `lwjgl.jar`, in any
encoding (this was checked). You recover it the same way you recover the Dag
key: by reading the live transform objects out of a running JVM. There is no
static shortcut.
**This key is in the jar**, as a 99-byte array literal in the class that
declares the nitro transform chains (`com/bitwig/nitro/NitroFile`, and two
obfuscated classes that carry the same array). The chain reads

```java
new ya(new GNy[]{Krq.TUp(), new LQt(EwU, 0), new LQt(EwU, EwU.length * 2)}, 2)
```

so the `iv_size` of 198 is simply `99 * 2`, and the sibling entry with
`iv_size == 0` is the second `LQt` over the same array. Selecting by IV size
works because it is derived from the key length, not stored independently.

### 2. The Dag key

Expand All @@ -55,12 +65,30 @@ Dag key recovers the readable metadata section; the file body may sit behind a
further layer that this key does not open, so treat `0004` support as
metadata-level.

Where it lives: the Dag key is reached in the running JVM through the field
chain `ZKE.uEK -> BIa.Xzy` (obfuscated class and field names, which shift
between releases). It is **not** stored as static class data. It is materialized
into Java runtime objects that are initialized by native code, which means a
plain static disassembly will not hand you the bytes; you recover it from a
live object graph.
Where it lives: a 128-byte array literal in a class under
`com/bitwig/base/serial/file/`. The class name is obfuscated and shifts between
releases — `Tl3` on 6.1, `q2p` on 5.1.9 — but the package path is not
obfuscated, and it is the only long array literal under it. The method holding
it ends in `return new LQt(var1, 16)`: the Dag cipher with a 16-byte IV, which
is the IV length the `0004` container prefixes each section with.

It is also reachable from a live JVM through the field chain described in
earlier revisions, if you prefer that route.

### 3. The nitro-std key

Decrypts the stdlib *source* members inside `<install>/Library/nitro-std`.

Where it lives: a 96-byte array literal in the same class as the nitro-image
key, in the version-1 chain:

```java
new ya(new GNy[]{Krq.TUp(), new LQt(jaQ, jaQ.length * 2)}, 1)
```

Each member is stored as `[version][iv:192][ciphertext]` and decrypts with the
same Dag routine the other two surfaces use. This surface does **not** require
a live JVM: `nitro-decrypt-std` and its controller predate this finding.

## The cipher, for context

Expand All @@ -80,6 +108,50 @@ position (never on the data), the transform is its own inverse. That is why
routine. `dag_decrypt(data, key, iv)` in `bitwig_nitro.dag_cipher` implements
it; you provide `key` and `iv`.

## Recovering the keys from the jar

```bash
python examples/extract_keys_from_jar.py # report
python examples/extract_keys_from_jar.py --write # write keys.json
python examples/extract_keys_from_jar.py --install /path/to/Bitwig
```

The script scans every class in the jar for `byte[]` literals of 48 bytes or
more — the cipher factory rejects anything shorter with `Key too short` — and
then **verifies each candidate against your own installed archives** before
reporting it. A wrong key yields high-entropy noise; a right one yields content
with the structure its container promises. Candidates that verify against
nothing are reported as unidentified rather than guessed at.

There are very few candidates to begin with. A 6.1 jar holds five arrays of
48+ bytes across its 31,476 classes: the three keys, one in the Skia shader
filesystem, and one unrelated 257-byte table.

A note if you unpack the jar to look around: do not trust a directory listing
on a case-insensitive filesystem. Bitwig's obfuscated names differ only by
case — `Aa.class` and `aA.class` are two classes — so `unzip` on macOS or
Windows silently collapses them and leaves you with 17,290 of the 31,476. Read
the archive directly, as the script does.

Nothing is printed as hex unless you pass `--hex`.

**Verified on two builds.** Bitwig 5.1.9 and 6.1 carry byte-identical values
for all three keys, and all three decrypt correctly on both. Checked by
decrypting every member of each archive:

| Surface | Result |
| --- | --- |
| `nitro-image`, 6.1 | 517/517 members decompile with `decompile_nitrobin` |
| `nitro-std`, 6.1 | 121/121 members decrypt to valid UTF-8 Nitro source |
| `0004` documents | factory `.bwdevice` metadata sections decrypt and parse |

One caveat worth recording: a 5.1.9 `nitro-image` decrypts correctly under this
key but does **not** decompile, because the nitrobin container gained a field
between 5.1.9 and 6.1. That is a format-version gap in the decompiler, not a
key problem — the decrypted plaintext carries the member's own name in clear
and its entropy drops from 7.8 to 4.1. The script's check looks for the member
name rather than decompiling, so it does not conflate the two.

## keys.json

Once you have the two keys as hex strings, put them in a `keys.json`:
Expand Down Expand Up @@ -197,12 +269,18 @@ nitro-image entry. For the Dag key the live object is reached through the
`ZKE.uEK -> BIa.Xzy` field chain (obfuscated names shift between releases;
identify the classes by role, not by name, using the disassembly as a map).

What you will **not** find in the class files is either key value. On disk those
fields are empty/default; the bytes are staged in at runtime. Disassembly of the
audio engine binary shows the cipher (registered under the name `BIa`) is a
small object constructed by a thread-safe factory, with the key material staged
through runtime state rather than baked into the class file. That is why there
is no static route for either key, and why recovery goes through a live JVM.
The key values **are** in the class files, as the array literals described
above. The fields on the transform objects are empty on disk because the
constructor is handed the array; the array itself is a constant in the class
that declares the chain, not in the cipher class. Looking at the cipher class
alone is what makes the key look absent.

A shortcut for locating the cipher class itself, if you want to confirm the
routine by eye: its rotate-right-by-`(n & 7)` step compiles to a distinctive
`iushr` / `bipush 8` / `isub` / `ishl` / `ior` window, and exactly one class in
the jar contains it — `QMl` on 6.1, `pPi` on 5.1.9. From there, the class that
constructs it is the chain wrapper, and the class that constructs *that* holds
the key.

**Verify.**

Expand Down
Loading