Decode C-APDU commands inside secured-packet payloads (frontend)
The secured data of an SCP80 packet is a stream of C-APDUs (e.g. SELECT / UPDATE BINARY sequences for OTA file management). Vendor the working C-APDU parser from the otaman project (frontend-only, ~10 KB): PARSE_INS table, describeP1P2, describeInstallDataLv, parseOneApdu and parseCompactApdus (concatenated streams with implied-CLA handling). Detail view now shows an "Application message" section for secured packets: - unciphered packets (SPI1.b3=0): the on-wire secured data decodes immediately; - ciphered packets: decodes from the client-side decrypted payload once the user supplies the key (before that, ciphertext rows only); - each C-APDU renders as a numbered row with indented CLA/INS/P1/P2/ Lc/Data/Le + descriptions (nested LV for INSTALL), with implied-CLA annotation; raw-hex fallback when nothing parses; - vendored parser uppercases input (otaman's expects uppercase hex). Tests deferred to the follow-up step (node:test infra, planned). Version 1.20.0, SW cache simtrace-analyser-v28.
This commit is contained in:
1 parent
6f058392f8
commit
6da5acca21
5 files changed
+330
-5
No files matched your search
+326
-1
@@ -615,6 +615,294 @@ function desCbcDecrypt(keyBytes, ivBytes, ctBytes) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ──────────────────── C-APDU parsing (vendored from otaman) ────────────────────
|
||||||
|
// Parses C-APDU hex streams (e.g. SCP80 secured-packet application messages)
|
||||||
|
// into {label, hex, desc, children} trees.
|
||||||
|
const PARSE_INS = {
|
||||||
|
'A4': {name:'SELECT', data:'lc'},
|
||||||
|
'B0': {name:'READ BINARY', data:'le'},
|
||||||
|
'B2': {name:'READ RECORD(S)', data:'le'},
|
||||||
|
'DC': {name:'UPDATE RECORD', data:'lc'},
|
||||||
|
'D6': {name:'UPDATE BINARY', data:'lc'},
|
||||||
|
'0E': {name:'ERASE BINARY', data:'none'},
|
||||||
|
'44': {name:'ACTIVATE FILE', data:'none'},
|
||||||
|
'04': {name:'DEACTIVATE FILE', data:'none'},
|
||||||
|
'20': {name:'VERIFY', data:'lc'},
|
||||||
|
'24': {name:'CHANGE', data:'lc'},
|
||||||
|
'26': {name:'DISABLE PIN', data:'lc'},
|
||||||
|
'28': {name:'ENABLE PIN', data:'lc'},
|
||||||
|
'2C': {name:'UNBLOCK PIN', data:'lc'},
|
||||||
|
'E6': {name:'INSTALL', data:'lc'},
|
||||||
|
'E8': {name:'LOAD', data:'lc'},
|
||||||
|
'D4': {name:'RESIZE FILE', data:'lc'},
|
||||||
|
'E0': {name:'CREATE FILE', data:'lc'},
|
||||||
|
'E4': {name:'DELETE', data:'lc'},
|
||||||
|
'F2': {name:'GET STATUS', data:'lc'},
|
||||||
|
'CA': {name:'GET DATA', data:'none'},
|
||||||
|
'DA': {name:'PUT DATA', data:'lc'},
|
||||||
|
'CB': {name:'RETRIEVE DATA', data:'le'},
|
||||||
|
'DB': {name:'SET DATA', data:'lc'},
|
||||||
|
'A2': {name:'SEARCH RECORD', data:'lc'},
|
||||||
|
'32': {name:'INCREASE', data:'lc'},
|
||||||
|
'84': {name:'GET CHALLENGE', data:'le'},
|
||||||
|
'88': {name:'AUTHENTICATE', data:'lc'},
|
||||||
|
'89': {name:'AUTHENTICATE (odd INS)', data:'lc'},
|
||||||
|
'E2': {name:'STORE DATA', data:'lc'},
|
||||||
|
'F0': {name:'SET STATUS', data:'lc'},
|
||||||
|
'82': {name:'EXTERNAL AUTHENTICATE', data:'lc'},
|
||||||
|
'C0': {name:'GET RESPONSE', data:'le'},
|
||||||
|
'10': {name:'TERMINAL PROFILE', data:'lc'},
|
||||||
|
'C2': {name:'ENVELOPE', data:'lc'},
|
||||||
|
'12': {name:'FETCH', data:'le'},
|
||||||
|
'14': {name:'TERMINAL RESPONSE', data:'lc'},
|
||||||
|
'76': {name:'SUSPEND UICC', data:'lc'},
|
||||||
|
'AA': {name:'TERMINAL CAPABILITY', data:'lc'},
|
||||||
|
'73': {name:'MANAGE SECURE CHANNEL', data:'lc'},
|
||||||
|
'75': {name:'TRANSACT DATA', data:'lc'},
|
||||||
|
'78': {name:'GET IDENTITY', data:'le'},
|
||||||
|
'7A': {name:'EXCHANGE CAPABILITIES', data:'lc'},
|
||||||
|
'7C': {name:'MANAGE LSI', data:'lc'},
|
||||||
|
'50': {name:'INITIALIZE UPDATE', data:'lc'},
|
||||||
|
'D8': {name:'PUT KEY', data:'lc'},
|
||||||
|
'70': {name:'MANAGE CHANNEL', data:'none'},
|
||||||
|
};
|
||||||
|
|
||||||
|
function describeP1P2(ins, p1, p2) {
|
||||||
|
const v1 = parseInt(p1, 16);
|
||||||
|
const v2 = parseInt(p2, 16);
|
||||||
|
let d1 = '', d2 = '';
|
||||||
|
if (ins === 'A4') {
|
||||||
|
d1 = {'00':'by FID','01':'child DF','03':'parent DF','04':'by DF name (AID)','08':'path from MF','09':'path from current DF'}[p1] || '';
|
||||||
|
const resp = (v2 >> 2) & 0x07;
|
||||||
|
if (resp === 0x01) d2 = 'return FCP template';
|
||||||
|
else if (resp === 0x03) d2 = 'no response data';
|
||||||
|
if (v1 === 0x04) {
|
||||||
|
const occName = {'00':'first or only occurrence','01':'last occurrence','10':'next occurrence','11':'previous occurrence'}[(v2 & 0x03).toString(2).padStart(2,'0')];
|
||||||
|
if (occName) d2 += (d2 ? ', ' : '') + occName;
|
||||||
|
}
|
||||||
|
} else if (ins === 'B0' || ins === 'D6') {
|
||||||
|
if (v1 & 0x80) d1 = 'SFI mode, SFI ' + (v1 & 0x1F);
|
||||||
|
else d1 = 'offset high';
|
||||||
|
d2 = 'offset low';
|
||||||
|
} else if (ins === 'B2' || ins === 'DC') {
|
||||||
|
const modeKey = (v2 & 0x07).toString(16).padStart(2, '0');
|
||||||
|
d2 = {'02':'next record','03':'previous record','04':'absolute/current'}[modeKey] || '';
|
||||||
|
const sfi = (v2 >> 3) & 0x1F;
|
||||||
|
if (sfi) d2 += (d2 ? ', ' : '') + 'SFI ' + sfi;
|
||||||
|
if ((v2 & 0x07) === 0x02 || (v2 & 0x07) === 0x03) d1 = v1 === 0 ? 'ignored' : 'record # (ignored)';
|
||||||
|
else if (v1 > 0) d1 = 'record #' + v1;
|
||||||
|
} else if (ins === 'F2') {
|
||||||
|
d1 = {'80':'ISD','40':'Applications incl. SSDs','20':'Executable Load Files','10':'ELFs and Modules'}[p1] || '';
|
||||||
|
d2 = {'02':'first/all, TLV format','03':'next, TLV format','00':'first/all, deprecated raw','01':'next, deprecated raw'}[p2] || '';
|
||||||
|
} else if (ins === 'E6') {
|
||||||
|
const roles = [];
|
||||||
|
if (v1 & 0x02) roles.push('for load');
|
||||||
|
if (v1 & 0x04) roles.push('for install');
|
||||||
|
if (v1 & 0x08) roles.push('for make selectable');
|
||||||
|
if (v1 & 0x10) roles.push('for extradition');
|
||||||
|
if (v1 & 0x20) roles.push('for personalization');
|
||||||
|
if (v1 & 0x40) roles.push('for registry update');
|
||||||
|
if (roles.length) d1 = roles.join(' + ');
|
||||||
|
if (v1 & 0x80) d1 = 'more INSTALL commands follow; ' + d1;
|
||||||
|
} else if (ins === 'F0') {
|
||||||
|
d1 = {'80':'ISD','40':'Application or SSD','60':'SD + associated apps'}[p1] || '';
|
||||||
|
if (p1 === '80') {
|
||||||
|
d2 = {'01':'OP_READY','07':'INITIALIZED','0F':'SECURED','7F':'CARD_LOCKED','FF':'TERMINATED'}[p2] || '';
|
||||||
|
} else {
|
||||||
|
d2 = {'00':'unlock (previous state)','80':'LOCKED'}[p2] || '';
|
||||||
|
}
|
||||||
|
} else if (ins === '20' || ins === '24') {
|
||||||
|
if (v2 >= 1 && v2 <= 8) d2 = 'PIN ref ' + v2;
|
||||||
|
}
|
||||||
|
return {d1, d2};
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeInstallDataLv(p1val, data) {
|
||||||
|
let fields;
|
||||||
|
if (p1val & 0x40) {
|
||||||
|
fields = ['SD AID', '(reserved)', 'Application AID', 'Privileges', 'RU parameters', 'RU token'];
|
||||||
|
} else if (p1val & 0x20) {
|
||||||
|
fields = ['(reserved)', '(reserved)', 'Application AID', 'Privileges', 'Personalization parameters', 'Personalization token'];
|
||||||
|
} else if (p1val & 0x10) {
|
||||||
|
fields = ['Target SD AID', '(reserved)', 'Application/ELF AID', '(reserved)', 'Extradition parameters', 'Extradition token'];
|
||||||
|
} else if ((p1val & 0x08) && !(p1val & 0x04)) {
|
||||||
|
fields = ['(reserved)', '(reserved)', 'Application AID', 'Privileges', 'MS parameters', 'MS token'];
|
||||||
|
} else if (p1val & 0x04) {
|
||||||
|
fields = ['ELF AID', 'Module AID', 'Application AID', 'Privileges', 'Install parameters', 'Install token'];
|
||||||
|
} else if (p1val & 0x02) {
|
||||||
|
fields = ['Load File AID', 'SD AID', 'LFDB hash', 'Load parameters', 'Load token'];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const nodes = [];
|
||||||
|
let i = 0;
|
||||||
|
for (const name of fields) {
|
||||||
|
if (i >= data.length) return null;
|
||||||
|
const f = readLvField(data, i);
|
||||||
|
if (!f) return null;
|
||||||
|
const node = {label: name, hex: f.lenHex + f.value, desc:''};
|
||||||
|
if (name === 'Privileges') node.desc = decodePrivileges(f.value);
|
||||||
|
else if (name.endsWith('parameters')) {
|
||||||
|
const walked = walkInstallParams(f.value);
|
||||||
|
node.children = walked || [{label:'Value', hex:f.value, desc:f.value}];
|
||||||
|
node.desc = f.value ? '' : '(empty)';
|
||||||
|
} else if (f.value) {
|
||||||
|
node.desc = f.value;
|
||||||
|
} else {
|
||||||
|
node.desc = '(empty)';
|
||||||
|
}
|
||||||
|
nodes.push(node);
|
||||||
|
i += f.consumed;
|
||||||
|
}
|
||||||
|
if (i !== data.length) return null;
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOneApdu(hex) {
|
||||||
|
hex = (hex || '').toUpperCase();
|
||||||
|
const isActivateDeactivate = hex.length >= 4 && (hex.substr(2, 2) === '44' || hex.substr(2, 2) === '04');
|
||||||
|
if (hex.length < 10 && !(isActivateDeactivate && hex.length === 8)) return null;
|
||||||
|
const cla = hex.substr(0, 2);
|
||||||
|
const ins = hex.substr(2, 2);
|
||||||
|
const p1 = hex.substr(4, 2);
|
||||||
|
const p2 = hex.substr(6, 2);
|
||||||
|
const p3 = hex.substr(8, 2);
|
||||||
|
const insInfo = PARSE_INS[ins];
|
||||||
|
const labels = describeP1P2(ins, p1, p2);
|
||||||
|
const children = [
|
||||||
|
{label:'CLA', hex:cla, desc:cla === '80' ? 'GP/RAM' : cla === 'A0' ? 'SIM' : parseInt(cla,16) >= 0x84 && parseInt(cla,16) <= 0x87 ? 'GlobalPlatform (secure messaging)' : 'UICC'},
|
||||||
|
{label:'INS', hex:ins, desc:insInfo ? insInfo.name : 'Unknown'},
|
||||||
|
{label:'P1', hex:p1, desc:labels.d1},
|
||||||
|
{label:'P2', hex:p2, desc:labels.d2},
|
||||||
|
];
|
||||||
|
if (isActivateDeactivate && hex.length === 8) {
|
||||||
|
return {label:'APDU (case 1)', hex:hex, desc:'no data, no Le', children};
|
||||||
|
}
|
||||||
|
if (ins === 'CA') {
|
||||||
|
if (hex.length === 10) {
|
||||||
|
children.push({label:'Le', hex:p3, desc:'Expected length: ' + parseInt(p3, 16)});
|
||||||
|
return {label:'APDU', hex:hex, desc:'', children};
|
||||||
|
}
|
||||||
|
const lc = parseInt(p3, 16);
|
||||||
|
children.push({label:'Lc', hex:p3, desc:'Data length: ' + lc});
|
||||||
|
const data = hex.substr(10, lc * 2);
|
||||||
|
children.push({label:'Data', hex:data, desc:data.startsWith('5C') && data.length >= 4 ? 'Tag list: ' + data : data});
|
||||||
|
const restLen = hex.length - (10 + lc * 2);
|
||||||
|
if (restLen >= 2) children.push({label:'Le', hex:hex.substr(10 + lc * 2, 2), desc:'Expected length: ' + parseInt(hex.substr(10 + lc * 2, 2), 16)});
|
||||||
|
return {label:'APDU', hex:hex, desc:'', children};
|
||||||
|
}
|
||||||
|
if (insInfo && insInfo.data === 'le') {
|
||||||
|
children.push({label:'Le', hex:p3, desc:'Expected length: ' + parseInt(p3, 16)});
|
||||||
|
} else if (isActivateDeactivate && hex.length >= 10) {
|
||||||
|
if (parseInt(p3, 16) === 0 && hex.length === 10) {
|
||||||
|
children.push({label:'P3', hex:p3, desc:'empty Lc (legacy form)'});
|
||||||
|
} else {
|
||||||
|
const alc = parseInt(p3, 16);
|
||||||
|
children.push({label:'Lc', hex:p3, desc:'Data length: ' + alc});
|
||||||
|
if (alc > 0 && hex.length >= 10 + alc * 2) {
|
||||||
|
const adata = hex.substr(10, alc * 2);
|
||||||
|
const adesc = adata.length === 4 ? 'FID ' + adata : (adata.length > 4 && adata.length % 2 === 0 ? 'Path ' + adata : adata);
|
||||||
|
children.push({label:'Data', hex:adata, desc:adesc});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (insInfo && insInfo.data === 'none') {
|
||||||
|
children.push({label:'P3', hex:p3, desc:'No data'});
|
||||||
|
} else {
|
||||||
|
const lc = parseInt(p3, 16);
|
||||||
|
children.push({label:'Lc', hex:p3, desc:'Data length: ' + lc});
|
||||||
|
if (lc > 0 && hex.length >= 10 + lc * 2) {
|
||||||
|
const data = hex.substr(10, lc * 2);
|
||||||
|
let dataDesc = data;
|
||||||
|
let dataChildren = null;
|
||||||
|
if (ins === 'E6') {
|
||||||
|
const lv = describeInstallDataLv(parseInt(p1, 16), data);
|
||||||
|
if (lv) { dataDesc = ''; dataChildren = lv; }
|
||||||
|
else dataDesc = data;
|
||||||
|
} else if (ins === 'F0') {
|
||||||
|
if (p1 === '80') dataDesc = 'ignored for ISD';
|
||||||
|
else if (data.startsWith('4F') && data.length >= 4 && 2 + 2 + parseInt(data.substr(2, 2), 16) * 2 === data.length) dataDesc = 'Legacy 4F-TLV AID: ' + data.substr(4);
|
||||||
|
else dataDesc = 'AID (raw): ' + data;
|
||||||
|
}
|
||||||
|
const node = {label:'Data', hex:data, desc:dataDesc};
|
||||||
|
if (dataChildren) node.children = dataChildren;
|
||||||
|
children.push(node);
|
||||||
|
if (hex.length > 10 + lc * 2) {
|
||||||
|
const le = hex.substr(10 + lc * 2, 2);
|
||||||
|
children.push({label:'Le', hex:le, desc:'Expected response length: ' + parseInt(le, 16)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const totalLen = hex.length;
|
||||||
|
return {label:'APDU', hex:hex.substr(0, totalLen), desc:'', children};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCompactApdus(hex) {
|
||||||
|
hex = (hex || '').toUpperCase();
|
||||||
|
const apdus = [];
|
||||||
|
let i = 0;
|
||||||
|
let prevCla = '';
|
||||||
|
while (i + 4 <= hex.length) {
|
||||||
|
const first = hex.substr(i, 2);
|
||||||
|
const second = hex.substr(i + 2, 2);
|
||||||
|
let fullCla = '';
|
||||||
|
let fullIns = '';
|
||||||
|
let consumed = 0;
|
||||||
|
const fb = parseInt(first, 16);
|
||||||
|
const knownCla = first === '80' || first === 'A0' || first === '00' || (fb >= 0x84 && fb <= 0x87);
|
||||||
|
if (knownCla && PARSE_INS[second]) {
|
||||||
|
fullCla = first;
|
||||||
|
fullIns = second;
|
||||||
|
consumed = 2;
|
||||||
|
} else if (PARSE_INS[first] && prevCla && i > 0) {
|
||||||
|
fullCla = prevCla;
|
||||||
|
fullIns = first;
|
||||||
|
consumed = 0;
|
||||||
|
}
|
||||||
|
if (fullCla) {
|
||||||
|
const p3Byte = parseInt(hex.substr(i + 6 + consumed, 2), 16);
|
||||||
|
const dataType = PARSE_INS[fullIns].data;
|
||||||
|
let apduLen = 8;
|
||||||
|
if (fullIns === 'CA') {
|
||||||
|
const remAvail = hex.length - (i + consumed);
|
||||||
|
if (p3Byte > 0 && remAvail >= p3Byte * 2 + 2) apduLen += p3Byte * 2 + 2;
|
||||||
|
} else if (fullIns === '44' || fullIns === '04') {
|
||||||
|
const avail = hex.length - i;
|
||||||
|
const p3Hex = avail >= consumed + 8 ? hex.substr(i + 6 + consumed, 2) : null;
|
||||||
|
if (p3Hex !== null) {
|
||||||
|
if (p3Byte > 0) apduLen += p3Byte * 2;
|
||||||
|
} else if (avail >= consumed + 6) {
|
||||||
|
apduLen -= 2;
|
||||||
|
}
|
||||||
|
} else if (dataType === 'lc') {
|
||||||
|
apduLen += p3Byte * 2;
|
||||||
|
}
|
||||||
|
if (i + consumed + apduLen <= hex.length) {
|
||||||
|
const apduHex = fullCla + hex.substr(i + consumed, apduLen);
|
||||||
|
const parsed = parseOneApdu(apduHex);
|
||||||
|
if (parsed) {
|
||||||
|
prevCla = fullCla;
|
||||||
|
parsed.hex = hex.substr(i, consumed + apduLen);
|
||||||
|
parsed.label = consumed === 0 ? 'APDU (implied CLA)' : 'APDU';
|
||||||
|
apdus.push(parsed);
|
||||||
|
i += consumed + apduLen;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apdus.push({label:'Unknown', hex:hex.substr(i, 2), desc:'Unrecognized'});
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
if (i < hex.length) {
|
||||||
|
if (apdus.length > 0 && hex.length - i === 2) {
|
||||||
|
const leHex = hex.substr(i);
|
||||||
|
apdus.push({label:'Le', hex:leHex, desc:'Expected response length: ' + parseInt(leHex, 16)});
|
||||||
|
} else {
|
||||||
|
apdus.push({label:'Trailing (unrecognized)', hex:hex.substr(i), desc:hex.substr(i)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apdus;
|
||||||
|
}
|
||||||
|
|
||||||
// Decrypt a ciphered secured packet block with the user-supplied key hex.
|
// Decrypt a ciphered secured packet block with the user-supplied key hex.
|
||||||
// Returns {cntr, pcntr, rc, data, pad} byte arrays; throws on bad key/size.
|
// Returns {cntr, pcntr, rc, data, pad} byte arrays; throws on bad key/size.
|
||||||
async function decryptSecured(sec, keyHex) {
|
async function decryptSecured(sec, keyHex) {
|
||||||
@@ -953,6 +1241,43 @@ function cmdRows(cmd) {
|
|||||||
rows.push(['Secured data (decrypted)', bytesToHex(dec.data)]);
|
rows.push(['Secured data (decrypted)', bytesToHex(dec.data)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Application message: the secured data is a C-APDU stream —
|
||||||
|
// plaintext when unciphered, decrypted content otherwise.
|
||||||
|
const appSrc = !sec.ciphered && sec.data ? {hex: sec.data, src: 'plaintext'}
|
||||||
|
: (sec.cipher_block && dec && !dec.error) ? {hex: bytesToHex(dec.data), src: 'decrypted'}
|
||||||
|
: null;
|
||||||
|
if (appSrc && appSrc.hex) {
|
||||||
|
const apdus = parseCompactApdus(appSrc.hex)
|
||||||
|
.filter(a => a.label !== 'Unknown');
|
||||||
|
if (apdus.length) {
|
||||||
|
rows.push(['Application message (' + appSrc.src + ')',
|
||||||
|
apdus.length + ' C-APDU' + (apdus.length > 1 ? 's' : '')]);
|
||||||
|
let n = 0;
|
||||||
|
for (const apdu of apdus) {
|
||||||
|
if (apdu.label === 'Le') {
|
||||||
|
rows.push(['· Le (trailing)',
|
||||||
|
(apdu.hex || '') + (apdu.desc ? ' — ' + apdu.desc : '')]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
const kids = apdu.children || [];
|
||||||
|
const insKid = kids.find(k => k.label === 'INS');
|
||||||
|
const name = insKid ? (insKid.desc || '') : apdu.label;
|
||||||
|
rows.push(['C-APDU #' + n + ' — ' + (name || apdu.label),
|
||||||
|
apdu.hex || '' + (apdu.label === 'APDU (implied CLA)' ? ' (implied CLA)' : '')]);
|
||||||
|
for (const k of kids) {
|
||||||
|
rows.push(['· ' + k.label,
|
||||||
|
(k.hex || '') + (k.desc ? ' — ' + k.desc : '')]);
|
||||||
|
if (k.children) for (const c of k.children) {
|
||||||
|
rows.push(['· · ' + c.label,
|
||||||
|
(c.hex || '') + (c.desc ? ' — ' + c.desc : '')]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rows.push(['Application message (' + appSrc.src + ')', appSrc.hex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rp = t.response_packet;
|
const rp = t.response_packet;
|
||||||
@@ -2126,7 +2451,7 @@ function onFilterChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────── Versioning ────────────────────
|
// ──────────────────── Versioning ────────────────────
|
||||||
const PWA_VERSION = '1.19.0';
|
const PWA_VERSION = '1.20.0';
|
||||||
const MIN_SERVER_VERSION = '1.0.0';
|
const MIN_SERVER_VERSION = '1.0.0';
|
||||||
|
|
||||||
function parseVersion(v) {
|
function parseVersion(v) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simtrace-analyser-v27';
|
const CACHE = 'simtrace-analyser-v28';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simtrace2-pysniff"
|
name = "simtrace2-pysniff"
|
||||||
version = "1.19.0"
|
version = "1.20.0"
|
||||||
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
|
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
|
||||||
readme = {file = "README.md", content-type = "text/markdown"}
|
readme = {file = "README.md", content-type = "text/markdown"}
|
||||||
license = {text = "GPL-2.0-or-later"}
|
license = {text = "GPL-2.0-or-later"}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver
|
|||||||
from .pcap import PcapWriter
|
from .pcap import PcapWriter
|
||||||
from .dump import FileDumper, format_message
|
from .dump import FileDumper, format_message
|
||||||
|
|
||||||
__version__ = '1.19.0'
|
__version__ = '1.20.0'
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'SniffSession',
|
'SniffSession',
|
||||||
'DeviceDisconnected',
|
'DeviceDisconnected',
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ class TestImportEndpoint(unittest.TestCase):
|
|||||||
class TestVersion(unittest.TestCase):
|
class TestVersion(unittest.TestCase):
|
||||||
def test_package_version(self):
|
def test_package_version(self):
|
||||||
from simtrace2_pysniff import __version__
|
from simtrace2_pysniff import __version__
|
||||||
self.assertEqual(__version__, '1.19.0')
|
self.assertEqual(__version__, '1.20.0')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in new issue
Block a user