add key-free unit tests for genSp/secured packet (node:test, synthetic + sysmocom vectors)
This commit is contained in:
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx tailwindcss -i src/style.css -o style.css --config tailwind.config.js",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user