Search code examples
javascriptaeswebcrypto-api

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


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?


Solution

  • 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.