Rockchip RKNN Model Encryption Is Recoverable Without a Key
Rockchip's RKNN model encryption XORs the model with a rand() keystream seeded by time(). Any encrypted model can be decrypted offline without a key. Rockchip has confirmed the issue.
TL;DR: Rockchip's RKNN-Toolkit2 has a "model encryption" feature for protecting the neural-network weights that run on its NPUs. It uses no key. The bundled tool XORs the model with a keystream from the C library's rand(), seeded with the current time, and the crypt_level setting only changes the length of that keystream. I reverse-engineered the scheme from the public SDK and wrote a decryptor that recovers encrypted models byte-for-byte without any secret, tested against Rockchip's own tool on a 2.3 MB production model. Rockchip confirmed the issue in writing in July. I've requested a CVE and will add the ID here once it's assigned.
What Rockchip ships
RKNN-Toolkit2 exposes export_encrypted_rknn_model(input_model, output_model, crypt_level), supported on the RK3562, RK3566, RK3568, RK3576, RK3588 and RV1126B. According to the documentation, it encrypts the whole model so that "third-party tools such as Netron will not be able to view the network structure and obtain weights," and crypt_level 1, 2 or 3 trades security against decryption speed. The User Guide also says:
"Password protection will be considered in the future."
There is no password or key today.
Reverse-engineering the scheme
The Python API calls a bundled native helper, rknn_crypt_tool, which sits in the toolkit wheel under rknn/3rdparty/platform-tools/rknn_crypt/. It's a 31 KB stripped x86-64 ELF. Its dynamic section and symbol table show the important part:
$ readelf -d rknn_crypt_tool | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
$ nm -D rknn_crypt_tool | egrep 'srand|rand|time|EVP_|AES|crypt'
U rand@GLIBC_2.2.5
U srand@GLIBC_2.2.5
U time@GLIBC_2.2.5
The only library it links is libc, and the only relevant imports are srand, rand and time. There is no cryptographic library in the binary at all.
The disassembly matches:
401349: call time@plt
401350: call srand@plt ; srand(time(NULL))
...
40136c: call rand@plt
40137a: imul $0xffffffff80808081,%rdx,%rdx
; compiler idiom for x % 255
401398: mov %al,-0x1(%r13) ; keystream[j] = rand() % 255
...
4013d4: xor (%rbx,%rcx,1),%al ; plaintext[i] XOR keystream[i % keylen]
4013d7: mov %al,0x120(%rsi,%rcx,1)
; ciphertext written after a 0x120 header
For every level, the algorithm is:
srand(time(NULL)), so the seed is the wall-clock time at the moment of encryption.keystream[j] = rand() % 255.ciphertext[i] = plaintext[i] XOR keystream[i mod keylen].
crypt_level picks the key length from a table in the binary: 1120, 2240 or 4480 bytes for levels 1, 2 and 3. A higher level only means a longer XOR key.
The output file starts with the 8-byte magic CYPTRKNN, followed by a header with the version, level and payload length, then a key-delivery block, then the XOR ciphertext of the model at offset 0x120. When I encrypted the same input twice, 229 seconds apart, the seeds were 1782153421 and 1782153650. The seed follows the wall clock, as the disassembly suggests.
Recovering the model
glibc's rand() is deterministic and its algorithm is public, so recovery needs no secret. A decrypted RKNN model always starts with the bytes RKNN, which gives a cheap test for a correct guess: try each candidate timestamp, decrypt, and look for RKNN. Starting from a rough date, such as a firmware build date or a file's timestamp, the search takes seconds. You don't strictly need the seed either. The key repeats every 4480 bytes or less, so standard repeating-key XOR analysis also works on a real model.
I reimplemented glibc's PRNG, checked it against the known srand(1) output sequence, and wrote a decryptor of about 120 lines. I tested it against models encrypted with Rockchip's own rknn_crypt_tool 2.3.2:
input level recovered identical to original
synthetic model 1 6,004 B yes
synthetic model 2 6,004 B yes
synthetic model 3 6,004 B yes
production model (2.3 MB) 1 & 3 2,311,562 B yes
There's no per-device or per-product key. The scheme is the same everywhere, so one recovery method works for every model encrypted with this feature on every supported chip.
Who's affected
This affects any product that relies on RKNN model encryption to keep its on-device model confidential. Anyone who gets hold of the encrypted file, whether from device flash, a firmware update image or an app package, can recover the original model. If you ship models on Rockchip NPUs and use this feature, treat those models as unprotected.
I tested RKNN-Toolkit2 2.3.2. The matching decryption is in the RKNPU2 runtime, librknnrt.so. The first-generation rknn-toolkit exposes the same interface under the name export_encrypt_rknn_model and is probably affected too, but I haven't tested it. No fix is available yet.
What would fix it
Protecting a model on the device needs:
- A key bound to the device, derived from OTP, eFuse or a secure element, so the model can't be decrypted elsewhere or copied to another device.
- Decryption inside the secure boundary, in the TEE or a secure NPU path, so plaintext weights never sit in normal DRAM.
- An authenticated cipher such as AES-GCM or ChaCha20-Poly1305, with a real key.
- Anti-debug, anti-dump and attestation, so the running model can't easily be observed.
Disclosure timeline
- 2026-06-23: Found the issue.
- 2026-06-24: Reported to CERT/CC (VRF#26-06-BVLFM). CERT/CC asked me to contact the vendor directly first.
- 2026-06-27: Reported to Rockchip.
- 2026-06-29: Sent the full technical report to Rockchip PSIRT, encrypted with their PGP key.
- 2026-07-01: Rockchip PSIRT acknowledged the report.
- 2026-07-14: Rockchip PSIRT confirmed the vulnerability and said it would redesign the scheme.
- 2026-07-17: Rockchip proposed public disclosure on 2026-09-30 and said it would deliver a solution to its customers before then. I agreed.
- 2026-09-30: The agreed date passed. I haven't heard from Rockchip since 2026-07-17.
- 2026-10-09: Requested a CVE and published this post.
This post doesn't include the decryptor's source code or any recovered model.