Private key (AES-CBC) encryption and decryption in Deno
📜I found myself needing to encrypt a small string on Deno (and subsequently decrypt it later) for a small side project. I checked out a few websites with code samples. But those didn’t work/were out of date. So this is what worked for me:
Note: I am not a security expert and I don’t play one on TV. You shouldn’t trust anything important with random code you found on the internet. Seriously.
1. Save the key
First we generate a key using the standard SubtleCrypto api that ships with Deno. Then export a JSON webkey and save it. I’m putting it in an environmental variable. Make sure it is safe and secure.
const key = await crypto.subtle.generateKey({ name: "AES-CBC", length: 128 },true,["encrypt", "decrypt"]);
const rawKey = JSON.stringify(await crypto.subtle.exportKey("jwk", key));
2. Encrypt a string
Here we take the JSON webkey from our environmental variable that we created in step one and import it. Then using the key along with some random bytes, the initialization vector iv, we encrypt the string. The last two lines deal with getting the encrypted string into a format to save. The iv needs to be random and unique per message but it doesn’t need to be secret. So we can prepend it to the encrypted message.
You could potentially take this one step further and get a hex string from the encrypted bytes and save that. But for my needs I stopped here and stored the comma separated string of numbers.
async function encryptMe(decrypted)
{
const _appSecret = JSON.parse(Deno.env.get("APP_SECRET"));
const iv = await crypto.getRandomValues(new Uint8Array(16));
const key = await crypto.subtle.importKey("jwk", _appSecret, "AES-CBC", true, ["encrypt", "decrypt"]);
const encrypted = await crypto.subtle.encrypt({name: "AES-CBC", iv}, key, new TextEncoder().encode(decrypted));
const encryptedBytes = new Uint8Array(encrypted);
return `${iv.toString()}|${encryptedBytes.toString()}`;
}
3. Decrypt a string
The next step is to decrypt a string we previously encrypted. After we make a key we will need to separate out the two parts of the passed in message. The first part specifies the iv that was used and the second stores the encrypted message. You can use the Uint8Array.from to re-create the Unit8Array. Then call the decrypt method from the key, use a text decoder and you have your decrypted message. Yay! 👏
async function decryptMe(encrypted)
{
const _appSecret = JSON.parse(Deno.env.get("APP_SECRET"));
const key = await crypto.subtle.importKey("jwk", _appSecret, "AES-CBC", true, ["encrypt", "decrypt"]);
const ivPart = encrypted.split('|')[0];
const encryptedPart = encrypted.split('|')[1];
const encryptedBytes = Uint8Array.from(encryptedPart.split(',').map(num => parseInt(num)));
const iv = Uint8Array.from(ivPart.split(',').map(num => parseInt(num)));
const decrypted = await crypto.subtle.decrypt({name: "AES-CBC", iv}, key, encryptedBytes);
const decryptedBytes = new Uint8Array(decrypted);
return new TextDecoder().decode(decryptedBytes);
}
