WebCrypto: Encrypt and Decrypt with AES
Published 2015-6-27I 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
- Unicode, UTF-8, TypeArray, Uint8Array, JavaScript, and You
- Direct TypedArray to Base64 JavaScript conversion
- Direct Unicode to Uint8Array JavaScript conversion
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
Did I make your day?
Buy me a coffee
(you can learn about the bigger picture I'm working towards on my patreon page )