这是上一个问题的后续问题,尝试从 NXP NTAG424 标签根据 NXP (4.4.2.1) 的文档获取 NDEF URL 的有效解密
。
我已经根据 @Topaco 的出色建议编写了一个初始 WebCrypto 代码,该代码现在成功解码了响应中的
picc
和
enc
并验证了 CMAC。
但是,这仅适用于某些有效负载,而其他有效负载仍然会抛出
bad decrypt
错误。我将问题隔离为使用来自
enc
的派生密钥解密
sv1
静态数据负载,请在参考代码中搜索
encDataFull
。完整的实现遵循Python中的参考,这也是我稍后提到的在线验证服务的后端: https://github.com/nfc-developer/sdm-backend/blob/master/libsdm/sdm.py
唯一的区别是在编码
AES-CBC
时使用
AES-ECB
与
encIVFull
,因为 WebCrypto 不支持
AES-ECB
。,我了解到 IV 为 0 的
AES-CBC
将等于
AES-ECB
,因为只需要第一个块:
const encIV = Buffer.from(encIVFull).subarray(0, 16)
对于 IV 为零,ECB / CBC 的参考相同: CBC 示例 欧洲央行示例
更新后的解码代码:
async function decipherTag() {
const keyBuffer = Uint8Array.from(Buffer.from(KEY, 'hex'))
const piccBuffer = Uint8Array.from(Buffer.from(PICC, 'hex'))
const encBuffer = Uint8Array.from(Buffer.from(ENC, 'hex'))
const cmacArgBuffer = Buffer.from(`${ENC}&${CMAC_ARG}=`)
/*
Step 1 - decrypt PICC data (UID + tap counter)
*/
// Import crypto key from key buffer
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBuffer,
{
name: 'AES-CBC',
length: 128,
},
true,
['encrypt', 'decrypt']
)
// Encrypt padding block to generate padding block
const piccPaddingBlock = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: piccBuffer }, cryptoKey, new Uint8Array())
// create cipher buffer with padding block
const piccPadded = concat(piccBuffer, new Uint8Array(piccPaddingBlock))
// Decrypt
const deciphered = await crypto.subtle.decrypt(
{
name: 'AES-CBC',
iv: IV,
},
cryptoKey,
piccPadded
)
// extract PICC data from deciphered buffer
const picc = Buffer.from(deciphered)
const tag = picc.slice(0, 1)
const uid = picc.subarray(1, 8)
const cnt = picc.subarray(8, 11)
// subbuffer from 1 to 11 for later calculcations (drop tag data)
const piccData = picc.subarray(1, 11)
/*
Step 2 - decrypt static data
*/
const sv1Data = Buffer.alloc(16)
SV1.copy(sv1Data, 0)
piccData.copy(sv1Data, 6)
const keyAesCmac = new AesCmac(keyBuffer)
const dataKeyRaw = await keyAesCmac.calculate(sv1Data)
const dataIV = Buffer.alloc(16)
cnt.copy(dataIV, 0)
const dataKey = await crypto.subtle.importKey(
'raw',
dataKeyRaw,
{
name: 'AES-CBC',
length: 128,
},
true,
['encrypt', 'decrypt']
)
const encIVFull = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: IV }, dataKey, dataIV)
const encIV = Buffer.from(encIVFull).subarray(0, 16)
const encPaddingBlock = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: encIV }, dataKey, new Uint8Array())
const encPadded = concat(encBuffer, new Uint8Array(encPaddingBlock))
const encDataFull = await crypto.subtle.decrypt(
{
name: 'AES-CBC',
iv: encIV,
},
dataKey,
encPadded
)
const encData = Buffer.from(encDataFull).subarray(0, 16)
/*
Step 3 - validate CMAC
*/
// construct SV2 buffer with PICC data
const sv2Data = Buffer.alloc(16)
SV2.copy(sv2Data, 0)
piccData.copy(sv2Data, 6)
// calculate SV2 CMAC
// const keyAesCmac = new AesCmac(keyBuffer)
const sv2 = await keyAesCmac.calculate(sv2Data)
// calculate full CMAC of SV2 and encrypted data
const sv2AesCmac = new AesCmac(sv2)
const fullCmac = await sv2AesCmac.calculate(cmacArgBuffer)
const fullCmacBuffer = Buffer.from(fullCmac)
const cmac = Buffer.alloc(8)
for (let i = 0; i < 8; i++) fullCmacBuffer.copy(cmac, i, i * 2 + 1, i * 2 + 2)
return {
dataTag: tag.toString('hex'),
uid: uid.toString('hex').toUpperCase(),
cnt: cnt.toString('hex'),
cntInt: picc.readUIntLE(8, 3),
cmacPass: Buffer.from(cmac).toString('hex').toUpperCase() === CMAC,
data: encData.toString('hex'),
}
}
使用此功能,有些有效负载可以工作,有些则失败。尽管事实上它们都通过了在线实用程序的验证(也提供了)
工作负载(在线验证链接
):// https://sdm.nfcdeveloper.com/tagtt?_____TRIAL_VERSION______NOT_FOR_PRODUCTION_____&picc_data=FDE4AFA99B5C820A2C1BB0F1C792D0EB&enc=94592FDE69FA06E8E3B6CA686A22842B&cmac=C48B89C17A233B2C
const KEY = '00000000000000000000000000000000'
const PICC = 'FDE4AFA99B5C820A2C1BB0F1C792D0EB'
const ENC = '94592FDE69FA06E8E3B6CA686A22842B'
const CMAC = 'C48B89C17A233B2C'
const CMAC_ARG = 'cmac'
退货
{"dataTag":"c7","uid":"04958CAA5C5E80","cnt":"010000","cntInt":1,"cmacPass":true,"data":"78787878787878787878787878787878"}
有效负载失败(在线验证链接
):// https://sdm.nfcdeveloper.com/tagtt?_____TRIAL_VERSION______NOT_FOR_PRODUCTION_____&picc_data=6107DD7607B179270EAFFCA2F0911940&enc=37C1E399E0948BEA54138F92DDD1E743&cmac=0406016621FC6AC6
const KEY = '00000000000000000000000000000000'
const PICC = '6107DD7607B179270EAFFCA2F0911940'
const ENC = '37C1E399E0948BEA54138F92DDD1E743'
const CMAC = '0406016621FC6AC6'
const CMAC_ARG = 'cmac'
失败并显示
bad decrypt
另一个失败的有效负载(在线验证链接
):// https://sdm.nfcdeveloper.com/tagtt?_____TRIAL_VERSION______NOT_FOR_PRODUCTION_____&picc_data=FD91EC264309878BE6345CBE53BADF40&enc=CEE9A53E3E463EF1F459635736738962&cmac=ECC1E7F6C6C73BF6
const KEY = '00000000000000000000000000000000'
const PICC = 'FD91EC264309878BE6345CBE53BADF40'
const ENC = 'CEE9A53E3E463EF1F459635736738962'
const CMAC = 'ECC1E7F6C6C73BF6'
const CMAC_ARG = 'cmac'
如果您能进一步了解如何使解密代码更加健壮,我们将不胜感激。
该错误是解决方法中对假填充块的错误计算。您使用 final
IV 作为 IV,而不是
ENC
密文,这是错误的。
错误的 IV 会导致错误的填充块。解密整个密文时,这会在明文末尾产生内部随机字节,这通常对应于不兼容的 PKCS#7 填充(提醒一下:PKCS#7 是 WebCrypto 使用的填充,无法禁用)。
然后这会触发
bad decrypt
(如您的两个失败案例中所示)。然而,偶然情况下,也可能会导致兼容的填充,从而不会生成
bad decrypt
(如您的成功案例所示),但由于填充字节不正确,数据通常仍会被后续处理损坏。下面提醒您这篇文章
中的解决方法是如何工作的:如果您有一个明文,其中最后一个块
pt_block_last
已完全填充,则在以下情况下会自动附加一个由16个0x10值组成的完整块
pt_padding
: (PKCS#7) 启用填充,使用 AES/CBC 加密,其中
ct_block_last
用作 IV。后者是因为 CBC 在加密明文块
ct_block_n-1
时将前一个块
pt_block_n
的密文应用为 IV,参见 CBC 流程图ENC
密文的最后一个块(或者在示例中为整个 ENC
密文,因为这只有 1 个块大小)作为 IV,加密会生成一个密文块,其中包含加密的填充并与 ENC
匹配密文。
ENC
和该密文块的串联会生成一个两块密文,在启用填充时可以使用 WebCrypto 对其进行解密。
您的实现已经生成了正确的最终 IV 和密钥,以便使用 CMAC 进行解密。我用您的代码计算了这两个值,并用它们来测试下面的
解密代码:
(async () => {
async function decryptPayload(keyHex, ivHex, encHex) {
const key = hex2ab(keyHex, 'hex')
const iv = hex2ab(ivHex, 'hex')
const enc = hex2ab(encHex, 'hex')
const cryptoKey = await crypto.subtle.importKey(
'raw',
key,
{
name: 'AES-CBC',
length: 128,
},
true,
['encrypt', 'decrypt']
)
const encPaddingBlock = await crypto.subtle.encrypt(
{
name: 'AES-CBC',
iv: enc.subarray(-16) // Fix: Apply enc
},
cryptoKey,
new Uint8Array()
)
const encPadded = concat(enc, new Uint8Array(encPaddingBlock))
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-CBC',
iv: iv,
},
cryptoKey,
encPadded
)
return decrypted
}
const decoder = new TextDecoder()
// 1
var ENC = '94592FDE69FA06E8E3B6CA686A22842B'
var IV_CMAC = '7b3f3cfc39d3b7ff5868636e38af7c3a'
var KEY_CMAC = '8097d73344d53f963b09e23e03b62336'
var decrypted = await(decryptPayload(KEY_CMAC, IV_CMAC, ENC))
console.log(ab2hex(decrypted))
console.log(decoder.decode(decrypted))
// 2
var ENC = '37C1E399E0948BEA54138F92DDD1E743'
var IV_CMAC = '771aed39fe43c881a9c616fb270be958'
var KEY_CMAC = 'bf3942b425b3b633146182f12d020999'
var decrypted = await(decryptPayload(KEY_CMAC, IV_CMAC, ENC));
console.log(ab2hex(decrypted))
console.log(decoder.decode(decrypted))
// 3
var ENC = 'CEE9A53E3E463EF1F459635736738962'
var IV_CMAC = '0ad3eb2717a58332cbc8899bbecbd411'
var KEY_CMAC = '42132d669442ad43e072c8c0c9828a72'
var decrypted = await(decryptPayload(KEY_CMAC, IV_CMAC, ENC));
console.log(ab2hex(decrypted))
console.log(decoder.decode(decrypted))
// helper
function concat(a, b) {
var c = new (a.constructor)(a.length + b.length);
c.set(a, 0);
c.set(b, a.length);
return c;
}
function hex2ab(hex){
return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {return parseInt(h, 16)}));
}
function ab2hex(ab) {
return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}
})();
ENC