v3.28.2: the outfile ADM encoding fix

The ADM column carries the key's hex value (the length varies, 4-16 hex
digits) - the form the preset store and /api/verify-adm use (pySim pads
it with 'f' to the 8 CHV bytes).  The parser treated it as an ASCII
string hex-encoded and either mangled it (3632333239463337 -> 62329F37,
a wrong ADM that would fail VERIFY and consume an attempt) or dropped it
(CFA3DD9C6B7F0F8C - the live report with AM0027_alfa_dsa.out).

- The ADM is stored as-is: non-hex stripped, uppercased, [0-9A-F]{4,16};
  a non-hex or too-short value is dropped with "ADM is not 4-16 hex
  digits".
- _ascii_from_hex() is gone (its only user was the ADM column).
- Docs: help EN/RU, api.md and the repo AGENTS now say "the key's hex
  value, 4-16 hex digits (the length varies)".

Note: presets imported from a BAP-style file before this fix hold the
mangled ADM - re-import or correct them.

Tests: the BAP fixture (the hex as-is), the AM0027 shape, a
short/lowercase value and the drop cases.  Both real samples verified
(BAP 3632333239463337, AM0027 CFA3DD9C6B7F0F8C; the ALFA sample has no
ADM column and is unchanged).  915 frontend + 925 Python; sw cache
simple-v440.
This commit is contained in:
2026-10-14 18:07:04 +03:00
parent 91996a941b
commit 09d8a12add
9 changed files with 45 additions and 25 deletions
+27 -3
View File
@@ -139,7 +139,7 @@ class BapShapeTests(unittest.TestCase):
self.assertEqual(p['pin2'], '4321')
self.assertEqual(p['puk1'], '12345678')
self.assertEqual(p['puk2'], '87654321')
self.assertEqual(p['adm'], '62329F37') # the ASCII-hex decode
self.assertEqual(p['adm'], '3632333239463337') # the key's hex value as-is
# EKI, KIK and the IMSI columns are ignored - one keyset only
self.assertEqual(len(p['keysets']), 1)
self.assertEqual(p['keysets'][0]['kicKey'], K16A)
@@ -233,11 +233,35 @@ class OptionTests(unittest.TestCase):
self.assertEqual(len(res['presets']), 1)
self.assertEqual(res['presets'][0]['pin1'], '1234')
self.assertNotIn('adm', res['presets'][0])
# a present ADM is decoded from its ASCII-hex form
# the ADM column is the key's hex value, stored as-is (v3.28.2): the
# length varies, and the value is never ASCII-decoded
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
' 3632333239463337\n')
res = outfile.parse_outfile(text)
self.assertEqual(res['presets'][0]['adm'], '62329F37')
self.assertEqual(res['presets'][0]['adm'], '3632333239463337')
# ...an 8-byte binary-looking value (the AM0027 sample's shape)
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
' CFA3DD9C6B7F0F8C\n')
res = outfile.parse_outfile(text)
self.assertEqual(res['presets'][0]['adm'], 'CFA3DD9C6B7F0F8C')
# ...a shorter key (4 hex digits) and the lowercase form
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
' 1a2b3c4d\n')
res = outfile.parse_outfile(text)
self.assertEqual(res['presets'][0]['adm'], '1A2B3C4D')
# a non-hex or too-short ADM is dropped with its reason
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
' ZZZZ\n')
res = outfile.parse_outfile(text)
self.assertNotIn('adm', res['presets'][0])
self.assertTrue(any('ADM is not 4-16 hex digits' in s
for s in res['report']['skipped']))
text = ('ICCID KIC1 KID1 ADM\n' + DIGITS_F + ' ' + K16A + ' ' + K16B +
' 123\n')
res = outfile.parse_outfile(text)
self.assertNotIn('adm', res['presets'][0])
self.assertTrue(any('ADM is not 4-16 hex digits' in s
for s in res['report']['skipped']))
# a present but invalid PIN is dropped, the card kept
text = ('ICCID KIC1 KID1 PIN1\n' + DIGITS_F + ' ' + K16A + ' ' + K16B + ' 12\n')
res = outfile.parse_outfile(text)