Skip to content
xashadocs
Tab to a result · Enter to open · Esc to close

Getting started

Encryption

Encrypt and decrypt locally with AES-256-GCM. The API receives an envelope, never plaintext or a key.

Envelope format#

json
{
  "version": 1,
  "iv": "<canonical unpadded base64url>",
  "ciphertext": "<encrypted bytes plus authentication tag>"
}
versionExactly 1.
ivFresh random 12-byte IV.
ciphertextEncrypted UTF-8 text with a 16-byte tag appended.

Key handling#

Generate a fresh random 32-byte key per secret. Encode binary fields as canonical, unpadded base64url. Do not use additional authenticated data in version 1.

The backend validates structure and size; it cannot prove the content was encrypted correctly.

Encryption helper#

Copy this complete helper into encryption.mjs. It comes from the working backend example ↗.

javascript
// Envelope interoperability example. No storage, logging, or network calls.
const encode = bytes => btoa(String.fromCharCode(...bytes))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
const decode = value => {
  if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) {
    throw new Error('Invalid base64url encoding.')
  }
  const bytes = Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))
  if (encode(bytes) !== value) throw new Error('Invalid base64url encoding.')
  return bytes
}

export async function encryptText(text) {
  if (typeof text !== 'string') throw new Error('Expected text.')
  const plaintext = new TextEncoder().encode(text)
  if (plaintext.length < 1 || plaintext.length > 32768) throw new Error('Text must be 1–32768 UTF-8 bytes.')
  const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'])
  const iv = crypto.getRandomValues(new Uint8Array(12))
  const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, key, plaintext)
  return {
    envelope: { version: 1, iv: encode(iv), ciphertext: encode(new Uint8Array(ciphertext)) },
    keyFragment: encode(new Uint8Array(await crypto.subtle.exportKey('raw', key))),
  }
}

export async function decryptText(envelope, keyFragment) {
  if (envelope?.version !== 1) throw new Error('Unsupported envelope version.')
  const rawKey = decode(keyFragment)
  const iv = decode(envelope.iv)
  const ciphertext = decode(envelope.ciphertext)
  if (rawKey.length !== 32 || iv.length !== 12 || ciphertext.length < 17 || ciphertext.length > 32784) {
    throw new Error('Invalid envelope or key size.')
  }
  const key = await crypto.subtle.importKey('raw', rawKey, 'AES-GCM', false, ['decrypt'])
  const plaintext = await crypto.subtle.decrypt({ name: 'AES-GCM', iv, tagLength: 128 }, key, ciphertext)
  return new TextDecoder('utf-8', { fatal: true }).decode(plaintext)
}

Text limits#

Accept 1 to 32,768 bytes of UTF-8 text. Count bytes, not characters. The ciphertext including its tag is 17 to 32,784 bytes. The full JSON request may not exceed 49,152 bytes.