Random Thoughts by Fabien Penso

Swift CryptoKit and Browser

This previous article explains how to read ChaCha20-Poly encrypted data using Ruby or Python. My first goal is to ensure other languages can read data encrypted within Beam, but the end goal is to decrypt it within your browser, using client-side HTML and Javascript. Sadly, WebCrypto omits ChaCha20-Poly, and I had to move to AES-GCM instead.

This extensive documentation says to use additionalData for the tag part, but that never worked on my code, and I had to do that manually.

Use the following in Xcode Playground to encrypt a string:

import UIKit
import CryptoKit

let str = "Hello, playground"
let strData = str.data(using: .utf8)!

let key = SymmetricKey(size: .bits256)
let keyString = key.withUnsafeBytes { Data($0) }.base64EncodedString()

let sealbox = try! AES.GCM.seal(strData, using: key)

print("Key: \(keyString)")
print("Combined: \(sealbox.combined!.base64EncodedString())")

The output when running it on my computer (you obviously will get a different result):

Key: lQ4F/9K45Ym9K8Qv9CkVrozkTsGij7/OErhzMmhb8Ec=
Combined: NYsQV/IXJDyZgSY3hb/AQapynEBSIDXlO4TdMC+6F6DHmUBOnXEPcE/+sVrz

Ruby

You can decode the encrypted string with the private key using Ruby:

#!/usr/bin/env ruby

require "openssl"
require "base64"

### AES GCM

key = Base64.decode64 "lQ4F/9K45Ym9K8Qv9CkVrozkTsGij7/OErhzMmhb8Ec="
combined = Base64.decode64 "NYsQV/IXJDyZgSY3hb/AQapynEBSIDXlO4TdMC+6F6DHmUBOnXEPcE/+sVrz"
text = "Hello, playground"

# Combined version
combinedTag = combined[-16..-1]
combinedNonce = combined[0..11]
combinedCipherText = combined[12..(combined.size-17)]

decipher = OpenSSL::Cipher.new("AES-256-GCM").decrypt
decipher.key = key

decipher.iv = combinedNonce
decipher.auth_tag = combinedTag

decrypted = decipher.update(combinedCipherText) + decipher.final
if decrypted == text
  puts "OK!"
end

Python

You can decode the encrypted string with the private key using Python:

#!/usr/bin/env python3
# Installation:
# pip install pycryptodome

import json
import Crypto
from base64 import b64decode
from Crypto.Cipher import AES

clearText = "Hello, playground"
key = b64decode("lQ4F/9K45Ym9K8Qv9CkVrozkTsGij7/OErhzMmhb8Ec=")
combined = b64decode("NYsQV/IXJDyZgSY3hb/AQapynEBSIDXlO4TdMC+6F6DHmUBOnXEPcE/+sVrz")
clearText = "Hello, playground"

combinedNonce = combined[:12]
combinedTag = combined[:-16]
combinedCipher = combined[12:-16]

cipher = AES.new(key, AES.MODE_GCM, nonce=combinedNonce)
clear = cipher.decrypt(combinedCipher).decode()

if clearText == clear:
    print("OK!")

HTML and Javascript

You can decode the encrypted string with the private key using browser based HTML and Javascript:

<!-- index.html -->
<html>
  <head>
    <meta charset="UTF-8">
    <style>
body { font-size: 0.8em; }
pre {
  white-space: pre-wrap;       /* css-3 */
  white-space: -moz-pre-wrap;  /* Mozilla, since 1999 */
  white-space: -pre-wrap;      /* Opera 4-6 */
  white-space: -o-pre-wrap;    /* Opera 7 */
  word-wrap: break-word;       /* Internet Explorer 5.5+ */
}
    </style>
  </head>
  <body>
    <pre id="results"></pre>
  </body>

  <script src="aes_gcm.js" charset="utf-8"></script>
</html>
// aes_gcm.js
const fromBase64 = base64String => Uint8Array.from(atob(base64String), c => c.charCodeAt(0))

let clearText = "Hello, playground"
let combined = fromBase64("NYsQV/IXJDyZgSY3hb/AQapynEBSIDXlO4TdMC+6F6DHmUBOnXEPcE/+sVrz")
let privateKey = fromBase64("lQ4F/9K45Ym9K8Qv9CkVrozkTsGij7/OErhzMmhb8Ec=")

let nonce = combined.slice(0, 12)
let tag = combined.slice(-16)
let cipher = combined.slice(12, -16)

async function test() {
  var key = await window.crypto.subtle.importKey("raw",
    privateKey,
    { name: "AES-GCM" },
    true,
    ["decrypt", "encrypt"])

  try {
    var encrypted = await window.crypto.subtle.encrypt(
      {
        name: "AES-GCM",
        iv: nonce,
      },
      key,
      new TextEncoder().encode(clearText)
    )

    combinedEncrypted = _append2Buffer(nonce, encrypted)
    // Encrypted version
    // add_log(_arrayBufferToBase64(combinedEncrypted))

    var decrypted = await window.crypto.subtle.decrypt(
      {
        name: "AES-GCM",
        iv: nonce,
        // Don't use `additionalData, it *does not* work
      },
      key,
      encrypted)
    // Decrypt the encrypted version
    //add_log(new TextDecoder().decode(decrypted))
  } catch(e) {
    add_log(e)
  }

  try {
    var decrypted = await window.crypto.subtle.decrypt(
      {
        name: "AES-GCM",
        iv: nonce,
      },
      key,
      _append2Buffer(cipher, tag))
    if (clearText == new TextDecoder().decode(decrypted)) {
      add_log("OK")
    }
  } catch(e) {
    add_log(e)
  }
}

test()

/*
 * -----------------------------------------------------------------------
 */

// Add logs
function add_log(text) {
  let results = document.getElementById("results")
  results.innerHTML += text + "\n"
  console.log(text)
}

function _arrayBufferToBase64(buffer) {
  var binary = '';
  var bytes = new Uint8Array( buffer );
  var len = bytes.byteLength;
  for (var i = 0; i < len; i++) {
    binary += String.fromCharCode( bytes[ i ] );
  }
  return window.btoa(binary);
}

function _append2Buffer(buffer1, buffer2) {
  var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength)
  tmp.set(new Uint8Array(buffer1), 0)
  tmp.set(new Uint8Array(buffer2), buffer1.byteLength)
  return tmp.buffer;
}