diff --git a/docs/scp81-findings.md b/docs/scp81-findings.md index c2f5772..f78134b 100644 --- a/docs/scp81-findings.md +++ b/docs/scp81-findings.md @@ -213,6 +213,22 @@ lengths are BER-encoded (`_ber_len_bytes`); the same rule as the BIP channel data TLV fix earlier the same day. Tests cover the 245-byte LOAD body, the short-form case and the definite variant. +## RESOLVED 2026-09-16d: RAM install - LOAD blocks were overlapping copies + +**Root cause:** the LOAD block slicer indexed the load file TLV with the +*block number* (`loadfile_tlv[i * 2:(i + 240) * 2] for i in range(blocks)`) +instead of a *character offset*, so every block after the first was a +1-byte-shifted copy of its predecessor. On the wire the cap header repeated +every 239 bytes. The card accepted the first three blocks and failed block 4 +with `SW 6400` (execution error), then refused the rest (`6985`) and the +final INSTALL answered `6A88`. The same slicing lived in the SCP80 +/api/ram-install path (the helper was extracted from it), so multi-block caps +could never install there either. + +**Fix:** consecutive chunks at char offsets +(`range(0, len(tlv), 240 * 2)`), with a reassembly test that pins the joined +blocks to the C4 TLV byte for byte. + ## Next tests / work 1. **UI:** group the per-page R-APDUs under their logical command in the diff --git a/frontend/index.html b/frontend/index.html index cd6633d..dd430f1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -18,7 +18,7 @@
-

OTAMan SIM OTA with a Human Face v2.1.11

+

OTAMan SIM OTA with a Human Face v2.1.12

diff --git a/frontend/sw.js b/frontend/sw.js index 587101b..2ca4573 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v148'; +const CACHE = 'otaman-v149'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index 5433bef..3520a70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-otaman-server" -version = "2.1.11" +version = "2.1.12" description = "HTTP REST server wrapping pysim for the OTAMan PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_otaman_server/server.py b/pysim_otaman_server/server.py index 2baae70..bcf25b5 100644 --- a/pysim_otaman_server/server.py +++ b/pysim_otaman_server/server.py @@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter from osmocom.tlv import BER_TLV_IE -VERSION = '2.1.11' +VERSION = '2.1.12' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE @@ -414,9 +414,12 @@ def _cap_apdu_sequence(loadfile_aid, module_aid, loadfile_data, sd_aid='', ifl_data = _lv(loadfile_aid) + _lv(sd) + '00' + '00' + '00' apdus = ['80E60200%02X%s00' % (len(ifl_data) // 2, ifl_data)] loadfile_tlv = 'C4' + _ber_len(len(loadfile_data) // 2) + loadfile_data - total_bytes = len(loadfile_tlv) // 2 - blocks = [loadfile_tlv[i * 2:(i + block_size) * 2] - for i in range(0, (total_bytes + block_size - 1) // block_size)] + # Split the TLV into consecutive 240-byte blocks (char offsets, 2 per + # byte). The earlier form indexed with the block number ('i * 2'), which + # produced overlapping 1-byte-shifted copies - the card failed mid-load + # with SW 6400 (live 2026-09-16). + blocks = [loadfile_tlv[off:off + block_size * 2] + for off in range(0, len(loadfile_tlv), block_size * 2)] for i, block in enumerate(blocks): p1 = 0x80 if i == len(blocks) - 1 else 0x00 apdus.append('80E8%02X%02X%02X%s00' % (p1, i % 256, len(block) // 2, block)) diff --git a/tests/test_ota_helpers.py b/tests/test_ota_helpers.py index 2304477..d653d34 100644 --- a/tests/test_ota_helpers.py +++ b/tests/test_ota_helpers.py @@ -879,10 +879,20 @@ class CapApduSequenceTest(unittest.TestCase): self.assertIn('06A00000010001' + '05A000000100' + '05A000000100' + '0100', seq[2]) def test_load_blocks_split_and_counter(self): - from pysim_otaman_server.server import _cap_apdu_sequence - data = 'AB' * 700 # 700 bytes -> C4 TLV 703 -> 3 x 240-byte blocks + from pysim_otaman_server.server import _cap_apdu_sequence, _ber_len as _ber_len_lower + data = ''.join('%02X' % (i % 256) for i in range(700)) seq = _cap_apdu_sequence('A00000010001', 'A000000100', data) self.assertEqual(len(seq), 5) # INSTALL + 3 LOAD + INSTALL self.assertEqual(seq[1][:8], '80E80000') self.assertEqual(seq[2][:8], '80E80001') self.assertEqual(seq[3][:8], '80E88002') # last block: P1=0x80 + # The blocks are consecutive chunks and reassemble the load file TLV + # byte-for-byte (a shifted/overlapping split fails the card mid-load). + def payload(apdu): + lc = int(apdu[8:10], 16) + return apdu[10:10 + lc * 2] + joined = payload(seq[1]) + payload(seq[2]) + payload(seq[3]) + self.assertTrue(joined.startswith('C482')) + expected = 'C4' + _ber_len_lower(700) + data # 700 = 0x2BC + self.assertEqual(joined.upper(), expected.upper()) + self.assertEqual(int(seq[3][8:10], 16), len(expected) // 2 - 480)