using web crypto to decrypt AES-CBC: Uncaught (in promise) Error

85 Views Asked by At

Here's my code:

var key = 'aaaaaaaaaaaaaaaa'
var iv = 'bbbbbbbbbbbbbbbb';
var ciphertext = '10f42fd95857ed2775cfbc4b471bc213';

// from https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#PKCS_8_import
function str2ab(str) {
    const buf = new ArrayBuffer(str.length);
    const bufView = new Uint8Array(buf);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return buf;
}

key = new TextEncoder().encode(key);
iv = new TextEncoder().encode(iv);
ciphertext = str2ab(ciphertext);

window.crypto.subtle.importKey(
    'raw',
    key,
    {
        name: 'AES-CBC'
    },
    true, // can the key be extracted using SubtleCrypto.exportKey() / SubtleCrypto.wrapKey()?
    ['decrypt'] // keyUsages
).then(function(key) {
    window.crypto.subtle.decrypt(
        {
            name: "AES-CBC",
            iv: iv
        },
        key,
        ciphertext
    ).then(function(plaintext) {
        console.log(new TextDecoder().decode(plaintext));
    })
});

When I run it I get Uncaught (in promise) Error in the JS Console.

Here's a JSFiddle showing the error:

https://jsfiddle.net/96gx7hz3/

Any ideas?

1

There are 1 best solutions below

0
Topaco On BEST ANSWER

The ciphertext is hex encoded and must therefore be hex decoded, e.g. with the following helper function:

function hex2ab(hex){
    return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {return parseInt(h, 16)}));
}

The str2ab() function used in the code is commonly applied for a latin1 encoding, which however would be wrong in this case.

With this change, decryption works:

var key = 'aaaaaaaaaaaaaaaa'
var iv = 'bbbbbbbbbbbbbbbb';
var ciphertext = '10f42fd95857ed2775cfbc4b471bc213';

/*
// from https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#PKCS_8_import
function str2ab(str) {
    const buf = new ArrayBuffer(str.length);
    const bufView = new Uint8Array(buf);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return buf;
}
*/
function hex2ab(hex){
    return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {return parseInt(h, 16)}));
}

key = new TextEncoder().encode(key);
iv = new TextEncoder().encode(iv);
//ciphertext = str2ab(ciphertext);
ciphertext = hex2ab(ciphertext); // Fix: Replcae latin1 encoding with hex decoding

window.crypto.subtle.importKey(
    'raw',
    key,
    {
        name: 'AES-CBC'
    },
    true, // can the key be extracted using SubtleCrypto.exportKey() / SubtleCrypto.wrapKey()?
    ['decrypt'] // keyUsages
).then(function(key) {
    window.crypto.subtle.decrypt(
        {
            name: "AES-CBC",
            iv: iv
        },
        key,
        ciphertext
    ).then(function(plaintext) {
        console.log(new TextDecoder().decode(plaintext));
    })
});


A note on key and IV: In the code, the key material is UTF-8 encoded and used directly as key. This is fine for testing purposes as here, but in general a key should be generated as a random byte sequence or, if a passphrase is used, derived using a key derivation function in conjunction with a random salt.
Likewise, to avoid reusing key/IV pairs, no static IV may be applied. Instead, a random IV is commonly generated for each encryption and passed along with the ciphertext.