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
da0c843bdd
commit
ef16ed2af2
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
|
||||
- otp.bin: AES-128-CBC decrypt + magic + SHA256 hash
|
||||
|
||||
RSA verification is pure Python (no dependencies).
|
||||
AES decryption requires 'cryptography' library or falls back to openssl CLI.
|
||||
RSA verification and AES decryption are pure Python (no dependencies).
|
||||
|
||||
Source refs:
|
||||
Azahar src/core/hw/unique_data.cpp
|
||||
@@ -19,7 +18,6 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import struct
|
||||
import subprocess
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
@@ -137,54 +135,94 @@ def _rsa_verify_pkcs1v15_sha256(
|
||||
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:
|
||||
"""Decrypt AES-128-CBC without padding."""
|
||||
# Try cryptography library first
|
||||
try:
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
|
||||
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
|
||||
decryptor = cipher.decryptor()
|
||||
return decryptor.update(data) + decryptor.finalize()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Try pycryptodome
|
||||
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"
|
||||
)
|
||||
if len(key) != 16 or len(iv) != 16 or len(data) % 16:
|
||||
raise ValueError("AES-128-CBC needs a 16-byte key and IV and whole blocks")
|
||||
round_keys = _expand_key(key)
|
||||
out = bytearray()
|
||||
previous = iv
|
||||
for offset in range(0, len(data), 16):
|
||||
block = data[offset : offset + 16]
|
||||
plain = _decrypt_block(block, round_keys)
|
||||
out += bytes(a ^ b for a, b in zip(plain, previous))
|
||||
previous = block
|
||||
return bytes(out)
|
||||
|
||||
|
||||
# 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