All files / app/lib encryption.ts

91.3% Statements 21/23
78.57% Branches 11/14
100% Functions 7/7
90.9% Lines 20/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69      88x 88x   23x 23x     23x                               23x       41x 41x 41x 41x 41x         41x             10x 10x       15x     15x 15x 15x 15x         15x            
import {compress, decompress} from './compression'
 
type StoredKey = {salt: Uint8Array; key: CryptoKey}
export const keys: {[index: string]: StoredKey} = {}
const encoder = new TextEncoder()
async function getKey(name: string, password: string, salt: Uint8Array): Promise<StoredKey | undefined> {
  Eif (!(name in keys)) {
    const baseKey = await crypto.subtle.importKey('raw', encoder.encode(password), {name: 'PBKDF2'}, false, [
      'deriveKey',
    ])
    keys[name] = {
      salt,
      key: await crypto.subtle.deriveKey(
        {
          name: 'PBKDF2',
          salt,
          iterations: 1e6,
          hash: 'SHA-256',
        },
        baseKey,
        {name: 'AES-GCM', length: 256},
        false,
        ['encrypt', 'decrypt']
      ),
    }
  }
  return keys[name]
}
export async function encrypt(name: string, content: object, password?: string) {
  const key =
    password && !(name in keys) ? await getKey(name, password, crypto.getRandomValues(new Uint8Array(16))) : keys[name]
  Eif (key) {
    try {
      const iv = crypto.getRandomValues(new Uint8Array(12))
      const encrypted = await crypto.subtle.encrypt(
        {name: 'AES-GCM', iv},
        key.key,
        new Uint8Array(await (await compress(content)).arrayBuffer())
      )
      return new Blob([iv, key.salt, encrypted])
    } catch (e) {
      console.error('failed to encrypt ' + name + ': ' + e)
    }
  }
}
export function parseStoredString(raw: string): Promise<Blob> {
  return new Promise(async resolve => {
    resolve(await fetch('data:application/octet-stream;base64,' + raw.substring(1)).then(res => res.blob()))
  })
}
export async function decrypt(name: string, content: string | Blob, password?: string) {
  const buffer = await ('string' === typeof content
    ? (await parseStoredString(content)).arrayBuffer()
    : content.arrayBuffer())
  const key = password ? await getKey(name, password, new Uint8Array(buffer, 12, 16)) : keys[name]
  Eif (key) {
    try {
      const decrypted = await crypto.subtle.decrypt(
        {name: 'AES-GCM', iv: new Uint8Array(buffer, 0, 12)},
        key.key,
        new Uint8Array(buffer, 28)
      )
      return decompress(new Blob([decrypted]))
    } catch (e) {
      console.error('failed to decrypt ' + name + ': ' + e)
    }
  }
}