From 288c2e5954f08381e96163b3f0ec40697d678e7c 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: Fri, 21 Aug 2026 22:37:39 +0300 Subject: [PATCH] C-APDU constructor spec fixes (TS 102 221 / GPC v2.3 / TS 102 226) SIM/USIM tab (TS 102 221): - Record P2 modes: 04/06/02 -> 04/02/03; P1=00 for next/previous - SELECT Le rules: USIM FID no Le, path/dfname P2=04+Le; SIM no Le - Path placeholder: 3F007FFF6FC5 -> 7FFF6FC5 - ACTIVATE/DEACTIVATE: 5-byte -> 4-byte case-1 - VERIFY PIN: dedicated input, Lc=08, FF-pad to 8 bytes - CHANGE PIN: old/new inputs, Lc=10, two 8-byte fields - ERASE BINARY removed from USIM tab RAM tab (GPC v2.3): - INSTALL: all 5 variants rewritten to strict LV + Le 00 - ELF/Module AID inputs for install-install - Privileges: 1 byte if only byte-1, else 3 bytes - InstallParams: C9 00 + toolkit TLV (mandatory) - LOAD/DELETE/GET STATUS: append Le 00 - GET STATUS P2: drop 40/42, keep 02/03/00 - GET DATA: 2F00/5031 -> case-4 5C00 data - SET STATUS: raw AID (no 4F), ISD card states, P1=60 - EXT AUTH: CLA 84, P1 security level, Lc=10 - INT AUTH: CLA 00 Toolkit (TS 102 226): - MSL: 01 -> 00 (no check) or 02 01 - SIM CA wrapped in EF: C9 00 EF CA... - TAR length %3 validation - Menu IDs <= 7F validation Script Chaining (TS 102 226 Table 5.9a): - Removed Script ID + Additional Data fields - Emit strictly 83 01 - Deleted generateScriptIdHint Privilege label: Token Verification -> Token Management --- frontend/index.html | 451 +++++++++++++++++++++---------------- frontend/tests/ram.test.js | 16 +- 2 files changed, 269 insertions(+), 198 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 7870ef6..8010243 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -87,7 +87,7 @@
- +
@@ -108,9 +108,9 @@
@@ -143,6 +143,21 @@ +
+ + +
+ +
+ + +
+ +
+ + +
+
@@ -178,7 +193,6 @@ - @@ -209,7 +223,7 @@
- +
@@ -230,9 +244,9 @@
@@ -265,6 +279,21 @@
+
+ + +
+ +
+ + +
+ +
+ + +
+
@@ -341,6 +370,14 @@
+
+ + +
+
+ + +
@@ -354,7 +391,7 @@ - + @@ -468,6 +505,14 @@
+
+ + +
- - - - + +
@@ -519,13 +562,19 @@
@@ -1052,7 +1101,7 @@ document.querySelectorAll('.tab-btn').forEach(btn => { const RECORD_OPS = new Set(['update-record', 'read-record']); const BINARY_OPS = new Set(['update-binary', 'read-binary', 'erase-binary']); const READ_OPS = new Set(['read-record', 'read-binary']); -const DATA_OPS = new Set(['update-record', 'update-binary', 'verify', 'change']); +const DATA_OPS = new Set(['update-record', 'update-binary']); const RECSIZE_OPS = new Set(['update-record']); const PIN_OPS = new Set(['verify', 'change']); @@ -1065,6 +1114,9 @@ function updateSimUsimFields(mode) { document.getElementById(mode + '-data-row').style.display = DATA_OPS.has(cmd) ? '' : 'none'; document.getElementById(mode + '-recsize-row').style.display = RECSIZE_OPS.has(cmd) ? '' : 'none'; document.getElementById(mode + '-pin-row').style.display = PIN_OPS.has(cmd) ? '' : 'none'; + document.getElementById(mode + '-pin-val-row').style.display = cmd === 'verify' ? '' : 'none'; + document.getElementById(mode + '-pin-old-row').style.display = cmd === 'change' ? '' : 'none'; + document.getElementById(mode + '-pin-new-row').style.display = cmd === 'change' ? '' : 'none'; if (cmd === 'select') { document.getElementById(mode + '-select-checkbox-row').style.display = 'none'; } else { @@ -1099,8 +1151,19 @@ function updateP1P2Display(mode) { const preset = mode === 'sim' ? {selP1: 0x00, selP2: 0x00} : {selP1: 0x00, selP2: 0x0C}; const cmd = document.getElementById(mode + '-cmd').value; if (cmd === 'select') { - document.getElementById(mode + '-p1').value = preset.selP1.toString(16).padStart(2, '0').toUpperCase(); - document.getElementById(mode + '-p2').value = preset.selP2.toString(16).padStart(2, '0').toUpperCase(); + const method = document.getElementById(mode + '-sel-method').value; + let p1, p2; + if (method === 'fid' || method === 'chain') { + p1 = preset.selP1; p2 = preset.selP2; + } else if (method === 'path') { + p1 = 0x08; p2 = mode === 'sim' ? 0x00 : 0x04; + } else if (method === 'dfname') { + p1 = 0x04; p2 = mode === 'sim' ? 0x00 : 0x04; + } else { + p1 = preset.selP1; p2 = preset.selP2; + } + document.getElementById(mode + '-p1').value = p1.toString(16).padStart(2, '0').toUpperCase(); + document.getElementById(mode + '-p2').value = p2.toString(16).padStart(2, '0').toUpperCase(); return; } const record = parseInt(document.getElementById(mode + '-record').value) || 1; @@ -1110,7 +1173,7 @@ function updateP1P2Display(mode) { const pin = document.getElementById(mode + '-pin').value; let p1, p2; if (cmd === 'read-record' || cmd === 'update-record') { - p1 = record; p2 = recMode; + p1 = (recMode === 0x02 || recMode === 0x03) ? 0x00 : record; p2 = recMode; } else if (cmd === 'read-binary' || cmd === 'update-binary' || cmd === 'erase-binary') { p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF; } else if (cmd === 'activate-file' || cmd === 'deactivate-file') { @@ -1127,13 +1190,17 @@ function updateP1P2Display(mode) { // ===== RAM field visibility ===== function updateRamFields() { const cmd = document.getElementById('ram-cmd').value; - document.getElementById('ram-priv-row').style.display = cmd === 'install-install' ? '' : 'none'; + const showPriv = ['install-install', 'install-make-sel', 'install-reg-update'].includes(cmd); + document.getElementById('ram-priv-row').style.display = showPriv ? '' : 'none'; document.getElementById('ram-toolkit-row').style.display = cmd === 'install-install' ? '' : 'none'; - document.getElementById('ram-sdaid-row').style.display = cmd === 'install-load' ? '' : 'none'; + document.getElementById('ram-sdaid-row').style.display = ['install-load', 'install-reg-update', 'install-extradition'].includes(cmd) ? '' : 'none'; + document.getElementById('ram-elfaid-row').style.display = cmd === 'install-install' ? '' : 'none'; + document.getElementById('ram-modaid-row').style.display = cmd === 'install-install' ? '' : 'none'; document.getElementById('ram-load-data-row').style.display = cmd === 'install-load' ? '' : 'none'; document.getElementById('ram-data-row').style.display = ['load', 'store-data', 'ext-auth', 'int-auth'].includes(cmd) ? '' : 'none'; document.getElementById('ram-block-row').style.display = ['load', 'store-data'].includes(cmd) ? '' : 'none'; document.getElementById('ram-enc-row').style.display = cmd === 'store-data' ? '' : 'none'; + document.getElementById('ram-authp1-row').style.display = cmd === 'ext-auth' ? '' : 'none'; document.getElementById('ram-del-mode-row').style.display = cmd === 'delete' ? '' : 'none'; document.getElementById('ram-gs-mode-row').style.display = cmd === 'get-status' ? '' : 'none'; document.getElementById('ram-gs-p2-row').style.display = cmd === 'get-status' ? '' : 'none'; @@ -1318,22 +1385,22 @@ function buildApdu(cla, ins, p1, p2, p3, data) { let s = cla.toString(16).padStart(2, '0').toUpperCase() + ins.toString(16).padStart(2, '0').toUpperCase() + p1.toString(16).padStart(2, '0').toUpperCase() + - p2.toString(16).padStart(2, '0').toUpperCase() + - p3.toString(16).padStart(2, '0').toUpperCase(); + p2.toString(16).padStart(2, '0').toUpperCase(); + if (p3 !== null && p3 !== undefined) s += p3.toString(16).padStart(2, '0').toUpperCase(); if (data) s += data.toUpperCase(); return s; } function buildSelect(cla, p1, p2, fid) { const data = fid.length === 4 ? fid : '0000'; - return buildApdu(cla, 0xA4, p1, p2, 0x02, data) + '00'; + return buildApdu(cla, 0xA4, p1, p2, 0x02, data); } function buildOpNoCla(ins, p1, p2, p3, data) { let s = ins.toString(16).padStart(2, '0').toUpperCase() + p1.toString(16).padStart(2, '0').toUpperCase() + - p2.toString(16).padStart(2, '0').toUpperCase() + - p3.toString(16).padStart(2, '0').toUpperCase(); + p2.toString(16).padStart(2, '0').toUpperCase(); + if (p3 !== null && p3 !== undefined) s += p3.toString(16).padStart(2, '0').toUpperCase(); if (data) s += data.toUpperCase(); return s; } @@ -1366,6 +1433,7 @@ function getSelValue(mode) { } function buildSelectApdu(mode, preset, selInfo) { + const isSim = preset.cla === 0xA0; if (!selInfo || !selInfo.value) return ''; if (selInfo.type === 'fid') { if (selInfo.value.length !== 4) return ''; @@ -1373,11 +1441,17 @@ function buildSelectApdu(mode, preset, selInfo) { } else if (selInfo.type === 'path') { if (selInfo.value.length < 2 || selInfo.value.length % 2 !== 0) return ''; const p3 = selInfo.value.length / 2; - return buildApdu(preset.cla, 0xA4, 0x08, 0x00, p3, selInfo.value) + '00'; + const p2 = isSim ? 0x00 : 0x04; + let apdu = buildApdu(preset.cla, 0xA4, 0x08, p2, p3, selInfo.value); + if (!isSim) apdu += '00'; + return apdu; } else if (selInfo.type === 'dfname') { if (selInfo.value.length < 2) return ''; const p3 = selInfo.value.length / 2; - return buildApdu(preset.cla, 0xA4, 0x04, 0x00, p3, selInfo.value) + '00'; + const p2 = isSim ? 0x00 : 0x04; + let apdu = buildApdu(preset.cla, 0xA4, 0x04, p2, p3, selInfo.value); + if (!isSim) apdu += '00'; + return apdu; } else if (selInfo.type === 'chain') { const fids = selInfo.value.split(',').map(f => f.trim().replace(/[^0-9a-fA-F]/g, '')).filter(f => f.length === 4); if (fids.length === 0) return ''; @@ -1426,22 +1500,31 @@ function genSimUsim(mode) { let p1, p2, p3, data = null; if (cmd === 'read-record') { - p1 = record; p2 = recMode; p3 = le; + p1 = (recMode === 0x02 || recMode === 0x03) ? 0x00 : record; p2 = recMode; p3 = le; } else if (cmd === 'read-binary') { p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF; p3 = le; } else if (cmd === 'update-record') { if (!dataHex) { resultEl.value = 'Error: specify data'; return; } - p1 = record; p2 = recMode; p3 = dataHex.length / 2; data = dataHex; + p1 = (recMode === 0x02 || recMode === 0x03) ? 0x00 : record; p2 = recMode; p3 = dataHex.length / 2; data = dataHex; } else if (cmd === 'update-binary') { if (!dataHex) { resultEl.value = 'Error: specify data'; return; } p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF; p3 = dataHex.length / 2; data = dataHex; } else if (cmd === 'erase-binary') { p1 = (offset >> 8) & 0xFF; p2 = offset & 0xFF; p3 = 0x00; } else if (cmd === 'activate-file' || cmd === 'deactivate-file') { - p1 = 0x00; p2 = 0x00; p3 = 0x00; - } else if (cmd === 'verify' || cmd === 'change') { - if (!dataHex) { resultEl.value = 'Error: specify data'; return; } - p1 = 0x00; p2 = parseInt(pin, 16); p3 = dataHex.length / 2; data = dataHex; + p1 = 0x00; p2 = 0x00; p3 = null; + } else if (cmd === 'verify') { + const pinVal = document.getElementById(mode + '-pin-val').value || ''; + if (!pinVal) { resultEl.value = 'Error: specify PIN value'; return; } + const pinHex = Array.from(pinVal).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').padEnd(16, 'F').slice(0, 16); + p1 = 0x00; p2 = parseInt(pin, 16); p3 = 0x08; data = pinHex; + } else if (cmd === 'change') { + const oldPin = document.getElementById(mode + '-pin-old').value || ''; + const newPin = document.getElementById(mode + '-pin-new').value || ''; + if (!oldPin || !newPin) { resultEl.value = 'Error: specify old and new PIN'; return; } + const oldHex = Array.from(oldPin).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').padEnd(16, 'F').slice(0, 16); + const newHex = Array.from(newPin).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').padEnd(16, 'F').slice(0, 16); + p1 = 0x00; p2 = parseInt(pin, 16); p3 = 0x10; data = oldHex + newHex; } if (override) { @@ -1469,6 +1552,8 @@ function genRam() { const cmd = document.getElementById('ram-cmd').value; const aid = (document.getElementById('ram-aid').value || '').replace(/[^0-9a-fA-F]/g, ''); const sdAid = (document.getElementById('ram-sdaid').value || '').replace(/[^0-9a-fA-F]/g, ''); + const elfAid = (document.getElementById('ram-elfaid').value || '').replace(/[^0-9a-fA-F]/g, ''); + const modAid = (document.getElementById('ram-modaid').value || '').replace(/[^0-9a-fA-F]/g, ''); const loadData = (document.getElementById('ram-load-data').value || '').replace(/[^0-9a-fA-F]/g, ''); const ramData = (document.getElementById('ram-data').value || '').replace(/[^0-9a-fA-F]/g, ''); const blockNum = parseInt(document.getElementById('ram-block').value) || 0; @@ -1476,134 +1561,163 @@ function genRam() { const delMode = document.getElementById('ram-del-mode').value; const gsMode = document.getElementById('ram-gs-mode').value; const tagHex = (document.getElementById('ram-tag').value || '5F50').replace(/[^0-9a-fA-F]/g, '').padStart(4, '0').slice(0, 4); - const ssMode = document.getElementById('ram-ss-mode').value; - const ssState = document.getElementById('ram-ss-state').value; + const ssMode = parseInt(document.getElementById('ram-ss-mode').value, 16); + const ssState = parseInt(document.getElementById('ram-ss-state').value, 16); const resultEl = document.getElementById('ram-result'); - if (cmd === 'install-load' && !sdAid) { resultEl.value = 'Error: specify SD AID'; return; } + const INSTALL_P1 = { + 'install-load': 0x02, + 'install-install': 0x0C, + 'install-make-sel': 0x08, + 'install-reg-update': 0x40, + 'install-extradition': 0x10, + }; - const INSTALL_P1 = { - 'install-load': 0x02, - 'install-install': 0x0C, - 'install-make-sel': 0x08, - 'install-reg-update': 0x40, - 'install-extradition': 0x10, - }; + function lv(data) { + if (!data) return '00'; + return berLenStr(data.length / 2) + data; + } + + function computePriv() { + const privHex = document.getElementById('ram-priv').value; + const b1 = parseInt(privHex.substr(0, 2), 16) || 0; + const b2 = privHex.length >= 4 ? parseInt(privHex.substr(2, 2), 16) : 0; + const b3 = privHex.length >= 6 ? parseInt(privHex.substr(4, 2), 16) : 0; + if (b2 === 0 && b3 === 0) return b1.toString(16).padStart(2, '0').toUpperCase(); + return b1.toString(16).padStart(2, '0').toUpperCase() + b2.toString(16).padStart(2, '0').toUpperCase() + b3.toString(16).padStart(2, '0').toUpperCase(); + } + + function buildToolkitParams() { + if (!document.getElementById('ram-toolkit-enable').checked) return ''; + const tkMode = document.getElementById('ram-tk-mode').value; + const priority = parseInt(document.getElementById('ram-tk-priority').value) || 0; + const timers = parseInt(document.getElementById('ram-tk-timers').value) || 0; + const textLen = parseInt(document.getElementById('ram-tk-textlen').value) || 0; + const menus = parseInt(document.getElementById('ram-tk-menus').value) || 0; + const firstPos = parseInt(document.getElementById('ram-tk-firstpos').value) || 0; + const firstId = (document.getElementById('ram-tk-firstid').value || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00'; + const lastPos = parseInt(document.getElementById('ram-tk-lastpos').value) || 0; + const lastId = (document.getElementById('ram-tk-lastid').value || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00'; + const channels = parseInt(document.getElementById('ram-tk-channels').value) || 0; + const msl = document.getElementById('ram-tk-msl').value; + const tar = (document.getElementById('ram-tk-tar').value || '').replace(/[^0-9a-fA-F]/g, ''); + const adRaw = (document.getElementById('ram-tk-ad').value || '').replace(/[^0-9a-fA-F]/g, ''); + const ad = adRaw.padStart(2, '0').slice(0, 2); + const services = parseInt(document.getElementById('ram-tk-services').value) || 0; + + if (tar && tar.length % 6 !== 0) { resultEl.value = 'Error: TAR must be 3-byte multiples'; return null; } + if (parseInt(firstId, 16) > 0x7F || parseInt(lastId, 16) > 0x7F) { resultEl.value = 'Error: menu IDs must be <= 7F'; return null; } + + let menuPairs = ''; + for (let i = 1; i <= menus; i++) { + if (i === 1) menuPairs += firstPos.toString(16).padStart(2, '0') + firstId; + else if (i === menus) menuPairs += lastPos.toString(16).padStart(2, '0') + lastId; + else menuPairs += '0000'; + } + + const mslField = msl === '00' ? '00' : '0201' + msl; + + let tkPayload = ''; + if (tkMode === 'ca') { + tkPayload = (adRaw ? '01' + ad : '00') + + priority.toString(16).padStart(2, '0') + + timers.toString(16).padStart(2, '0') + + textLen.toString(16).padStart(2, '0') + + menus.toString(16).padStart(2, '0') + + menuPairs + + channels.toString(16).padStart(2, '0') + + mslField + + (tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00'); + const caTlv = 'CA' + berLenStr(tkPayload.length / 2) + tkPayload; + return 'EF' + berLenStr(caTlv.length / 2) + caTlv; + } else { + tkPayload = priority.toString(16).padStart(2, '0') + + timers.toString(16).padStart(2, '0') + + textLen.toString(16).padStart(2, '0') + + menus.toString(16).padStart(2, '0') + + menuPairs + + channels.toString(16).padStart(2, '0') + + mslField + + (tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00') + + services.toString(16).padStart(2, '0'); + const innerTlv = '80' + berLenStr(tkPayload.length / 2) + tkPayload; + return 'EA' + berLenStr(innerTlv.length / 2) + innerTlv; + } + } let apdu = ''; if (cmd === 'install-load') { - const sdAidLen = (sdAid.length / 2).toString(16).padStart(2, '0'); - const aidLen = (aid.length / 2).toString(16).padStart(2, '0'); - let dataField = 'C9' + sdAidLen + sdAid + '5F50' + aidLen + aid; - if (loadData) { - const ldLen = (loadData.length / 2).toString(16).padStart(2, '0'); - dataField += 'C4' + ldLen + loadData; - } - const dataLen = (dataField.length / 2).toString(16).padStart(2, '0'); - apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField); + if (!aid) { resultEl.value = 'Error: specify Load File AID'; return; } + let dataField = lv(aid) + lv(sdAid) + '00' + (loadData ? lv(loadData) : '00') + '00'; + const dataLen = berLenStr(dataField.length / 2); + apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField) + '00'; } else if (cmd === 'install-install') { - const aidLen = (aid.length / 2).toString(16).padStart(2, '0'); - let dataField = '4F' + aidLen + aid; - const privHex = document.getElementById('ram-priv').value; - const privLen = (privHex.length / 2).toString(16).padStart(2, '0'); - dataField += 'C7' + privLen + privHex; - if (document.getElementById('ram-toolkit-enable').checked) { - const tkMode = document.getElementById('ram-tk-mode').value; - const priority = parseInt(document.getElementById('ram-tk-priority').value) || 0; - const timers = parseInt(document.getElementById('ram-tk-timers').value) || 0; - const textLen = parseInt(document.getElementById('ram-tk-textlen').value) || 0; - const menus = parseInt(document.getElementById('ram-tk-menus').value) || 0; - const firstPos = parseInt(document.getElementById('ram-tk-firstpos').value) || 0; - const firstId = (document.getElementById('ram-tk-firstid').value || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00'; - const lastPos = parseInt(document.getElementById('ram-tk-lastpos').value) || 0; - const lastId = (document.getElementById('ram-tk-lastid').value || '00').replace(/[^0-9a-fA-F]/g, '').padStart(2, '0').slice(0, 2) || '00'; - const channels = parseInt(document.getElementById('ram-tk-channels').value) || 0; - const msl = document.getElementById('ram-tk-msl').value; - const tar = (document.getElementById('ram-tk-tar').value || '').replace(/[^0-9a-fA-F]/g, ''); - const adRaw = (document.getElementById('ram-tk-ad').value || '').replace(/[^0-9a-fA-F]/g, ''); - const ad = adRaw.padStart(2, '0').slice(0, 2); - const services = parseInt(document.getElementById('ram-tk-services').value) || 0; - let menuPairs = ''; - for (let i = 1; i <= menus; i++) { - if (i === 1) menuPairs += firstPos.toString(16).padStart(2, '0') + firstId; - else if (i === menus) menuPairs += lastPos.toString(16).padStart(2, '0') + lastId; - else menuPairs += '0000'; - } - let tkPayload = ''; - if (tkMode === 'ca') { - tkPayload = (adRaw ? '01' + ad : '00') + - priority.toString(16).padStart(2, '0') + - timers.toString(16).padStart(2, '0') + - textLen.toString(16).padStart(2, '0') + - menus.toString(16).padStart(2, '0') + - menuPairs + - channels.toString(16).padStart(2, '0') + - '01' + msl + - (tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00'); - dataField += 'CA' + berLenStr(tkPayload.length / 2) + tkPayload; - } else { - tkPayload = priority.toString(16).padStart(2, '0') + - timers.toString(16).padStart(2, '0') + - textLen.toString(16).padStart(2, '0') + - menus.toString(16).padStart(2, '0') + - menuPairs + - channels.toString(16).padStart(2, '0') + - '01' + msl + - (tar ? (tar.length / 2).toString(16).padStart(2, '0') + tar : '00') + - services.toString(16).padStart(2, '0'); - const innerTlv = '80' + berLenStr(tkPayload.length / 2) + tkPayload; - dataField += 'EA' + berLenStr(innerTlv.length / 2) + innerTlv; - } - } - const dataLen = (dataField.length / 2).toString(16).padStart(2, '0'); - apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField); + if (!aid) { resultEl.value = 'Error: specify AID'; return; } + const privBytes = computePriv(); + let installParams = 'C900'; + const tkParams = buildToolkitParams(); + if (tkParams === null) return; + if (tkParams) installParams += tkParams; + let dataField = lv(elfAid) + lv(modAid) + lv(aid) + lv(privBytes) + lv(installParams) + '00'; + const dataLen = berLenStr(dataField.length / 2); + apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField) + '00'; } else if (cmd === 'install-make-sel') { if (!aid) { resultEl.value = 'Error: specify AID'; return; } - const aidLen = (aid.length / 2).toString(16).padStart(2, '0'); - const dataField = '4F' + aidLen + aid; - const dataLen = (dataField.length / 2).toString(16).padStart(2, '0'); - apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField); - } else if (cmd === 'install-reg-update' || cmd === 'install-extradition') { + const privBytes = computePriv(); + let dataField = '00' + '00' + lv(aid) + lv(privBytes) + '00' + '00'; + const dataLen = berLenStr(dataField.length / 2); + apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField) + '00'; + } else if (cmd === 'install-reg-update') { if (!aid) { resultEl.value = 'Error: specify AID'; return; } - const aidLen = (aid.length / 2).toString(16).padStart(2, '0'); - const dataField = '4F' + aidLen + aid; - const dataLen = (dataField.length / 2).toString(16).padStart(2, '0'); - apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField); + const privBytes = computePriv(); + let dataField = lv(sdAid) + '00' + lv(aid) + lv(privBytes) + '00' + '00'; + const dataLen = berLenStr(dataField.length / 2); + apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField) + '00'; + } else if (cmd === 'install-extradition') { + if (!sdAid || sdAid.length < 10 || sdAid.length > 20) { resultEl.value = 'Error: specify SD AID (5-10 bytes)'; return; } + if (!aid) { resultEl.value = 'Error: specify AID'; return; } + let dataField = lv(sdAid) + '00' + lv(aid) + '00' + '00' + '00'; + const dataLen = berLenStr(dataField.length / 2); + apdu = buildApdu(0x80, 0xE6, INSTALL_P1[cmd], 0x00, parseInt(dataLen, 16), dataField) + '00'; } else if (cmd === 'load') { if (!ramData) { resultEl.value = 'Error: specify data'; return; } - const dataLen = (ramData.length / 2).toString(16).padStart(2, '0'); - apdu = buildApdu(0x80, 0xE8, 0x80, blockNum, parseInt(dataLen, 16), ramData); + const dataLen = berLenStr(ramData.length / 2); + apdu = buildApdu(0x80, 0xE8, 0x80, blockNum, parseInt(dataLen, 16), ramData) + '00'; } else if (cmd === 'delete') { if (!aid) { resultEl.value = 'Error: specify AID'; return; } - const aidLen = (aid.length / 2).toString(16).padStart(2, '0'); - const dataField = '4F' + aidLen + aid; - const dataLen = (dataField.length / 2).toString(16).padStart(2, '0'); - apdu = buildApdu(0x80, 0xE4, 0x00, parseInt(delMode, 16), parseInt(dataLen, 16), dataField); + const dataField = '4F' + berLenStr(aid.length / 2) + aid; + const dataLen = berLenStr(dataField.length / 2); + apdu = buildApdu(0x80, 0xE4, 0x00, parseInt(delMode, 16), parseInt(dataLen, 16), dataField) + '00'; } else if (cmd === 'get-status') { const gsP2 = document.getElementById('ram-gs-p2').value; - apdu = buildApdu(0x80, 0xF2, parseInt(gsMode, 16), parseInt(gsP2, 16), 0x02, '4F00'); + apdu = buildApdu(0x80, 0xF2, parseInt(gsMode, 16), parseInt(gsP2, 16), 0x02, '4F00') + '00'; } else if (cmd === 'get-data') { const tag = parseInt(tagHex, 16); - apdu = buildApdu(0x80, 0xCA, (tag >> 8) & 0xFF, tag & 0xFF, 0x00, null); + if (tagHex === '2F00' || tagHex === '5031') { + apdu = buildApdu(0x80, 0xCA, (tag >> 8) & 0xFF, tag & 0xFF, 0x02, '5C00') + '00'; + } else { + apdu = buildApdu(0x80, 0xCA, (tag >> 8) & 0xFF, tag & 0xFF, 0x00, null) + '00'; + } } else if (cmd === 'store-data') { if (!ramData) { resultEl.value = 'Error: specify data'; return; } - const dataLen = (ramData.length / 2).toString(16).padStart(2, '0'); + const dataLen = berLenStr(ramData.length / 2); apdu = buildApdu(0x80, 0xE2, parseInt(enc, 16), blockNum, parseInt(dataLen, 16), ramData); } else if (cmd === 'set-status') { - if (!aid) { resultEl.value = 'Error: specify AID'; return; } - const aidLen = (aid.length / 2).toString(16).padStart(2, '0'); - const dataField = '4F' + aidLen + aid; - const dataLen = (dataField.length / 2).toString(16).padStart(2, '0'); - apdu = buildApdu(0x80, 0xF0, parseInt(ssMode, 16), parseInt(ssState, 16), parseInt(dataLen, 16), dataField); + if (ssMode === 0x80) { + apdu = buildApdu(0x80, 0xF0, 0x80, ssState, 0x00, null); + } else { + if (!aid) { resultEl.value = 'Error: specify AID'; return; } + apdu = buildApdu(0x80, 0xF0, ssMode, ssState, aid.length / 2, aid); + } } else if (cmd === 'ext-auth') { - if (!ramData) { resultEl.value = 'Error: specify data (host cryptogram)'; return; } - const dataLen = (ramData.length / 2).toString(16).padStart(2, '0'); - apdu = buildApdu(0x80, 0x82, 0x00, 0x00, parseInt(dataLen, 16), ramData); + if (!ramData || ramData.length !== 32) { resultEl.value = 'Error: specify 16-byte data (host cryptogram + MAC)'; return; } + const authP1 = parseInt(document.getElementById('ram-authp1').value, 16); + apdu = buildApdu(0x84, 0x82, authP1, 0x00, 0x10, ramData); } else if (cmd === 'int-auth') { if (!ramData) { resultEl.value = 'Error: specify data (challenge)'; return; } - const dataLen = (ramData.length / 2).toString(16).padStart(2, '0'); - apdu = buildApdu(0x80, 0x88, 0x00, 0x00, parseInt(dataLen, 16), ramData) + '00'; + const dataLen = berLenStr(ramData.length / 2); + apdu = buildApdu(0x00, 0x88, 0x00, 0x00, parseInt(dataLen, 16), ramData) + '00'; } resultEl.value = apdu || 'Error: unknown command'; @@ -2306,24 +2420,6 @@ function buildScriptChainingRow(rowIdx, hint = '') { Keep chaining info across reset (RFM)
- -
- Script ID: - - (hex, 1-4 bytes) - -
- -
- Additional Data: - -
`; } @@ -2341,31 +2437,13 @@ function updateChainingFlags(rowIdx) { updateBerRow(document.querySelector(`[data-ber-idx="${rowIdx}"] .ber-type`)); } -function generateScriptIdHint(rowIdx) { - const row = document.querySelector(`[data-ber-idx="${rowIdx}"]`); - if (!row) return; - const scriptIdInput = row.querySelector('.chaining-script-id'); - if (!scriptIdInput) return; - - const allIds = Array.from(document.querySelectorAll('.chaining-script-id')) - .map(input => input.value || input.placeholder) - .filter(id => id && id !== ''); - - const lastId = allIds.length ? allIds[allIds.length - 1] : '00000000'; - const num = parseInt(lastId, 16) + 1; - const nextId = num.toString(16).padStart(8, '0').toUpperCase(); - scriptIdInput.placeholder = nextId; -} - function genScriptChainingValue(row, tag) { const rowIdx = row.dataset.berIdx; const firstRadio = row.querySelector(`input[name="chaining-${rowIdx}"][value="first"]`); const intermediateRadio = row.querySelector(`input[name="chaining-${rowIdx}"][value="intermediary"]`); const lastCheckbox = row.querySelector(`input[name="chaining-${rowIdx}-last"]`); const keepCheckbox = row.querySelector(`input[name="chaining-${rowIdx}-keep"]`); - const scriptIdInput = row.querySelector('.chaining-script-id'); - const additionalInput = row.querySelector('.chaining-additional'); - + let flags = ''; if (firstRadio && firstRadio.checked) { flags = (keepCheckbox && keepCheckbox.checked) ? '11' : '01'; @@ -2373,21 +2451,8 @@ function genScriptChainingValue(row, tag) { flags = (lastCheckbox && lastCheckbox.checked) ? '03' : '02'; } if (!flags) return ''; - - let value = flags; - - const scriptId = (scriptIdInput.value || '').replace(/[^0-9a-fA-F]/g, ''); - if (scriptId) { - value += scriptId; - } - - const additional = (additionalInput.value || '').replace(/[^0-9a-fA-F]/g, ''); - if (additional) { - value += additional; - } - - if (!value || value === '00') return ''; - return tag + berLenStr(value.length/2) + value; + + return tag + '01' + flags; } function genBerPcValue(row, tag) { @@ -3096,7 +3161,7 @@ const LIFECYCLE_MAP = { const PRIVILEGE_NAMES = [ ['Security Domain', 'DAP Verification', 'Delegated Management', 'Card Lock', 'Card Terminate', 'Card Reset', 'CVM Management', 'Mandated DAP Verification'], - ['Trusted Path', 'Authorized Management', 'Token Verification', 'Global Delete', 'Global Lock', 'Global Registry', 'Final Application', 'Global Service'], + ['Trusted Path', 'Authorized Management', 'Token Management', 'Global Delete', 'Global Lock', 'Global Registry', 'Final Application', 'Global Service'], ['Receipt Generation', 'Ciphered Load File Data Block', 'Contactless Activation', 'Contactless Self-Activation'], ]; @@ -5377,6 +5442,12 @@ const LANG_RU = { 'Record size (bytes)': 'Размер записи (байт)', 'Allow P1/P2 editing': 'Разрешить редактирование P1/P2', 'Privileges': 'Привилегии', + 'PIN value': 'Значение PIN', + 'Old PIN': 'Старый PIN', + 'New PIN': 'Новый PIN', + 'ELF AID (hex)': 'ELF AID (hex)', + 'Module AID (hex)': 'AID модуля (hex)', + 'Security level (P1)': 'Уровень безопасности (P1)', 'Byte 1': 'Байт 1', 'Byte 2': 'Байт 2', 'SIM/UICC Toolkit parameters': 'Параметры SIM/UICC Toolkit', diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 7a8fbd2..4bd32a3 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -87,7 +87,7 @@ function genRamApdu() { test('UICC toolkit nested inside EA (m=2, services 0)', () => { const apdu = genRamApdu(); - assert.ok(apdu.includes('EA128010000000020101020200011603B0000100'), apdu); + assert.ok(apdu.includes('EA13801100000002010102020002011603B0000100'), apdu); }); test('UICC m=1 emits single pair', () => { @@ -111,7 +111,7 @@ test('UICC m=1 emits single pair', () => { set('ram-tk-ad', ''); set('ram-tk-services', '0'); const apdu = genRamResult(); - assert.ok(apdu.includes('EA10800E00000001010100011603B0000100'), apdu); + assert.ok(apdu.includes('EA11800F0000000101010002011603B0000100'), apdu); }); test('UICC m=3 fills middle pair with 0000', () => { @@ -135,7 +135,7 @@ test('UICC m=3 fills middle pair with 0000', () => { set('ram-tk-ad', ''); set('ram-tk-services', '0'); const apdu = genRamResult(); - assert.ok(apdu.includes('EA1480120000000301010000030300011603B0000100'), apdu); + assert.ok(apdu.includes('EA158013000000030101000003030002011603B0000100'), apdu); }); test('UICC services 7 appended as final byte', () => { @@ -159,7 +159,7 @@ test('UICC services 7 appended as final byte', () => { set('ram-tk-ad', ''); set('ram-tk-services', '7'); const apdu = genRamResult(); - assert.ok(apdu.includes('EA128010000000020101020200011603B0000107'), apdu); + assert.ok(apdu.includes('EA13801100000002010102020002011603B0000107'), apdu); }); test('SIM (CA) access domain FIRST, no services byte', () => { @@ -183,7 +183,7 @@ test('SIM (CA) access domain FIRST, no services byte', () => { set('ram-tk-ad', '5A'); set('ram-tk-services', '0'); const apdu = genRamResult(); - assert.ok(apdu.includes('CA11015A000000020101020200011603B00001'), apdu); + assert.ok(apdu.includes('EF14CA12015A00000002010102020002011603B00001'), apdu); }); test('SIM (CA) blank access domain emits length byte 00', () => { @@ -207,7 +207,7 @@ test('SIM (CA) blank access domain emits length byte 00', () => { set('ram-tk-ad', ''); set('ram-tk-services', '0'); const apdu = genRamResult(); - assert.ok(apdu.includes('CA1000000000020101020200011603B00001'), apdu); + assert.ok(apdu.includes('EF13CA110000000002010102020002011603B00001'), apdu); }); test('SIM (CA) m=3, no TAR, blank access domain', () => { @@ -231,7 +231,7 @@ test('SIM (CA) m=3, no TAR, blank access domain', () => { set('ram-tk-ad', ''); set('ram-tk-services', '0'); const apdu = genRamResult(); - assert.ok(apdu.includes('CA0F000000000301010000030300011600'), apdu); + assert.ok(apdu.includes('EF12CA1000000000030101000003030002011600'), apdu); }); test('UICC m=60 uses long-form BER lengths (EA 81 87 / inner 81 84)', () => { @@ -255,7 +255,7 @@ test('UICC m=60 uses long-form BER lengths (EA 81 87 / inner 81 84)', () => { set('ram-tk-ad', ''); set('ram-tk-services', '0'); const apdu = genRamResult(); - assert.ok(apdu.includes('EA8187808184'), apdu); + assert.ok(apdu.includes('EA8188808185'), apdu); }); test('LOAD P1 fixed to 80 (last block)', () => {