From 61ed4d39cff9f3cd2b51b95bfd4ee01ceae5fa90 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Tue, 12 Aug 2025 21:57:05 +0000 Subject: [PATCH] Add a utility for encrypting/decrypting NVRAM files. --- bemani/protocol/protocol.py | 9 ++++-- bemani/tests/test_RC4.py | 12 +++---- bemani/utils/nvram.py | 62 +++++++++++++++++++++++++++++++++++++ nvram | 12 +++++++ 4 files changed, 86 insertions(+), 9 deletions(-) create mode 100644 bemani/utils/nvram.py create mode 100755 nvram diff --git a/bemani/protocol/protocol.py b/bemani/protocol/protocol.py index 8d6ec10..4cbced8 100644 --- a/bemani/protocol/protocol.py +++ b/bemani/protocol/protocol.py @@ -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 diff --git a/bemani/tests/test_RC4.py b/bemani/tests/test_RC4.py index 53262ea..65a6ae4 100644 --- a/bemani/tests/test_RC4.py +++ b/bemani/tests/test_RC4.py @@ -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) diff --git a/bemani/utils/nvram.py b/bemani/utils/nvram.py new file mode 100644 index 0000000..fac5191 --- /dev/null +++ b/bemani/utils/nvram.py @@ -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() diff --git a/nvram b/nvram new file mode 100755 index 0000000..49ddf3a --- /dev/null +++ b/nvram @@ -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__")