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;
}