Skip to content

Instantly share code, notes, and snippets.

@chrisveness
Last active March 17, 2024 15:36
Show Gist options
  • Save chrisveness/43bcda93af9f646d083fad678071b90a to your computer and use it in GitHub Desktop.
Save chrisveness/43bcda93af9f646d083fad678071b90a to your computer and use it in GitHub Desktop.
Uses the SubtleCrypto interface of the Web Cryptography API to encrypt and decrypt text using AES-GCM (AES Galois counter mode).
/**
* Encrypts plaintext using AES-GCM with supplied password, for decryption with aesGcmDecrypt().
* (c) Chris Veness MIT Licence
*
* @param {String} plaintext - Plaintext to be encrypted.
* @param {String} password - Password to use to encrypt plaintext.
* @returns {String} Encrypted ciphertext.
*
* @example
* const ciphertext = await aesGcmEncrypt('my secret text', 'pw');
* aesGcmEncrypt('my secret text', 'pw').then(function(ciphertext) { console.log(ciphertext); });
*/
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 ivStr = Array.from(iv).map(b => String.fromCharCode(b)).join(''); // iv as utf-8 string
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
return btoa(ivStr+ctStr); // iv+ciphertext base64-encoded
}
/**
* Decrypts ciphertext encrypted with aesGcmEncrypt() using supplied password.
* (c) Chris Veness MIT Licence
*
* @param {String} ciphertext - Ciphertext to be decrypted.
* @param {String} password - Password to use to decrypt ciphertext.
* @returns {String} Decrypted plaintext.
*
* @example
* const plaintext = await aesGcmDecrypt(ciphertext, 'pw');
* aesGcmDecrypt(ciphertext, 'pw').then(function(plaintext) { console.log(plaintext); });
*/
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 ivStr = atob(ciphertext).slice(0,12); // decode base64 iv
const iv = new Uint8Array(Array.from(ivStr).map(ch => ch.charCodeAt(0))); // iv as Uint8Array
const alg = { name: 'AES-GCM', iv: iv }; // specify algorithm to use
const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']); // generate key from pw
const ctStr = atob(ciphertext).slice(12); // decode base64 ciphertext
const ctUint8 = new Uint8Array(Array.from(ctStr).map(ch => ch.charCodeAt(0))); // ciphertext as Uint8Array
// note: why doesn't ctUint8 = new TextEncoder().encode(ctStr) work?
try {
const plainBuffer = await crypto.subtle.decrypt(alg, key, ctUint8); // decrypt ciphertext using key
const plaintext = new TextDecoder().decode(plainBuffer); // plaintext from ArrayBuffer
return plaintext; // return the plaintext
} catch (e) {
throw new Error('Decrypt failed');
}
}
@0xdeepmehta
Copy link

@korywka Awesome work 🙏

@c-plus-plus-equals-c-plus-one

@chrisveness , could you please advice on decrypting the crypto subtle encrypted string with openssl aes-cbc-256?

Has anyone tried to decrypt crypto subtle encrypted code with openssl enc -aes-cbc-256 or openssl aes-cbc-256?

@kyo-ago , @sindresorhus , @birkir , @zhulik , @mientjan , @aadeshkulkarni-fynd , @ahmedghazi , @alfari16 , @amineoutmal , @Arjis2020 , @balde73 , @birkir , @mbaer3000 , @cobiwave , @timbru31

@ve3
Copy link

ve3 commented Feb 6, 2023

Thanks for the code about create key from string await crypto.subtle.digest('SHA-256', pwUtf8). I'm stuck at this.

This code helped me.

@rstropek
Copy link

Thank you for the Gist, very useful! Created a variant for use with Next.js and Typescript. Removed deprecated functions (e.g. btoa in Node). Maybe it is useful for some of you. I am not a crypto expert, so please let me know if I made a mistake.

// Based on: https://gist.github.com/chrisveness/43bcda93af9f646d083fad678071b90a
// Added TypeScript support and changed deprecated functions (e.g. btoa in Node.js)

/**
 * Encrypts plaintext using AES-GCM with supplied password, for decryption with aesGcmDecrypt().
 *                                                                      (c) Chris Veness MIT Licence
 *
 * @param   {String} plaintext - Plaintext to be encrypted.
 * @param   {String} password - Password to use to encrypt plaintext.
 * @returns {String} Encrypted ciphertext.
 *
 * @example
 *   const ciphertext = await aesGcmEncrypt('my secret text', 'pw');
 *   aesGcmEncrypt('my secret text', 'pw').then(function(ciphertext) { console.log(ciphertext); });
 */
export async function aesGcmEncrypt(
  plaintext: string,
  password: string
): Promise<string> {
  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 ivStr = Buffer.from(iv).toString("base64"); // iv as base64 string

  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 ctStr = Buffer.from(ctBuffer).toString("base64"); // ciphertext as base64 string

  return `${ivStr}.${ctStr}`;
}

/**
 * Decrypts ciphertext encrypted with aesGcmEncrypt() using supplied password.
 *                                                                      (c) Chris Veness MIT Licence
 *
 * @param   {String} ciphertext - Ciphertext to be decrypted.
 * @param   {String} password - Password to use to decrypt ciphertext.
 * @returns {String} Decrypted plaintext.
 *
 * @example
 *   const plaintext = await aesGcmDecrypt(ciphertext, 'pw');
 *   aesGcmDecrypt(ciphertext, 'pw').then(function(plaintext) { console.log(plaintext); });
 */
export async function aesGcmDecrypt(
  ciphertext: string,
  password: string
): Promise<string> {
  const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
  const pwHash = await crypto.subtle.digest("SHA-256", pwUtf8); // hash the password

  if (ciphertext.indexOf(".") === -1) {
    throw new Error("Invalid ciphertext");
  }
  const cipherSplitted = ciphertext.split(".");

  const ivStr = cipherSplitted[0]; // decode base64 iv
  const iv = Buffer.from(ivStr, "base64"); // iv as Uint8Array

  const alg = { name: "AES-GCM", iv: iv }; // specify algorithm to use

  const key = await crypto.subtle.importKey("raw", pwHash, alg, false, [
    "decrypt",
  ]); // generate key from pw

  const ctStr = cipherSplitted[1]; // decode base64 iv
  const ctUint8 = Buffer.from(ctStr, "base64"); // ciphertext as Uint8Array

  try {
    const plainBuffer = await crypto.subtle.decrypt(alg, key, ctUint8); // decrypt ciphertext using key
    const plaintext = new TextDecoder().decode(plainBuffer); // plaintext from ArrayBuffer
    return plaintext; // return the plaintext
  } catch (e) {
    throw new Error("Decrypt failed");
  }
}

@kalisjoshua
Copy link

kalisjoshua commented Mar 17, 2024

I really like and appreciate the example code @rstropek. I wanted to try and reduce repetition a little; not sure if I've fundamentally undermined the performance or anything else but this is what I came up with.

const B64 = "base64";
const CON = "|";

const textEncode = (s) => new TextEncoder().encode(s);

async function getBuffer(subject, password, iv, direction) {
  const alg = { name: "AES-GCM", iv };

  return [
    Promise.resolve(textEncode(password)),
    (password) => crypto.subtle.digest("SHA-256", password),
    (pwHash) => crypto.subtle.importKey("raw", pwHash, alg, false, [direction]),
    (key) => crypto.subtle[direction](alg, key, subject),
  ].reduce(async (input, fn) => await fn(await input));
}

export async function decrypt(ciphertext, password) {
  if (!ciphertext.includes(CON)) throw new Error("Invalid ciphertext");

  const [ivStr, ctStr] = ciphertext.split(CON).map((i) => Buffer.from(i, B64));

  try {
    return await getBuffer(ctStr, password, ivStr, "decrypt").then((val) => new TextDecoder().decode(val));
  } catch (e) {
    throw new Error("Decrypt failed");
  }
}

export async function encrypt(plaintext, password) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const buff = await getBuffer(textEncode(plaintext), password, iv, "encrypt");

  return [iv, buff].map((i) => Buffer.from(i).toString(B64)).join(CON);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment