FEATURE: Use AES-GCM instead of AES-CBC.

FEATURE: Use AES-GCM instead of AES-CBC.
From 134702390b87b7e8b5b88d895df5ddf3636b3ca2 Mon Sep 17 00:00:00 2001
From: Dan Ungureanu <dan@ungureanu.me>
Date: Thu, 6 Dec 2018 00:20:21 +0200
Subject: [PATCH] FEATURE: Use AES-GCM instead of AES-CBC.


diff --git a/assets/javascripts/lib/keys.js.es6 b/assets/javascripts/lib/keys.js.es6
index dbd067e..0593e78 100644
--- a/assets/javascripts/lib/keys.js.es6
+++ b/assets/javascripts/lib/keys.js.es6
@@ -75,9 +75,9 @@ export function importPublicKey(publicKey) {
  * @return Promise<String>
  */
 export function exportPrivateKey(privateKey, key) {
-  const iv = window.crypto.getRandomValues(new Uint8Array(16));
+  const iv = window.crypto.getRandomValues(new Uint8Array(12));
   return window.crypto.subtle
-    .wrapKey("jwk", privateKey, key, { name: "AES-CBC", iv })
+    .wrapKey("jwk", privateKey, key, { name: "AES-GCM", iv })
     .then(buffer => bufferToBase64(iv) + bufferToBase64(buffer));
 }
 
@@ -91,13 +91,13 @@ export function exportPrivateKey(privateKey, key) {
  * @return Promise<CryptoKey>
  */
 export function importPrivateKey(privateKey, key, extractable) {
-  const iv = base64ToBuffer(privateKey.substring(0, 24));
-  const wrapped = base64ToBuffer(privateKey.substring(24));
+  const iv = base64ToBuffer(privateKey.substring(0, 16));
+  const wrapped = base64ToBuffer(privateKey.substring(16));
   return window.crypto.subtle.unwrapKey(
     "jwk",
     wrapped,
     key,
-    { name: "AES-CBC", iv },
+    { name: "AES-GCM", iv },
     { name: "RSA-OAEP", hash: { name: "SHA-256" } },
     isSafari || extractable,
     ["decrypt", "unwrapKey"]
@@ -174,7 +174,7 @@ export function generatePassphraseKey(passphrase, salt) {
           hash: "SHA-256"
         },
         key,
-        { name: "AES-CBC", length: 256 },
+        { name: "AES-GCM", length: 256 },
         false,
         ["wrapKey", "unwrapKey"]
       )
@@ -193,7 +193,7 @@ export function generatePassphraseKey(passphrase, salt) {
  */
 export function generateKey() {
   return window.crypto.subtle.generateKey(
-    { name: "AES-CBC", length: 256 },
+    { name: "AES-GCM", length: 256 },
     true,
     ["encrypt", "decrypt"]
   );
@@ -230,7 +230,7 @@ export function importKey(key, privateKey) {
     base64ToBuffer(key),
     privateKey,
     { name: "RSA-OAEP", hash: { name: "SHA-256" } },
-    { name: "AES-CBC", length: 256 },
+    { name: "AES-GCM", length: 256 },
     true,
     ["encrypt", "decrypt"]
   );
@@ -245,11 +245,11 @@ export function importKey(key, privateKey) {
  * @return Promise<String>
  */
 export function encrypt(key, plaintext) {
-  const iv = window.crypto.getRandomValues(new Uint8Array(16));
+  const iv = window.crypto.getRandomValues(new Uint8Array(12));
   const buffer = stringToBuffer(plaintext);
 
   return window.crypto.subtle
-    .encrypt({ name: "AES-CBC", iv: iv }, key, buffer)
+    .encrypt({ name: "AES-GCM", iv: iv, tagLength: 128 }, key, buffer)
     .then(encrypted => bufferToBase64(iv) + bufferToBase64(encrypted));
 }
 
@@ -262,10 +262,10 @@ export function encrypt(key, plaintext) {
  * @return Promise<String>
  */
 export function decrypt(key, ciphertext) {
-  const iv = base64ToBuffer(ciphertext.substring(0, 24));
-  const encrypted = base64ToBuffer(ciphertext.substring(24));
+  const iv = base64ToBuffer(ciphertext.substring(0, 16));
+  const encrypted = base64ToBuffer(ciphertext.substring(16));
 
   return window.crypto.subtle
-    .decrypt({ name: "AES-CBC", iv: iv }, key, encrypted)
+    .decrypt({ name: "AES-GCM", iv: iv, tagLength: 128 }, key, encrypted)
     .then(buffer => bufferToString(buffer));
 }
diff --git a/test/javascripts/lib/keys-test.js.es6 b/test/javascripts/lib/keys-test.js.es6
index 249bf80..34a53a2 100644
--- a/test/javascripts/lib/keys-test.js.es6
+++ b/test/javascripts/lib/keys-test.js.es6
@@ -69,6 +69,20 @@ test("encrypt & decrypt", async assert => {
   const key = await generateKey();
   const plaintext = "this is a message";
   const ciphertext = await encrypt(key, plaintext);
+
+  /*
+   * Length of ciphertext is computed as sum:
+   *   - input length (UTF-16, input size = output size for AES-GCM)
+   *   - tag length is 128-bits
+   *   - IV has 12 bytes
+   *
+   * Base64 is used for encoding, so every 3 bytes become 4 bytes.
+   */
+  let length =
+    4 * Math.ceil((plaintext.length * 2 + 128 / 8) / 3) + // base64(tag + ciphertext).length
+    4 * Math.ceil(12 / 3); // base64(iv).length
+  assert.equal(ciphertext.length, length);
+
   const plaintext2 = await decrypt(key, ciphertext);
   assert.equal(plaintext, plaintext2);
 });

GitHub

2 Likes