Client-side SCP80 decryption: pure-JS 3DES/AES with user-supplied keys
Adds optional decryption of ciphered secured packets, entirely in the browser — the key never leaves the frontend. - decode.py: _decode_secured_packet exposes 'cipher_block' (the contiguous on-wire ciphered octets CNTR..end, ready for CBC) and raw kic/kid bytes for algorithm selection. Additive only. - index.html: pure-JS DES/3DES core (FIPS 46-3 bit-array tables, parity ignored) + CBC; AES-CBC via native SubtleCrypto; both with zero IV per TS 102 225. decryptSecured() picks the algorithm from the KIc nibble (02 AES-CBC, 05 3DES-2K, 09 3DES-3K, 01 DES) with key-length validation. - UI: ciphered packets show a key-hex input + Decrypt strip below the detail table; keys are memoized per key-set for the session in an in-memory OTA_KEYS map — never sent to the server, never persisted. - Decrypted results cached by cipher_block (survive live-polling re-renders); plaintext rows rendered as CNTR/PCNTR/RC/Secured data (decrypted) alongside the ciphertext; wrong-key guard via the decrypted padding counter (PCNTR > remaining → "invalid padding"). - 3DES implementation validated byte-identical against openssl des-ede3-cbc (3-key, 2-key ×3 random vectors, single-key). No CC verification (out of scope). Tests: +2 (cipher_block contiguity, AES registry guard) — 350 total. Version 1.19.0, SW cache simtrace-analyser-v27.
This commit is contained in:
1 parent
840e233baf
commit
d21997764f
7 files changed
+253
-5
No files matched your search
@@ -2127,6 +2127,28 @@ class TestSecuredCiphering(unittest.TestCase):
|
||||
r = decode_message(raw, prev=prev)
|
||||
self.assertTrue(r['response']['ciphered'])
|
||||
|
||||
def test_cipher_block_contiguous(self):
|
||||
# cipher_block = the on-wire ciphered octets (CNTR..end) and equals
|
||||
# the reassembled cntr+pcntr+rc+data — what a client-side decryptor
|
||||
# feeds to 3DES/AES-CBC.
|
||||
from simtrace2_pysniff.server.decode import _decode_secured_packet
|
||||
sec = bytes.fromhex('00381516011515b000011830a44e4191ad45d0ec'
|
||||
'2a291315986468a539d248fcdce7952c31cd635baec'
|
||||
'7e93a5d567b59f6283316c0dad8603bf3'.replace(' ', ''))
|
||||
r = _decode_secured_packet(sec)
|
||||
self.assertEqual(len(r['cipher_block']) // 2, 48)
|
||||
self.assertEqual(r['cipher_block'],
|
||||
(r['cntr'] + f'{r["pcntr"]:02X}' + r['rc_cc_ds'] + r['data']))
|
||||
self.assertEqual(r['kic_raw'], 0x15)
|
||||
self.assertEqual(r['kid_raw'], 0x15)
|
||||
|
||||
def test_aes_algorithm_recognized(self):
|
||||
# AES must stay recognized for ciphering (KIc nibble 2) and for the
|
||||
# CC (KID nibble 2 = AES-CMAC) — the client-side decryptor relies on it.
|
||||
from simtrace2_pysniff.server.decode import _KIC_ALGO, _KID_CC_ALGO
|
||||
self.assertEqual(_KIC_ALGO[0x02], 'AES-CBC')
|
||||
self.assertEqual(_KID_CC_ALGO[0x02], 'AES-CMAC')
|
||||
|
||||
|
||||
class TestSpiBits(unittest.TestCase):
|
||||
"""TS 102 225 §5.1.1: SPI1 b3 = ciphering, SPI2 b5 = PoR ciphering."""
|
||||
|
||||
Reference in new issue
Block a user