mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-10 13:33:24 -05:00
fix: decrypt otp.bin with the standard library
This commit is contained in:
1 parent
fdc7fc8326
commit
d257ecc6ab
2 files changed
+139
-47
No files matched your search
+85
-47
@@ -6,8 +6,7 @@ Reproduces the exact verification logic from Azahar/Citra source code:
|
|||||||
- movable.sed: magic check + RSA on embedded LFCS
|
- movable.sed: magic check + RSA on embedded LFCS
|
||||||
- otp.bin: AES-128-CBC decrypt + magic + SHA256 hash
|
- otp.bin: AES-128-CBC decrypt + magic + SHA256 hash
|
||||||
|
|
||||||
RSA verification is pure Python (no dependencies).
|
RSA verification and AES decryption are pure Python (no dependencies).
|
||||||
AES decryption requires 'cryptography' library or falls back to openssl CLI.
|
|
||||||
|
|
||||||
Source refs:
|
Source refs:
|
||||||
Azahar src/core/hw/unique_data.cpp
|
Azahar src/core/hw/unique_data.cpp
|
||||||
@@ -19,7 +18,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import struct
|
import struct
|
||||||
import subprocess
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -137,54 +135,94 @@ def _rsa_verify_pkcs1v15_sha256(
|
|||||||
return em == expected_em
|
return em == expected_em
|
||||||
|
|
||||||
|
|
||||||
# AES-128-CBC decryption (with fallback)
|
# AES-128-CBC decryption, standard library only. The build promises stdlib +
|
||||||
|
# pyyaml; a contributor without cryptography, pycryptodome or openssl on PATH
|
||||||
|
# got a RuntimeError from verify.py on otp.bin. 256 bytes need no speed.
|
||||||
|
|
||||||
|
|
||||||
|
def _xtime(value: int) -> int:
|
||||||
|
value <<= 1
|
||||||
|
return (value ^ 0x11B) & 0xFF if value & 0x100 else value
|
||||||
|
|
||||||
|
|
||||||
|
def _gmul(a: int, b: int) -> int:
|
||||||
|
product = 0
|
||||||
|
while b:
|
||||||
|
if b & 1:
|
||||||
|
product ^= a
|
||||||
|
a = _xtime(a)
|
||||||
|
b >>= 1
|
||||||
|
return product
|
||||||
|
|
||||||
|
|
||||||
|
def _build_sbox() -> tuple[list[int], list[int]]:
|
||||||
|
sbox = [0] * 256
|
||||||
|
inverse = [0] * 256
|
||||||
|
for value in range(256):
|
||||||
|
# Multiplicative inverse in GF(2^8), then the affine transform.
|
||||||
|
inv = 0 if value == 0 else next(
|
||||||
|
c for c in range(1, 256) if _gmul(value, c) == 1
|
||||||
|
)
|
||||||
|
out = inv
|
||||||
|
for shift in range(1, 5):
|
||||||
|
out ^= ((inv << shift) | (inv >> (8 - shift))) & 0xFF
|
||||||
|
out ^= 0x63
|
||||||
|
sbox[value] = out
|
||||||
|
inverse[out] = value
|
||||||
|
return sbox, inverse
|
||||||
|
|
||||||
|
|
||||||
|
_SBOX, _INV_SBOX = _build_sbox()
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_key(key: bytes) -> list[list[int]]:
|
||||||
|
"""The eleven round keys of AES-128, each sixteen bytes."""
|
||||||
|
words = [list(key[i : i + 4]) for i in range(0, 16, 4)]
|
||||||
|
rcon = 1
|
||||||
|
for i in range(4, 44):
|
||||||
|
word = list(words[i - 1])
|
||||||
|
if i % 4 == 0:
|
||||||
|
word = [_SBOX[b] for b in word[1:] + word[:1]]
|
||||||
|
word[0] ^= rcon
|
||||||
|
rcon = _xtime(rcon)
|
||||||
|
words.append([a ^ b for a, b in zip(words[i - 4], word)])
|
||||||
|
return [sum(words[r * 4 : r * 4 + 4], []) for r in range(11)]
|
||||||
|
|
||||||
|
|
||||||
|
def _decrypt_block(block: bytes, round_keys: list[list[int]]) -> bytes:
|
||||||
|
state = [b ^ k for b, k in zip(block, round_keys[10])]
|
||||||
|
for rnd in range(9, -1, -1):
|
||||||
|
# Inverse ShiftRows on a column-major state, then inverse SubBytes.
|
||||||
|
state = [state[(i + 4 * (i % 4) * 3) % 16] for i in range(16)]
|
||||||
|
state = [_INV_SBOX[b] for b in state]
|
||||||
|
state = [b ^ k for b, k in zip(state, round_keys[rnd])]
|
||||||
|
if rnd:
|
||||||
|
mixed = []
|
||||||
|
for c in range(4):
|
||||||
|
a0, a1, a2, a3 = state[c * 4 : c * 4 + 4]
|
||||||
|
mixed += [
|
||||||
|
_gmul(a0, 14) ^ _gmul(a1, 11) ^ _gmul(a2, 13) ^ _gmul(a3, 9),
|
||||||
|
_gmul(a0, 9) ^ _gmul(a1, 14) ^ _gmul(a2, 11) ^ _gmul(a3, 13),
|
||||||
|
_gmul(a0, 13) ^ _gmul(a1, 9) ^ _gmul(a2, 14) ^ _gmul(a3, 11),
|
||||||
|
_gmul(a0, 11) ^ _gmul(a1, 13) ^ _gmul(a2, 9) ^ _gmul(a3, 14),
|
||||||
|
]
|
||||||
|
state = mixed
|
||||||
|
return bytes(state)
|
||||||
|
|
||||||
|
|
||||||
def _aes_128_cbc_decrypt(data: bytes, key: bytes, iv: bytes) -> bytes:
|
def _aes_128_cbc_decrypt(data: bytes, key: bytes, iv: bytes) -> bytes:
|
||||||
"""Decrypt AES-128-CBC without padding."""
|
"""Decrypt AES-128-CBC without padding."""
|
||||||
# Try cryptography library first
|
if len(key) != 16 or len(iv) != 16 or len(data) % 16:
|
||||||
try:
|
raise ValueError("AES-128-CBC needs a 16-byte key and IV and whole blocks")
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
round_keys = _expand_key(key)
|
||||||
|
out = bytearray()
|
||||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
previous = iv
|
||||||
decryptor = cipher.decryptor()
|
for offset in range(0, len(data), 16):
|
||||||
return decryptor.update(data) + decryptor.finalize()
|
block = data[offset : offset + 16]
|
||||||
except ImportError:
|
plain = _decrypt_block(block, round_keys)
|
||||||
pass
|
out += bytes(a ^ b for a, b in zip(plain, previous))
|
||||||
|
previous = block
|
||||||
# Try pycryptodome
|
return bytes(out)
|
||||||
try:
|
|
||||||
from Crypto.Cipher import AES # type: ignore[import-untyped]
|
|
||||||
|
|
||||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
|
||||||
return cipher.decrypt(data)
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Fallback to openssl CLI
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
[
|
|
||||||
"openssl",
|
|
||||||
"enc",
|
|
||||||
"-aes-128-cbc",
|
|
||||||
"-d",
|
|
||||||
"-K",
|
|
||||||
key.hex(),
|
|
||||||
"-iv",
|
|
||||||
iv.hex(),
|
|
||||||
"-nopad",
|
|
||||||
],
|
|
||||||
input=data,
|
|
||||||
capture_output=True,
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
return result.stdout
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
raise RuntimeError(
|
|
||||||
"AES decryption requires 'cryptography' or 'pycryptodome' library, "
|
|
||||||
"or 'openssl' CLI tool"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# File verification functions
|
# File verification functions
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""otp.bin decrypts with the standard library alone.
|
||||||
|
|
||||||
|
The build promises stdlib + pyyaml. AES needed cryptography, pycryptodome
|
||||||
|
or an openssl binary, and verify.py raised RuntimeError on otp.bin for a
|
||||||
|
contributor with none of them.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
||||||
|
|
||||||
|
import crypto_verify # noqa: E402
|
||||||
|
|
||||||
|
STDLIB_OR_LOCAL = {"__future__", "hashlib", "struct", "collections", "pathlib", "sect233r1"}
|
||||||
|
|
||||||
|
|
||||||
|
class StdlibAes(unittest.TestCase):
|
||||||
|
def test_fips_197_vector(self):
|
||||||
|
key = bytes.fromhex("000102030405060708090a0b0c0d0e0f")
|
||||||
|
cipher = bytes.fromhex("69c4e0d86a7b0430d8cdb78070b4c55a")
|
||||||
|
plain = crypto_verify._decrypt_block(cipher, crypto_verify._expand_key(key))
|
||||||
|
self.assertEqual(plain.hex(), "00112233445566778899aabbccddeeff")
|
||||||
|
|
||||||
|
def test_cbc_chains_blocks(self):
|
||||||
|
# NIST SP 800-38A F.2.2, CBC-AES128 decrypt, first two blocks.
|
||||||
|
key = bytes.fromhex("2b7e151628aed2a6abf7158809cf4f3c")
|
||||||
|
iv = bytes.fromhex("000102030405060708090a0b0c0d0e0f")
|
||||||
|
cipher = bytes.fromhex(
|
||||||
|
"7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b2"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
crypto_verify._aes_128_cbc_decrypt(cipher, key, iv).hex(),
|
||||||
|
"6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_third_party_import(self):
|
||||||
|
tree = ast.parse((REPO_ROOT / "scripts" / "crypto_verify.py").read_text())
|
||||||
|
modules = {
|
||||||
|
(node.module if isinstance(node, ast.ImportFrom) else alias.name).split(".")[0]
|
||||||
|
for node in ast.walk(tree)
|
||||||
|
if isinstance(node, (ast.Import, ast.ImportFrom))
|
||||||
|
for alias in node.names
|
||||||
|
}
|
||||||
|
self.assertLessEqual(modules, STDLIB_OR_LOCAL)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user