Skip to content
Snippets Groups Projects
encryption-internals.web.ts 2.73 KiB
let ENCRYPTION_ALGORITHM = 'aes-256-gcm';

function browserAlgorithmName(name) {
  switch (name) {
    case 'aes-256-gcm':
      return 'AES-GCM';
    default:
      throw new Error('unsupported crypto algorithm: ' + name);
  }
}

export async function sha256String(str) {
  // @ts-expect-error TextEncoder might not accept an argument
  let inputBuffer = new TextEncoder('utf-8').encode(str).buffer;
  let buffer = await crypto.subtle.digest('sha-256', inputBuffer);
  let outputStr = Array.from(new Uint8Array(buffer))
    .map(n => String.fromCharCode(n))
    .join('');
  return btoa(outputStr);
}

export function randomBytes(n) {
  return Buffer.from(crypto.getRandomValues(new Uint8Array(n)));
}

export async function encrypt(masterKey, value) {
  let iv = crypto.getRandomValues(new Uint8Array(12));

  let encrypted = await crypto.subtle.encrypt(
    {
      name: browserAlgorithmName(ENCRYPTION_ALGORITHM),
      iv,
      tagLength: 128,
    },
    masterKey.getValue().raw,
    value,
  );

  encrypted = Buffer.from(encrypted);

  // Strip the auth tag off the end
  let authTag = encrypted.slice(-16);
  encrypted = encrypted.slice(0, -16);

  return {
    value: encrypted,
    meta: {
      keyId: masterKey.getId(),
      algorithm: ENCRYPTION_ALGORITHM,
      iv: Buffer.from(iv).toString('base64'),
      // @ts-expect-error base64 argument is valid only on NodeJS
      authTag: authTag.toString('base64'),
    },
  };
}

export async function decrypt(masterKey, encrypted, meta) {
  let { algorithm, iv, authTag } = meta;

  let decrypted = await crypto.subtle.decrypt(
    {
      name: browserAlgorithmName(algorithm),
      iv: Buffer.from(iv, 'base64'),
      tagLength: 128,
    },
    masterKey.getValue().raw,
    Buffer.concat([encrypted, Buffer.from(authTag, 'base64')]),
  );

  return Buffer.from(decrypted);
}

export async function createKey({ secret, salt }) {
  let passwordBuffer = Buffer.from(secret);
  let saltBuffer = Buffer.from(salt);

  let passwordKey = await crypto.subtle.importKey(
    'raw',
    passwordBuffer,
    { name: 'PBKDF2' },
    false,
    ['deriveBits', 'deriveKey'],
  );

  let derivedKey = await crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      hash: 'SHA-512',
      salt: saltBuffer,
      iterations: 10000,
    },
    passwordKey,
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt'],
  );

  let exported = await crypto.subtle.exportKey('raw', derivedKey);

  return {
    raw: derivedKey,
    base64: Buffer.from(exported).toString('base64'),
  };
}

export async function importKey(str) {
  let key = await crypto.subtle.importKey(
    'raw',
    Buffer.from(str, 'base64'),
    { name: 'AES-GCM' },
    false,
    ['encrypt', 'decrypt'],
  );

  return {
    raw: key,
    base64: str,
  };
}