Decoding
The decode* parameters (uid, ctr, picc_data, enc, cmac) are the SDM values parsed from the
tag’s URL query string (or text record). AES vs LRP is detected automatically from the PICC data length —
no hardware, pure JavaScript.
Every method returns the unified result; the decoded payload is nested under data:
{ success: true, data: { cmacValid, uid, counter, … } } // decode ran{ success: false, error: { code, message } } // hard failuresuccess: true only means the decode ran — authenticity is result.data.cmacValid, so always
check it (a tap can decode cleanly yet still fail CMAC verification). On a hard failure code is E400
for a bad argument (a missing, non-hex, or wrong-length field) or an unsatisfiable config (a required SDM
key set to 'free'), and E600 when the cryptographic decode itself could not be completed.
On success, result.data holds:
| Field | Type | Meaning |
|---|---|---|
cmacValid | boolean | CMAC verified — the proof of authenticity |
encryptionMode | string | 'AES' or 'LRP' (auto-detected) |
uid | string | Tag UID (hex, uppercase) |
counter | number | SDM read counter |
expectedCmac / receivedCmac | string | Computed vs supplied CMAC (hex) |
Detecting the Mode
detectEncryptionMode(piccData)
Detect 'AES' or 'LRP' from the PICC data length. piccData is a Buffer; throws a ValidationError
on an unsupported length.
const mode = decoder.detectEncryptionMode(piccData);Returns
'AES'Verifying a Tap
decodePlain(params)
Profile 1, plain mirroring (uid + ctr + cmac in cleartext). Tries AES, then LRP. Requires a
non-'free' sdmFileRead. params: uid (14 hex), ctr (6 hex), cmac (16 hex).
// from a URL like ...?uid=04..&ctr=000007&cmac=...const out = decoder.decodePlain({ uid, ctr, cmac });if (out.success && out.data.cmacValid) console.log('valid tap', out.data.counter);Returns
{ success: true, data: { cmacValid: true, encryptionMode: 'AES', uid: '04A1B2C3D4E580', counter: 7, expectedCmac: 'A1F9C7E03B5D8A42', receivedCmac: 'A1F9C7E03B5D8A42' }}decodeEncrypted(params)
Profile 2, encrypted PICCData + CMAC. Requires non-'free' sdmMetaRead and sdmFileRead. params:
picc_data, cmac (16 hex). On success data adds dataTag (a 0x-prefixed hex string) and a
debug object.
const out = decoder.decodeEncrypted({ picc_data, cmac });if (out.success && out.data.cmacValid) console.log(out.data.uid, out.data.dataTag);Returns
{ success: true, data: { cmacValid: true, encryptionMode: 'AES', uid: '04A1B2C3D4E580', counter: 7, expectedCmac: 'A1F9C7E03B5D8A42', receivedCmac: 'A1F9C7E03B5D8A42', dataTag: '0xC7', debug: { sv2: '3CC300010080D3F704A1B2C3D4E58070', sessionMacKey: 'B264A1F9C7E03B5D8A42D3F7A91C5E08', cmacFull: 'A1F9C7E03B5D8A42D3F7A91C5E08B264' } }}decodeFull(params)
Profile 3, encrypted PICCData + encrypted file data + CMAC. Requires non-'free' sdmMetaRead and
sdmFileRead. params: picc_data, enc (encrypted file data), cmac (16 hex), and an optional
cmacSeparator — the text the tag MACs between the enc and cmac values (readNDEF supplies it
automatically and it overrides the constructor’s sdmSettings.cmacSeparator). On success data adds
dataTag, fileData (UTF-8 string), fileDataHex (hex), and a debug object.
// values parsed from the tag's SDM URL query stringconst out = decoder.decodeFull({ picc_data, enc, cmac });if (out.success && out.data.cmacValid) { console.log(out.data.uid, out.data.counter, out.data.fileData);}Returns — large object, click to expand
{ success: true, data: { cmacValid: true, encryptionMode: 'AES', uid: '04A1B2C3D4E580', counter: 7, expectedCmac: 'A1F9C7E03B5D8A42', receivedCmac: 'A1F9C7E03B5D8A42', dataTag: '0xC7', fileData: 'HelloWorld!', fileDataHex: '48656C6C6F576F726C6421', debug: { sv1: 'C33C00010080D3F704A1B2C3D4E58070', sv2: '3CC300010080D3F704A1B2C3D4E58070', sessionEncKey: 'B264A1F9C7E03B5D8A42D3F7A91C5E08', sessionMacKey: '5E08B264A1F9C7E03B5D8A42D3F7A91C', cmacInput: '48656C6C6F576F726C6421', cmacFull: 'A1F9C7E03B5D8A42D3F7A91C5E08B264' } }}