feat: SCP80 SMS concatenation + packet size / SMS count display (v3.2.0)
Packets longer than one SMS now go out as concatenated SMS-PP downloads per TS 31.115 4.3 and the UI shows how many SMS a packet needs. Server: - `_split_secured_packet` cuts the command packet at the exact SMS user-data capacities (first SM 132 octets: concat IE 5 + CPI IE 2; following ones 134; a single-SM packet may be 137 with the CPI IE) and `_build_sms_tpdu` tags every segment with the fixed concatenation reference 01; `_build_sms_tpdu` also enforces the 140-octet budget. - `_send_secured_packet` (shared by /api/send-ota and /api/ram-install) sends one ENVELOPE per segment in order, refuses more than MAX_ENVELOPE_SEGMENTS (5, the card's concatenation buffer) and reports `bytes`/`segments` in the response (RAM install per step as well). - `_build_secured_packet`/`_encode_cmd_unlimited`: our own TS 102 225 5.1.1 encoder on pySim's keyset/header constructors, byte-identical to pySim for packets <= 140 octets (tests) and not limited to one SMS (pySim refuses the longer ones, which is why they never went out). - RAM LOAD blocks are no longer clamped to one SMS: 1-240 bytes of payload with the default 240 (the GP maximum); `load_block_size_auto` replaces `load_block_size_clamped`. PWA: - `scp80SegmentInfo` / `spSizeInfoText` show "N bytes . M SMS (concatenated)" under the packet field, turn red past 5 SMS and report size/SMS in the send result and the RAM step log; LOAD block hints and placeholders updated; EN/RU. Tests/docs: Python +2 cases incl. segment order/capacities and byte identity with pySim; Node drift guard against the server constants and UI text tests; README/README_RUS, help EN/RU, docs/api.md, AGENTS.
This commit is contained in:
@@ -28,9 +28,14 @@ function extractFunc(src, name) {
|
||||
|
||||
const FNS = ['hexToBytes', 'bytesToHex', 'des3Keys', 'des3EncryptBlock', 'des3CbcEncrypt',
|
||||
'xorBytes', 'zeroPad', 'cbcMac', 'aesCbcEncrypt', 'aesShiftLeft1', 'aesCmacSubkeys',
|
||||
'aesCmac', 'genSp', 'spNextCntr'];
|
||||
'aesCmac', '_genSpBuild', 'genSp', 'spNextCntr', 'scp80SegmentInfo', 'spSizeInfoText',
|
||||
'spShowSizeInfo'];
|
||||
let code = '';
|
||||
for (const f of FNS) code += extractFunc(html, f) + '\n';
|
||||
for (const c of ['SCP80_MAX_SMS', 'SCP80_SINGLE_BYTES', 'SCP80_FIRST_BYTES', 'SCP80_NEXT_BYTES']) {
|
||||
code += html.match(new RegExp('const ' + c + ' = \\d+;'))[0].replace('const ', 'var ') + '\n';
|
||||
}
|
||||
code += 'function t(s){return s;}\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
|
||||
@@ -229,3 +234,39 @@ test('spNextCntr tolerates lower case and separators', () => {
|
||||
assert.strictEqual(spNextCntr('00000000 0a'), '000000000B');
|
||||
assert.strictEqual(spNextCntr(''), '0000000001');
|
||||
});
|
||||
|
||||
test('scp80SegmentInfo mirrors the server segmentation rules', () => {
|
||||
assert.deepStrictEqual(scp80SegmentInfo(''), { bytes: 0, segments: 0 });
|
||||
assert.deepStrictEqual(scp80SegmentInfo('AA'.repeat(137)), { bytes: 137, segments: 1 });
|
||||
assert.deepStrictEqual(scp80SegmentInfo('AA'.repeat(138)), { bytes: 138, segments: 2 });
|
||||
assert.deepStrictEqual(scp80SegmentInfo('AA'.repeat(132 + 134)), { bytes: 266, segments: 2 });
|
||||
assert.deepStrictEqual(scp80SegmentInfo('AA'.repeat(132 + 134 * 2)), { bytes: 400, segments: 3 });
|
||||
});
|
||||
|
||||
test('spSizeInfoText reports size, SMS count and the card buffer limit', () => {
|
||||
assert.strictEqual(spSizeInfoText('AA'.repeat(18)), '18 bytes · 1 SMS');
|
||||
assert.strictEqual(spSizeInfoText('AA'.repeat(266)), '266 bytes · 2 SMS (concatenated)');
|
||||
assert.match(spSizeInfoText('AA'.repeat(132 + 134 * 5)),
|
||||
/too large for the card concatenation buffer \(5 SMS\)/);
|
||||
assert.strictEqual(spSizeInfoText(''), '');
|
||||
});
|
||||
|
||||
test('the SCP80 UI constants match the server segmentation constants', () => {
|
||||
const py = fs.readFileSync(
|
||||
path.join(__dirname, '..', '..', 'pysim_simple_server', 'server.py'), 'utf8');
|
||||
const read = (name) => {
|
||||
const m = new RegExp('^' + name + '\\s*=\\s*(\\d+)', 'm').exec(py);
|
||||
assert.ok(m, name + ' not found in server.py');
|
||||
return parseInt(m[1], 10);
|
||||
};
|
||||
assert.strictEqual(SCP80_SINGLE_BYTES, read('SCP80_SINGLE_BYTES'));
|
||||
assert.strictEqual(SCP80_FIRST_BYTES, read('SCP80_FIRST_BYTES'));
|
||||
assert.strictEqual(SCP80_NEXT_BYTES, read('SCP80_NEXT_BYTES'));
|
||||
assert.strictEqual(SCP80_MAX_SMS, read('MAX_ENVELOPE_SEGMENTS'));
|
||||
});
|
||||
|
||||
test('a generated single-SMS packet shows its size and SMS count', () => {
|
||||
const pkt = makeRun({});
|
||||
const info = elements['sp-size-info'] || {};
|
||||
assert.strictEqual(info.textContent, (pkt.length / 2) + ' bytes · 1 SMS');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user