async function encryptValue (sharedKey, value) {
  // random IVs are _really_ important in GCM: reusing the IV once can lead to catastrophic failure
  // see https://crypto.stackexchange.com/questions/26790/how-bad-it-is-using-the-same-iv-twice-with-aes-gcm
  // 12 bytes (96 bits) is the recommended IV size for AES-GCM
  const iv = window.crypto.getRandomValues(new Uint8Array(12))
  const encoded = new TextEncoder().encode(JSON.stringify(value))
  const encrypted = await window.crypto.subtle.encrypt(
    {
      name: 'AES-GCM',
      iv
    },
    sharedKey,
    encoded
  )
  return {
    iv: toHex(iv.buffer),
    value: toHex(encrypted)
  }
}
Encrypt data using AES-GCM
@param {CryptoKey} sharedKey - the key to use for encryption
@param {Object} value - the value to encrypt
@returns {Promise<Object>} an object with iv and value properties, can be passed to decryptValue to get the original data back