From fc537a81866c136fd6c5b82fc77567f576d2e9a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sat, 8 Aug 2026 13:20:44 +0300 Subject: [PATCH] add key-free unit tests for genSp/secured packet (node:test, synthetic + sysmocom vectors) --- package.json | 3 +- tests/sp.test.js | 123 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 tests/sp.test.js diff --git a/package.json b/package.json index 1d23a7b..1b0e8dc 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "index.js", "scripts": { "build": "npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js", - "build:prod": "NODE_ENV=production npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js --minify" + "build:prod": "NODE_ENV=production npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js --minify", + "test": "node --test" }, "repository": { "type": "git", diff --git a/tests/sp.test.js b/tests/sp.test.js new file mode 100644 index 0000000..b3d8033 --- /dev/null +++ b/tests/sp.test.js @@ -0,0 +1,123 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const fs = require('node:fs'); +const path = require('node:path'); + +const des = require('des.js'); +global.des = des; + +const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); + +function extractFunc(src, name) { + const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{'); + const m = re.exec(src); + if (!m) throw new Error('function ' + name + ' not found'); + let i = m.index + m[0].length - 1; + let depth = 0; + for (; i < src.length; i++) { + if (src[i] === '{') depth++; + else if (src[i] === '}') { + depth--; + if (depth === 0) break; + } + } + return src.slice(m.index, i + 1); +} + +const FNS = ['hexToBytes', 'bytesToHex', 'des3Keys', 'des3EncryptBlock', 'des3CbcEncrypt', + 'xorBytes', 'zeroPad', 'cbcMac', 'genSp']; +let code = ''; +for (const f of FNS) code += extractFunc(html, f) + '\n'; + +// All test vectors are computed with the synthetic dummy key material below +// (no live/sample card keys, no ICCIDs). They are cross-checked byte-for-byte +// against pySim's OtaDialectSms.encode_cmd reference implementation. +const K = '00112233445566778899AABBCCDDEEFF'; + +const DEFAULTS = { + 'sp-apdu': '00A40000023F00', + 'sp-spi1': '06', + 'sp-spi2-hex': '09', + 'sp-kic-hex': '15', + 'sp-kid-hex': '15', + 'sp-tar': 'B00000', + 'sp-cntr': '0000000001', + 'sp-kic-key': K, + 'sp-kid-key': K, + 'sp-padding': '00', +}; + +const values = {}; +const elements = {}; +global.document = { + getElementById(id) { + if (!elements[id]) elements[id] = { value: values[id] || '' }; + return elements[id]; + }, +}; + +eval(code); + +function run() { + genSp(); + return (elements['sp-result'] || { value: '' }).value; +} + +function makeRun(overrides) { + for (const [id, v] of Object.entries(DEFAULTS)) { + values[id] = v; + if (elements[id]) elements[id].value = v; + } + for (const [id, v] of Object.entries(overrides || {})) { + values[id] = v; + if (elements[id]) elements[id].value = v; + } + return run(); +} + +test('ciphered + CC SPI 06/09', () => { + assert.strictEqual( + makeRun({}), + '00201506091515B00000C08F58C38860ACB3A362FFFE670AD13759A2A6B4C1A91116'); +}); + +test('ciphered + CC SPI 16/01 (counter_must_be_higher, plaintext PoR)', () => { + assert.strictEqual( + makeRun({ 'sp-spi1': '16', 'sp-spi2-hex': '01' }), + '00201516011515B00000E42573469E68A8462A57A505B0E2B1C09C1928C7A182311F'); +}); + +test('unciphered + CC SPI 02/09', () => { + assert.strictEqual( + makeRun({ 'sp-spi1': '02', 'sp-spi2-hex': '09' }), + '001D1502091515B0000000000000010085A8CA1A9828B0BB00A40000023F00'); +}); + +test('unciphered packet uses CPL = octets from CHL to end (0x001d)', () => { + const out = makeRun({ 'sp-spi1': '02', 'sp-spi2-hex': '09' }); + assert.strictEqual(out.slice(0, 4), '001D'); + assert.strictEqual(out.length, 62); +}); + +test('sysmocom public reference vector (spi1 04 / spi2 19, cntr=0)', () => { + assert.strictEqual( + makeRun({ + 'sp-spi1': '04', + 'sp-spi2-hex': '19', + 'sp-kic-hex': '35', + 'sp-kid-hex': '35', + 'sp-cntr': '0000000000', + 'sp-kic-key': 'C21DD66ACAC13CB3BC8B331B24AFB57B', + 'sp-kid-key': '12110C78E678C25408233076AA033615', + }), + '00180D04193535B00000E3EC80A849B554421276AF3883927C20'); +}); + +test('missing APDU reports an error', () => { + assert.strictEqual(makeRun({ 'sp-apdu': '' }), 'Error: specify APDU'); +}); + +test('cbcMac known answer (synthetic key)', () => { + const input = hexToBytes('001d1502091515b0000000000000010000a40000023f00'); + assert.strictEqual(bytesToHex(cbcMac(input, hexToBytes(K))), '85A8CA1A9828B0BB'); +});