Skip to content

Instantly share code, notes, and snippets.

@theprojectsomething
Last active September 6, 2022 06:24
Show Gist options
  • Save theprojectsomething/2759e944385db89c19a41de80705aa1c to your computer and use it in GitHub Desktop.
Save theprojectsomething/2759e944385db89c19a41de80705aa1c to your computer and use it in GitHub Desktop.
Simple Passphrase-based Encryption Example for the Web

Simple Passphrase-based Encryption Example for the Web

A straightforward form of encryption using a passphrase (or a shared secret) that can be useful in web-based environments - especially in storage and transport. Consider e.g. securing sensitive user data in localStorage, or sending data via a third-party server (such as a serverless endpoint).

This method uses a passphrase to encrypt and decrypt any string, number, array, or other JSON-compatible object using AES in GCM mode and is ideal for cases where one party is both encrypting and decrypting. For cases where you are sending encrypted data to other parties you might consider alternative methods, such as public-key cryptography. For a comprehensive rundown of encryption methods available in the browser, refer to MDN's SubtleCrypto documentation.

Example usage

// store the script locally and import
import { encryptData, decryptData } from './passphrase-encryption.js'

// a passphrase to be used for encryption and decryption
const passphrase = 'its a secret';
// a simple json-compatible object to encrypt
const dataToEncrypt = { a: 'list', of: [1, 2, 3, 4, 5, 'example'], items: { to: 'encrypt' } };

// encrypt the data ...
// encryption returns a base64 encoded cyphertext (the encrypted data) and an IV (initialisation vector)
// these can be safely stored/transported together, in public
const { iv, data } = await encryptData(dataToEncrypt, passphrase);
console.log({ iv, data })

// you could then create a string for transport
const estring = `${iv}|${data}`;
// and then retrieve the components with a selective split
const [ivFromString, dataFromString] = estring.split(/\|(.*)/, 2);

// or store the encrypted data in localStorage
localStorage.setItem('this-is-encrypted', JSON.stringify({ iv, data }));

// and wait for a month before retrieving it
const { iv: ivFromStorage, data: dataFromStorage } = JSON.parse(localStorage.getItem('this-is-encrypted'));

// and then decrypt it, ensuring you pass in the encrypted data and IV as per the original encryption
const decrypted = await decryptData({ iv: ivFromStorage, data: dataFromStorage }, passphrase);

// your decrypted data should be idenitical (but not equal) to your original object
console.log({ decrypted })

Comments, suggestions & improvements welcome! 🤙

// See notes re. GCM and *authenticated encryption*
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt#supported_algorithms
const ALGO = { name: 'AES-GCM' };
// store key in memory for re-use
const KEY = {};
const encoder = new TextEncoder();
// encode url-safe base64
function to64(string) {
return btoa(string).replaceAll('+', '-');
}
// decode from url-safe base64
function from64(encoded) {
return atob(encoded.replaceAll('-', '+'));
}
// convert our typed array to a string
function toString(binary) {
return String.fromCharCode(...new Uint8Array(binary));
}
// convert our string back to a typed array
function fromString(binaryString) {
const binary = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; ++i) {
binary[i] = binaryString.charCodeAt(i);
}
return binary;
}
// convert our typed array to base64 encoded binary string
function fromArray(binary) {
const binaryString = toString(binary);
return to64(binaryString);
}
// convert our base64 encoded binary string to an array
function toArray(binary64) {
const binaryString = from64(binary64);
return fromString(binaryString);
}
// get/create the current cryptokey for a given passphrase
const getKey = async (passphrase) => {
if (KEY.passphrase === passphrase) {
return KEY.cryptoKey;
}
KEY.passphrase = passphrase;
KEY.cryptoKey = await crypto.subtle.importKey(
'raw',
await crypto.subtle.digest('SHA-256', encoder.encode(passphrase)),
ALGO,
false,
['encrypt', 'decrypt'],
);
return KEY.cryptoKey;
}
// encrypt data with a passphrase
export const encryptData = async (data, passphrase) => {
const iv = crypto.getRandomValues(new Uint8Array(12));
const cryptoKey = await getKey(passphrase);
const encrypted = await crypto.subtle.encrypt(
{ iv, ...ALGO },
cryptoKey,
encoder.encode(JSON.stringify(data)),
);
return {
iv: fromArray(iv),
data: fromArray(encrypted),
};
}
// decrypt an ecrypted { data, iv } object with a passphrase
export const decryptData = async (encrypted, passphrase) => {
const { iv, data } = encrypted;
const cryptoKey = await getKey(passphrase);
const decrypted = await crypto.subtle.decrypt(
{
iv: toArray(iv),
...ALGO,
},
cryptoKey,
toArray(data),
);
return JSON.parse(toString(decrypted));
}
export default { encryptData, decryptData }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment