Webcrypto and Go
August 17, 2017
Encrypt on client side using javascript WebCrypto and decrypt on backend using go:
javascript code:
async function aesGcmEncrypt(plaintext, password) {
const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password
const iv = crypto.getRandomValues(new Uint8Array(12)); // get 96-bit random iv
const alg = { name: 'AES-GCM', iv: iv }; // specify algorithm to use
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['encrypt']); // generate key from pw
const ptUint8 = new TextEncoder().encode(plaintext); // encode plaintext as UTF-8
const ctBuffer = await crypto.subtle.encrypt(alg, key, ptUint8); // encrypt plaintext using key
const ctArray = Array.from(new Uint8Array(ctBuffer)); // ciphertext as byte array
const ctStr = ctArray.map(byte => String.fromCharCode(byte)).join(''); // ciphertext as string
const ctBase64 = btoa(ctStr); // encode ciphertext as base64
const ivHex = Array.from(iv).map(b => ('00' + b.toString(16)).slice(-2)).join(''); // iv as hex string
return ivHex+ctBase64; // return iv+ciphertext
}
async function aesGcmDecrypt(ciphertext, password) {
const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password
const iv = ciphertext.slice(0,24).match(/.{2}/g).map(byte => parseInt(byte, 16)); // get iv from ciphertext
const alg = { name: 'AES-GCM', iv: new Uint8Array(iv) }; // specify algorithm to use
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']); // use pw to generate key
const ctStr = atob(ciphertext.slice(24)); // decode base64 ciphertext
const ctUint8 = new Uint8Array(ctStr.match(/./g).map(ch => ch.charCodeAt(0))); // ciphertext as Uint8Array
// note: why doesn't ctUint8 = new TextEncoder().encode(ctStr) work?
const plainBuffer = await crypto.subtle.decrypt(alg, key, ctUint8); // decrypt ciphertext using key
const plaintext = new TextDecoder().decode(plainBuffer); // decode password from UTF-8
return plaintext; // return the plaintext
}
Backend code:
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"log"
)
func main() {
data := "e2f4b56b1961ca12d38031420DVbsv0o5Tux4MUCGG0ZcMUZ7yZHlSWn3DsZJmbg"
message := bytes.Buffer{}
iv, err := hex.DecodeString(data[:24])
if err != nil {
log.Fatal(err)
}
message.Write(iv)
payload, err := base64.StdEncoding.DecodeString(data[24:])
if err != nil {
fmt.Println("decode error:", err)
return
}
message.Write(payload)
h := sha256.New()
io.WriteString(h, "pw")
key := h.Sum(nil)
out, err := Decrypt(key, message.Bytes())
if err != nil {
fmt.Println("decode error:", err)
return
}
fmt.Printf("message: %s\n", out)
}
// Decrypt AES-256 GCM
func Decrypt(password, message []byte) ([]byte, error) {
c, err := aes.NewCipher(password)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
copy(nonce, message[:gcm.NonceSize()])
out, err := gcm.Open(nil, nonce, message[gcm.NonceSize():], nil)
if err != nil {
return nil, err
}
return out, nil
}
Example of web page index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>test webcrypto</title>
<script src="common.js"></script>
<script>
(async() => {
const ciphertext = await aesGcmEncrypt("my secret text", "pw");
out = await aesGcmDecrypt(ciphertext, "pw");
console.log(ciphertext, out);
})()
</script>
</head>
<body>
check your console.log
</body>
</html>
To test create a directory with index.html
and common.js
and execute command
www: