Porting node.js crypto code to SubtleCrypto (WebCrypto) fails with bad decrypt

197 views Asked by At

I'm trying to port working code decoding the values read from an NXP NTAG424 NFC tag in a web-based application. I have a working example that uses crypto in a node.js environment but when I try to port it so crypto.subtle it fails.

The issue seems to be the decipher.setAutoPadding(false), which when removed in the crypto example below causes an error when calling .final(). This is strange because the payload being decrypted, ENC, is 32 bytes (hex) and the encrypted source coming from the NTAG424 IC is 16 bytes according to documentation (4.4.2.1):

Random padding generated by the PICC to make the input 16 bytes long. It is only relevant if SDMReadCtr is not mirrored, as SDMReadCtr adds uniqueness already

Constants shared between both approachs:

const KEY = '00000000000000000000000000000000'
const ENC = 'EF963FF7828658A599F3041510671E88'
const IV = Buffer.alloc(16)

I don't understand why the padding is throwing an error, assuming it's the same issue with both the cryptoand crypto.subtle approaches, as the source data is exactly 16 bytes, but searches on this have yielded no clear solution.

Thanks for any insights on how to get this to work with SubtleCrypto.

1

There are 1 answers

3
Topaco On BEST ANSWER

Apparently no padding was used during encryption (this is possible whenever the length of the plaintext already corresponds to an integer multiple of the AES block size, i.e. 16 bytes) or one that differs from PKCS#7 padding. If decryption is then carried out with PKCS#7 padding enabled, the bytes at the end do not match the specified padding, which leads to the error message. For correct decryption, the padding must be disabled in this case.

WebCrypto uses PKCS#7 padding by default and does not allow you to disable padding. Therefore, WebCrypto might not be the best choice for this task.

But if you absolutely have to use WebCrypto, there is a workaround: You have to append the missing block with the encrypted padding before decryption.
In CBC mode, when encrypting a block, the previous block is used as IV, i.e. to determine the required block, you only need to encrypt an empty block and use your ciphertext as IV (as your ciphertext consists of only one block).

The following code does this and can decrypt the ciphertext:

(async () => {

const KEY = '00000000000000000000000000000000'
const ENC = 'EF963FF7828658A599F3041510671E88'
const IV = new Uint8Array(16)

async function testSubtleCrypto() {
    const keyBuffer = Uint8Array.from(hex2ab(KEY, 'hex'))
    const cipherBuffer = Uint8Array.from(hex2ab(ENC, 'hex'))

    const cryptoKey = await crypto.subtle.importKey(
        'raw',
        keyBuffer,
        {
            name: 'AES-CBC',
            length: 128,
        },
        false,
        ['encrypt', 'decrypt']
    )

    try {
        const encPaddingBlock = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: cipherBuffer }, cryptoKey, new Uint8Array());
        const cipherBufferPadded = concat(cipherBuffer, new Uint8Array(encPaddingBlock));
    
        const deciphered = await crypto.subtle.decrypt(
            {
                name: 'AES-CBC',
                length: 128,
                iv: IV,
            },
            cryptoKey,
            cipherBufferPadded
        )
        return deciphered
    } catch (error) {
        console.dir(error)
        console.log(error.message)
    }
}

const data = await testSubtleCrypto();
console.log({
    dataTag: ab2hex(data.slice(0, 1)),
    uid: ab2hex(data.slice(1, 8)),
    cnt: ab2hex(data.slice(8, 11)),
    cntInt: readInt(data.slice(8, 11)),
});

// 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 readInt(array) {
    array = new Uint8Array(array)
    var value = 0;
    for (var i = array.length - 1; i >= 0; i--) {
        value = (value * 256) + array[i];
    }
    return value;
}

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('');
}

})();