Add a utility for encrypting/decrypting NVRAM files.

This commit is contained in:
Jennifer Taylor 2025-08-12 21:57:05 +00:00
parent 98d245f824
commit 61ed4d39cf
4 changed files with 86 additions and 9 deletions

View File

@ -41,7 +41,7 @@ class EAmuseProtocol:
self.last_text_encoding: Optional[str] = None
self.last_packet_encoding: Optional[int] = None
def _rc4_crypt(self, data: bytes, key: bytes) -> bytes:
def rc4_crypt(self, data: bytes, key: bytes) -> bytes:
"""
Given a data blob and a key blob, perform RC4 encryption/decryption.
@ -58,7 +58,10 @@ class EAmuseProtocol:
# KSA Phase
for i in range(256):
j = (j + S[i] + key[i % len(key)]) & 0xFF
if key:
j = (j + S[i] + key[i % len(key)]) & 0xFF
else:
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
# PRGA Phase
@ -97,7 +100,7 @@ class EAmuseProtocol:
if key:
# This is an encrypted old-style packet
return self._rc4_crypt(data, key)
return self.rc4_crypt(data, key)
# No encryption
return data

View File

@ -12,10 +12,10 @@ class TestRC4Cipher(unittest.TestCase):
encrypted = b"\x04]Q\x11\x0cw\x7fO\xfa\x03\xa3\xdf\xb6\x02\xb7d\x9f\x13U\x19\xc9-j\x96\x15yl\x98\xee_<\xfa\x9b\x8f\xbe}\xf4\x05l5\x0e\xd6"
proto = EAmuseProtocol()
cyphertext = proto._rc4_crypt(data, key)
cyphertext = proto.rc4_crypt(data, key)
self.assertEqual(encrypted, cyphertext)
plaintext = proto._rc4_crypt(cyphertext, key)
plaintext = proto.rc4_crypt(cyphertext, key)
self.assertEqual(data, plaintext)
def test_small_data_random(self) -> None:
@ -23,10 +23,10 @@ class TestRC4Cipher(unittest.TestCase):
key = bytes([random.randint(0, 255) for _ in range(16)])
proto = EAmuseProtocol()
cyphertext = proto._rc4_crypt(data, key)
cyphertext = proto.rc4_crypt(data, key)
self.assertNotEqual(data, cyphertext)
plaintext = proto._rc4_crypt(cyphertext, key)
plaintext = proto.rc4_crypt(cyphertext, key)
self.assertEqual(data, plaintext)
def test_large_data_random(self) -> None:
@ -34,8 +34,8 @@ class TestRC4Cipher(unittest.TestCase):
key = bytes([random.randint(0, 255) for _ in range(16)])
proto = EAmuseProtocol()
cyphertext = proto._rc4_crypt(data, key)
cyphertext = proto.rc4_crypt(data, key)
self.assertNotEqual(data, cyphertext)
plaintext = proto._rc4_crypt(cyphertext, key)
plaintext = proto.rc4_crypt(cyphertext, key)
self.assertEqual(data, plaintext)

62
bemani/utils/nvram.py Normal file
View File

@ -0,0 +1,62 @@
import argparse
import os
from bemani.protocol import EAmuseProtocol
def main() -> None:
parser = argparse.ArgumentParser(description="A utility to encrypt or decrypt NVRAM files.")
parser.add_argument(
"file",
help="File to encrypt or decrypt.",
type=str,
)
parser.add_argument(
"-o",
"--output",
default=None,
type=str,
help="Output to a different file instead of overwriting original.",
)
parser.add_argument(
"--strip-padding",
action="store_true",
default=False,
help="Strip null padding on decryption.",
)
parser.add_argument(
"--add-padding",
action="store_true",
default=False,
help="Add null padding on encryption.",
)
args = parser.parse_args()
with open(args.file, "rb") as bfp:
data = bfp.read()
if args.add_padding:
off_by = len(data) % 768
if off_by != 0:
data = data + b"\x00" * (768 - off_by)
assert (len(data) % 768) == 0
chunks = [data[x:x + 768] for x in range(0, len(data), 768)]
outputs = []
proto = EAmuseProtocol()
for chunk in chunks:
outputs.append(proto.rc4_crypt(chunk, b""))
output = b"".join(outputs)
if args.strip_padding:
while output[-1] == 0:
output = output[:-1]
with open(args.output or args.file, "wb") as bfp:
bfp.write(output)
if __name__ == "__main__":
main()

12
nvram Executable file
View File

@ -0,0 +1,12 @@
#! /usr/bin/env python3
if __name__ == "__main__":
import os
path = os.path.abspath(os.path.dirname(__file__))
name = os.path.basename(__file__)
import sys
sys.path.append(path)
os.environ["SQLALCHEMY_SILENCE_UBER_WARNING"] = "1"
import runpy
runpy.run_module(f"bemani.utils.{name}", run_name="__main__")