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