Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

window.crypto returns 352 bit key instead of 256?

Tags:

I'm trying to encrypt some text using window.crypto:

await crypto.subtle.encrypt(algorithm, key, dataArrayBuffer).catch(error => console.error(error));

However I get this error AES key data must be 128 or 256 bits. I'm using PBKDF2 to create a 256 bit key from a password and I specify a key length of 256:

window.crypto.subtle.deriveKey(
            {
                "name": "PBKDF2",
                "salt": salt,
                "iterations": iterations,
                "hash": hash
            },
            baseKey,
            {"name": "AES-GCM", "length": 256}, //<------------
            true, 
            ["encrypt", "decrypt"]
            );

But I end up getting this key edi5Fou4yCdSdx3DX3Org+L2XFAsVdomVgpVqUGjJ1g= after I exportKey it and convert it from an ArrayBuffer to a string with a length of 44 bytes and 352 bits...

Which would explain the error, but how can I create an actual 256 bit key from window.crypto's PBKDF2?

JSFiddle: https://jsfiddle.net/6Lyaoudc/1/

like image 753
now_world Avatar asked Aug 08 '20 07:08

now_world


1 Answers

The displayed lengths refer to the Base64 encoded key. A 32 bytes key has a Base64 encoded length of 44 bytes (352 bits) in accordance with the displayed values, which are therefore correct, see e.g. here.

The bug is that in the 2nd importKey (see comment Create CryptoKey for AES-GCM from import) the Base64 encoded key (i.e. keyString) is passed instead of the binary key (i.e. the ArrayBuffer keyBytes). If keyBytes is passed, the encryption works:

function deriveAKey(password, salt, iterations, hash) {

    // First, create a PBKDF2 "key" containing the password
    window.crypto.subtle.importKey(
        "raw",
        stringToArrayBuffer(password),
        {"name": "PBKDF2"},
        false,
        ["deriveKey"]).
    then(function(baseKey){
        // Derive a key from the password
        return window.crypto.subtle.deriveKey(
            {
                "name": "PBKDF2",
                "salt": salt,
                "iterations": iterations,
                "hash": hash
            },
            baseKey,
            {"name": "AES-GCM", "length": 256}, // Key we want.Can be any AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH", or "HMAC")
            true,                               // Extractable
            ["encrypt", "decrypt"]              // For new key
            );
    }).then(function(aesKey) {
        // Export it so we can display it
        return window.crypto.subtle.exportKey("raw", aesKey);
    }).then(async function(keyBytes) {
        // Display key in Base64 format
        var keyS = arrayBufferToString(keyBytes);
        var keyB64 = btoa (keyS);
        console.log(keyB64);
        
        console.log('Key byte size: ', byteCount(keyB64));
        console.log('Key bit size: ', byteCount(keyB64) * 8);
        
        var keyString = stringToArrayBuffer(keyB64);
    
        const iv = window.crypto.getRandomValues(new Uint8Array(12));

        const algorithm = {
          name: 'AES-GCM',
          iv: iv,
        };

        //Create CryptoKey for AES-GCM from import
        const key = await crypto.subtle.importKey(
          'raw',//Provided key will be of type ArrayBuffer
          // keyString,
          keyBytes,                                                    // 1. Use keyBytes instead of keyString
          {
            name: "AES-GCM",
          },
          false,//Key not extractable
          ['encrypt', 'decrypt']
        );

        //Convert data to ArrayBuffer
        var data = "The quick brown fox jumps over the lazy dog";      // 2. Define data
        const dataArrayBuffer = new TextEncoder().encode(data);

        const encryptedArrayBuffer = await crypto.subtle.encrypt(algorithm, key, dataArrayBuffer).catch(error => console.error(error));
        
        var ivB64 = btoa(arrayBufferToString(iv));
        console.log(ivB64);                                            // 3. Display (Base64 encoded) IV
        
        var encB64 = btoa(arrayBufferToString(encryptedArrayBuffer));
        console.log(encB64.replace(/(.{56})/g,'$1\n'));                // 4. Display (Base64 encoded) ciphertext (different for each encryption, because of random salt and IV)

    }).catch(function(err) {
        console.error("Key derivation failed: " + err.message);
    });
}

//Utility functions

function stringToArrayBuffer(byteString){
    var byteArray = new Uint8Array(byteString.length);
    for(var i=0; i < byteString.length; i++) {
        byteArray[i] = byteString.codePointAt(i);
    }
    return byteArray;
}

function  arrayBufferToString(buffer){
    var byteArray = new Uint8Array(buffer);
    var byteString = '';
    for(var i=0; i < byteArray.byteLength; i++) {
        byteString += String.fromCodePoint(byteArray[i]);
    }
    return byteString;
}

function byteCount(s) {
    return encodeURI(s).split(/%..|./).length - 1;
}

var salt = window.crypto.getRandomValues(new Uint8Array(16));
var iterations = 5000;
var hash = "SHA-512";
var password = "password";

deriveAKey(password, salt, iterations, hash);

In addition to the key, the code also displays IV and ciphertext, each Base64 encoded. The ciphertext can be verified e.g. here using the Base64 encoded key and IV.

like image 125
Topaco Avatar answered Oct 02 '22 15:10

Topaco