Skip to content

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 failure

success: 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:

FieldTypeMeaning
cmacValidbooleanCMAC verified — the proof of authenticity
encryptionModestring'AES' or 'LRP' (auto-detected)
uidstringTag UID (hex, uppercase)
counternumberSDM read counter
expectedCmac / receivedCmacstringComputed 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 string
const 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'
}
}
}