I was writing Symmetric Cryptography (AES) with WebCrypto and Node.js and I was going to include the WebCrypto examples inline, right alongside the Node.js examples, but then this happened and I cried myself to sleep.

Then I woke up and finished the tutorial.

The Unicode Problem

Update 2021: There is now TextEncoder:

let buf = new TextEncoder().encode("👩‍🏫👨‍💻");
// Uint8Array(22)

Because there are no native utilities to convert between unicode, buffers, base64, and hex, and because it's 80 lines of code I don't want to show here, I'm assuming the use of Unibabel.js and Unibabel.hex.js.

See also

Live Demo

https://coolaj86.github.io/web-crypto-utahjs-preso/

WebCrypto Create & Import Key

AES requires strict 128-bit (or 256-bit) keys.

You can generate that key from random data (and store it, share it, etc) or you can derive it from a passphrase (scroll to the bottom).

'use strict';

var crypto = window.crypto;

// You can use crypto.generateKey or crypto.importKey,
// but since I'm always either going to share, store, or receive a key
// I don't see the point of using 'generateKey' directly
function generateKey(rawKey) {
  var usages = ['encrypt', 'decrypt'];
  var extractable = false;

  return crypto.subtle.importKey(
    'raw'
  , rawKey
  , { name: 'AES-CBC' }
  , extractable
  , usages
  );
}

See the appendix (down below) for Importing a JWK (JSON Web Key)

Demo

'use strict';

var sharedSecret = new Uint8Array(32);
var hexSecret;

crypto.getRandomValues(sharedSecret);
// efa7acd1646858f301c049d304896b3e017b824371683735c98568caee1eac65

hexSecret = Unibabel.bufferToHex(sharedSecret);

console.log(hexSecret);

WebCrypto Encrypt

'use strict';

var crypto = window.crypto;
var ivLen = 16; // the IV is always 16 bytes

function joinIvAndData(iv, data) {
  var buf = new Uint8Array(iv.length + data.length);
  Array.prototype.forEach.call(iv, function (byte, i) {
    buf[i] = byte;
  });
  Array.prototype.forEach.call(data, function (byte, i) {
    buf[ivLen + i] = byte;
  });
  return buf;
}

function encrypt(data, key) {
  // a public value that should be generated for changes each time
  var initializationVector = new Uint8Array(ivLen);

  crypto.getRandomValues(initializationVector);

  return crypto.subtle.encrypt(
    { name: 'AES-CBC', iv: initializationVector }
  , key
  , data
  ).then(function (encrypted) {
    var ciphered = joinIvAndData(initializationVector, new Uint8Array(encrypted))

    var base64 = Unibabel.bufferToBase64(ciphered)
      .replace(/\-/g, '+')
      .replace(/_/g, '\/')
      ;

    while (base64.length % 4) {
      base64 += '=';
    }
    return base64;
  });
}

Demo

'use strict';

var sharedSecret = Unibabel.hexToBuffer(hexSecret);
var base64Data;

generateKey(sharedSecret).then(function (key) {
  // "I ½ ♥ 💩";
  var arr = [73, 32, 194, 189, 32, 226, 153, 165, 32, 240, 159, 146, 169];
  return encrypt(new Uint8Array(arr), key).then(function (base64) {
    base64Data = base64;

    // cR9Q6YSq8LYlAsoIuZ/TyWd5AaUu5EKIL2/SwG1N0Mw=
    console.log(base64);
  });
});

WebCrypto Decrypt

'use strict';

var crypto = window.crypto;
var ivLen = 16; // the IV is always 16 bytes

function separateIvFromData(buf) {
  var iv = new Uint8Array(ivLen);
  var data = new Uint8Array(buf.length - ivLen);
  Array.prototype.forEach.call(buf, function (byte, i) {
    if (i < ivLen) {
      iv[i] = byte;
    } else {
      data[i - ivLen] = byte;
    }
  });
  return { iv: iv, data: data };
}

function decrypt(buf, key) {
  var parts = separateIvFromData(buf);

  return crypto.subtle.decrypt(
    { name: 'AES-CBC', iv: parts.iv }
  , key
  , parts.data
  ).then(function (decrypted) {
    var base64 = Unibabel.bufferToBase64(new Uint8Array(decrypted))
      .replace(/\-/g, '+')
      .replace(/_/g, '\/')
      ;

    while (base64.length % 4) {
      base64 += '=';
    }
    return base64;
  });
}

Demo

'use strict';

var sharedSecret = Unibabel.hexToBuffer(hexSecret);
var data = Unibabel.base64ToBuffer(base64Data);

generateKey(sharedSecret).then(function (key) {
  var ciphered = Unibabel.base64ToBuffer(base64Data);
  return decrypt(ciphered, key).then(function (base64) {
    var msg = Unibabel.base64ToUtf8(base64);
    console.log(msg);
  });
});

Appendix

Passphrase-Derived Key

PBKDF2, bcrypt, and scrypt are all algorithms for generating keys from passwords.

PBKDF2 is the most widely implemented and the most generic. It's only downfall (as far as I can tell) is that it can be accelerated on GPUs, bitcoin mining hardware, and other hardware on which you can accelerate hashing.

You need 3 pieces of information to derive a key:

  • the user's passphrase (16 characters long is very strong)
  • a non-private application-specific salt
  • a user-specific salt, if possible - such as the username
function deriveKey(saltBuf, passphrase) {
  var keyLenBits = 128;
  var kdfname = "PBKDF2";
  var aesname = "AES-CBC"; // AES-CTR is also popular
  // 100 - probably safe even on a browser running from a raspberry pi using pure js ployfill
  // 10000 - no noticeable speed decrease on my MBP
  // 100000 - you can notice
  // 1000000 - annoyingly long
  var iterations = 100; // something a browser on a raspberry pi or old phone could do
  var hashname = "SHA-256";
  var extractable = true;

  console.log('');
  console.log('passphrase', passphrase);
  console.log('salt (hex)', Unibabel.bufferToHex(saltBuf));
  console.log('iterations', iterations);
  console.log('keyLen (bytes)', keyLenBits / 8);
  console.log('digest', hashname);

  // First, create a PBKDF2 "key" containing the password
  return crypto.subtle.importKey(
    "raw",
    Unibabel.utf8ToBuffer(passphrase),
    { "name": kdfname },
    false,
    ["deriveKey"]).
  // Derive a key from the password
  then(function(passphraseKey){
    return crypto.subtle.deriveKey(
      { "name": kdfname
      , "salt": saltBuf
      , "iterations": iterations
      , "hash": hashname
      }
    , passphraseKey
      // required to be 128 (or 256) bits
    , { "name": aesname, "length": keyLenBits } // Key we want
    , extractable                               // Extractble
    , [ "encrypt", "decrypt" ]                  // For new key
    );
  }).
  // Export it so we can display it
  then(function(aesKey) {
    return crypto.subtle.exportKey("raw", aesKey).then(function (arrbuf) {
      return new Uint8Array(arrbuf);
    });
  }).
  catch(function(err) {
    window.alert("Key derivation failed: " + err.message);
  });
}

Demo

// Part of the salt is application-specific (same on iOS, Android, and Web)
var saltHex = '2618a03369d25a4bf216dd4136aa8a9cec15085a15d34ce9f21812f7b1e66863';
var saltBuf = Unibabel.hexToBuffer(saltHex);
var username = 'aj';
var nameBuf = Unibabel.utf8ToBuffer(username);
var passphrase = 'secret';

// we replace part of the salt with the username
// so that it becomes unique
Array.prototype.forEach.call(nameBuf, function (byte, i) {
  saltBuf[i] = byte;
});

// NOTE: the salt will be truncated to the length of the hash algo being used
deriveKey(saltBuf, passphrase).then(function (keyBuf) {
  var hexKey = Unibabel.bufferToHex(keyBuf);
  console.log(hexKey);
});

SHA-256 Hashsum

You can't use MD5 with webcrypto and although the attack vector is slim, something as simple as a little bit rot on an old drive can break it, so it's time to be using SHA algos anyway.

var text = "I ½ ♥ 💩";

var buf = Unibabel.utf8ToBuffer(text);
crypto.subtle.digest("SHA-256", buf).then(function (hashbuf) {
  var hex = Unibabel.bufferToHex(hashbuf);

  console.log(hex);
});

Import a Key as JWK

  // Alternate method of importing the key
  return crypto.subtle.importKey(
    'jwk'
  , { kty: 'oct'      // key-type: meaning octet-stream, meaning raw bytes
                      // key: base64-url-encoded bytes
    , k: Unibabel.bufferToBase64(rawKey)
          .replace(/\+/g, '-')
          .replace(/\//g, '_')
          .replace(/=/g, '')
    , alg: 'A128CBC'  // algorithm: no idea why this isn't AES-CBC
    , ext: true       // https://github.com/diafygi/webcrypto-examples/issues/9
    }
  , { name: 'AES-CBC' }
  , extractable
  , usages
  );

By AJ ONeal

If you loved this and want more like it, sign up!


Did I make your day?
Buy me a coffeeBuy me a coffee  

(you can learn about the bigger picture I'm working towards on my patreon page )