AES-GCM encryption in JavaScript, decrypted in C#
If you must encrypt in the browser, JavaScript with Web Crypto API is an option. If you need to decrypt the results in a different language, it takes some time to find out how. Here’s one approach with AES-GCM.
Introduction
Crypto is complex - there is always a lot to consider. Choosing the right technology for your current purpose can be challenging and requires a lot of know-how and experience in nearly every case.
Disclaimer: This article neither claims to show the right, nor the only way to do encryption across technology boundaries. It is just an example that should help you to understand one possible approach - you may adopt it to your needs.
Encryption in JavaScript - Web Crypto API
The Web Crypto API is very powerful and fast enough to perform realtime file-encryption in the browser. However, in this example we are going to use symmetric text message encryption using Galois/Counter Mode (GCM) including a Nonce (also called IV) and a Tag for verification. Let’s dive right into the code:
// generates a 128 bit key for encryption and decryption in base64 format
const generateKey = async () => {
const key = await window.crypto.subtle.generateKey({
name: 'AES-GCM',
length: 128
},
true, [
'encrypt',
'decrypt'
]);
const exportedKey = await window.crypto.subtle.exportKey(
'raw',
key,
);
return bufferToBase64(exportedKey);
}
// arrayBuffer to base64
const bufferToBase64 = (arrayBuffer) => {
return window.btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
}
// load a base64 encoded key
const loadKey = async (base64Key) => {
return await window.crypto.subtle.importKey(
'raw',
base64ToBuffer(base64Key),
"AES-GCM",
true, [
"encrypt",
"decrypt"
]
);
}
// base64 to arrayBuffer
const base64ToBuffer = (base64) => {
const binary_string = window.atob(base64);
const len = binary_string.length;
let bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
const cryptGcm = async (base64Key, bytes) => {
const key = await loadKey(base64Key);
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const algorithm = {
iv,
name: 'AES-GCM'
};
const cipherData = await window.crypto.subtle.encrypt(
algorithm,
key,
bytes
);
// prepend the random IV bytes to raw cipherdata
const cipherText = concatArrayBuffers(iv.buffer, cipherData);
return bufferToBase64(cipherText);
}
// concatenate two array buffers
const concatArrayBuffers = (buffer1, buffer2) => {
let tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
}
const plaintext = "This is my message";
const plaintextBytes = (new TextEncoder()).encode(plaintext, 'utf-8');
const encryptionKey = await generateKey();
const ciphertext = await cryptGcm(encryptionKey, plaintextBytes);
console.log("plaintext: ", plaintext);
console.log("encryptionKey (base64):", encryptionKey);
console.log("ciphertext (base64):", ciphertext);
What is happening?
- First, generate a random key (for convenient storage as base64 string)
- This key needs to be safely stored for encryption / decryption
- Then encrypt the plaintext with this key by
- Generating a random 12 byte IV (also called nonce)
- Encode the text message in UTF-8
- Encrypt the plaintext with GCM
- Prepend the random IV bytes (this step is arbitrary, you could also store the IV in another way)
- base64 encode the whole byte stream
Decryption in C#
Now that you have stored your randomly generated key and the ciphertext, you can decrypt the whole thing with C#:
using System;
using System.Security.Cryptography;
using System.Text;
namespace Decrypter
{
class Program
{
static void Main(string[] args)
{
var key = "<put your base64 key here>";
var ciphertext = "<put your base64 message here>";
var plaintext = Decrypt(key, ciphertext);
Console.WriteLine(plaintext);
}
private static string Decrypt(string base64Key, string base64Ciphertext)
{
// convert from base64 to raw bytes spans
var encryptedData = Convert.FromBase64String(base64Ciphertext).AsSpan();
var key = Convert.FromBase64String(base64Key).AsSpan();
var tagSizeBytes = 16; // 128 bit encryption / 8 bit = 16 bytes
var ivSizeBytes = 12; // 12 bytes iv
// ciphertext size is whole data - iv - tag
var cipherSize = encryptedData.Length - tagSizeBytes - ivSizeBytes;
// extract iv (nonce) 12 bytes prefix
var iv = encryptedData.Slice(0, ivSizeBytes);
// followed by the real ciphertext
var cipherBytes = encryptedData.Slice(ivSizeBytes, cipherSize);
// followed by the tag (trailer)
var tagStart = ivSizeBytes + cipherSize;
var tag = encryptedData.Slice(tagStart);
// now that we have all the parts, the decryption
Span<byte> plainBytes = cipherSize < 1024
? stackalloc byte[cipherSize]
: new byte[cipherSize];
using var aes = new AesGcm(key);
aes.Decrypt(iv, cipherBytes, tag, plainBytes);
return Encoding.UTF8.GetString(plainBytes);
}
}
}
Conclusion
You see that it is totally possible to encrypt in JavaScript and decrypt in other langauges (e.g. C#
). The clue is to find out where the different parts of the encryption headers and trailers are being stored by different technologies (IV/Nonce, Tag, CipherData) - often you have to adjust or add them manually.
Hints:
- Storing the IV (nonce) as prefix of the ciphertext is arbitrary - you could also define a custom header with length or store it as a trailer, but the IV must be somehow transmitted with the ciphertext. The extraction in
C#
has to match the process of adding and vice versa. - Encrypting raw binary bytes would work similar, but you have to adjust some lines if you don’t want to encrypt text messages
Hope I could save you some time figuring out how to do this.