使用 WebCrypto 解密 NTAG424 加密数据偶尔会失败

问题描述 投票:0回答:1

这是上一个问题的后续问题,尝试从 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'

如果您能进一步了解如何使解密代码更加健壮,我们将不胜感激。
javascript encryption aes webcrypto-api subtlecrypto
1个回答
0
投票

该错误是解决方法中对假填充块的错误计算。您使用 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 流程图
解决方法正是这样做的:当加密一个空块时,启用的 (PKCS#7) 填充会自动生成一个由 16 个 0x10 值组成的完整填充块。通过使用 ENC 密文的最后一个块(或者在示例中为整个 ENC 密文,因为这只有 1 个块大小)作为 IV,加密会生成一个密文块,其中包含加密的填充并与 ENC 匹配
 密文。
ENC
和该密文块的串联会生成一个两块密文,在启用填充时可以使用 WebCrypto 对其进行解密。


测试:
您的实现已经生成了
正确

的最终 IV 和密钥,以便使用 CMAC 进行解密。我用您的代码计算了这两个值,并用它们来测试下面的
fixed

解密代码:

(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
密文现已正确解密。

© www.soinside.com 2019 - 2024. All rights reserved.