Home > Software engineering >  How to Decrypt AES SubtleCrypto Web API at SJCL Library
How to Decrypt AES SubtleCrypto Web API at SJCL Library

Time:12-13

We have an Expo React Native Project utilizing encryption. Our current encryption is based on SubtleCrypto / Web API [window.subtle.crypto], using AES-GCM 128, now we need to use a library that is universally available on all platforms [Web, iOS and Android], from my previous question, we've found SJCL that supports GCM mode and we can completely replace all the web-only based code BUT the challenge is that we need to ensure that all the current encrypted data is decrypted at this new library too, we have to make it so:

window.crypto.subtle.encrypt [AES-GCM 128] => (a) ---> SJCL.mode.gcm.decrypt(a)

Once we can do that successfully, we can fully replace the library and have universal platform support as well as backwards compatibility.

This means that we cannot change the way encryption is handled at all, as that is the requirement, and we're encrypting it exactly as the code below.

I got a very good lead here by Neneil94 but I'm still facing issues at encoding / formats; and here's the current code:

function arrayBufferToString(buffer){
    var str = "";
    for (var iii = 0; iii < buffer.byteLength; iii  ){
        str  = String.fromCharCode(buffer[iii]);
    }
    return str;
}

const generateKey = async () =>
{
    const key = await window.crypto.subtle.generateKey(
    {
        name: "AES-GCM",
        length: 128
    }, true, ["encrypt", "decrypt"]);
    const key_exported = await window.crypto.subtle.exportKey("jwk", key);
    return key_exported.k;
}

const text = "This is an encrypted message";
const printCurrent = async () =>
{
    let kkey = await generateKey();
    await window.crypto.subtle.importKey(
        "jwk",
        {
            k: kkey,
            alg: "A128GCM",
            ext: true,
            key_ops: ["encrypt", "decrypt"],
            kty: "oct",
        },
        {
            name: "AES-GCM",
            length: 128
        },
        false,
        ["encrypt", "decrypt"]
    ).then(function(key)
    {
        window.crypto.subtle.encrypt(
        {
            name: "AES-GCM",
            iv: new Uint8Array(12)
        }, key, new TextEncoder().encode(JSON.stringify(text))).then(function(encryptedObject)
        {
            console.log({kkey}); //{kkey: 'eKM_Cen2Z-jhedM284cltA'}

            let bkey = sjcl.codec.utf8String.toBits(kkey);
            console.log({bkey}); //{bkey: Array(6)}
            let cipher = new sjcl.cipher.aes(bkey);
            //let bdata = sjcl.codec.base64.toBits(encryptedObject); //gives error of a.replace
            let bdata = arrayBufferToString(encryptedObject);
            let ivvv = new Uint8Array(12);
            let ivv = Buffer.from(ivvv).toString('base64');
            let ive = sjcl.codec.base64.toBits(ivv);

            // decrypt
            let decbits = sjcl.mode.gcm.decrypt(cipher, bdata, ive);

            // convert into utf8string
            decryptedData = sjcl.codec.utf8String.fromBits(decbits);
        });
    });
}

the error I'm getting is:

CORRUPT: gcm: tag doesn't match

I'm uncertain what precisely is aesKey at Neneil's code linked above, is it the CryptoKey object or the 22 character being generated from JsonWebKey? And for 'encryptedObject' how I'd be able to convert it to BitArray. For the IV, I'm just providing what's being done at Encrypt but I'm unsure about it too..

Nonetheless, any insight would be quite helpful, thanks for your time!

CodePudding user response:

There are two problems in your code:

  • kkey is the Base64url encoded raw key. This must first be converted to Base64 and then to a bitArray:
let kkeyB64 = kkey.replace(/-/g, ' ').replace(/_/g, '/');   // Base64url -> Base64 (ignore optional padding)
let bkey = sjcl.codec.base64.toBits(kkeyB64);               // conert to bitArray
  • Ciphertext and IV are given as ArrayBuffer, so it makes sense to use the ArrayBuffer codec, e.g.:
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/core/codecArrayBuffer.js"></script>
let bdata = sjcl.codec.arrayBuffer.toBits(encryptedObject)
let ive = sjcl.codec.arrayBuffer.toBits(new Uint8Array(12).buffer)

With these changes, decryption with sjcl is successful.


Full code:

const generateKey = async () =>
{
    const key = await window.crypto.subtle.generateKey(
    {
        name: "AES-GCM",
        length: 128
    }, true, ["encrypt", "decrypt"]);
    const key_exported = await window.crypto.subtle.exportKey("jwk", key);
    return key_exported.k;
}

const text = "This is an encrypted message";
const printCurrent = async () =>
{
    let kkey = await generateKey();
    await window.crypto.subtle.importKey(
        "jwk",
        {
            k: kkey,
            alg: "A128GCM",
            ext: true,
            key_ops: ["encrypt", "decrypt"],
            kty: "oct",
        },
        {
            name: "AES-GCM",
            length: 128
        },
        false,
        ["encrypt", "decrypt"]
    ).then(function(key)
    {
        window.crypto.subtle.encrypt(
        {
            name: "AES-GCM",
            iv: new Uint8Array(12)
        }, key, new TextEncoder().encode(JSON.stringify(text))).then(function(encryptedObject)
        {
            //console.log({kkey}); //{kkey: 'eKM_Cen2Z-jhedM284cltA'}

            let kkeyB64 = kkey.replace(/-/g, ' ').replace(/_/g, '/'); // Fix 1
            let bkey = sjcl.codec.base64.toBits(kkeyB64); 
            //console.log({bkey}); //{bkey: Array(6)}
            let cipher = new sjcl.cipher.aes(bkey);
            
            
            let bdata = sjcl.codec.arrayBuffer.toBits(encryptedObject) // Fix 2
            let ive = sjcl.codec.arrayBuffer.toBits(new Uint8Array(12).buffer) // Fix 3
            
            // decrypt
            let decbits = sjcl.mode.gcm.decrypt(cipher, bdata, ive);

            // convert into utf8string
            decryptedData = sjcl.codec.utf8String.fromBits(decbits);
            document.getElementById("pt").innerHTML = JSON.parse(decryptedData);
        });
    });
}

printCurrent();
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/sjcl.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/core/codecArrayBuffer.js"></script>
<p style="font-family:'Courier New', monospace;" id="pt"></p>


If the ArrayBuffer codec is not available in your environment for some reason (I'm not sure about that), then an ArrayBuffer can also be explicitly hex or Base64 encoded and then converted to a bitArray, e.g.:

// https://stackoverflow.com/a/40031979/9014097
function buf2hex(buffer) { 
    return Array.prototype.map.call(new Uint8Array(buffer), x => ('00'   x.toString(16)).slice(-2)).join('');
}
...
let bdata = sjcl.codec.hex.toBits(buf2hex(encryptedObject)) 
let ive = sjcl.codec.hex.toBits(buf2hex(new Uint8Array(12).buffer)) 

Security:
A static IV (as here with new Uint8Array(12)) is a fatal bug in the context of GCM, s. e.g. here. Instead, a random IV is to be generated for each encryption, which is to be passed together with the ciphertext (usually concatenated). So unless the static IV is only used for testing purposes, this must be fixed.

  • Related