---
title: Encryption
description: Client-side encryption, shareable encrypted links, and zero-knowledge patterns
tags: [encryption, security, zero-knowledge]
dependencies: [privacy, safety, supply-chain]
---

# Encryption

If data leaves the user's device, it should be unreadable to everyone except the intended recipient. If it does not need to leave, it should not.

## Principles

- Encrypt before transmit: data is encrypted on the client before any network access.
- Authenticated encryption only: all ciphertext must be integrity-protected.
- Zero plaintext secrets: never store secrets unencrypted in browser storage nor at rest.
- Zero trust in intermediaries: assume CDNs, logs, and proxies are observable.

## The URL fragment pattern

URL fragments (after `#`) are not sent by browsers in HTTP requests. This makes them useful for carrying sensitive content such as encrypted agent configurations. Always encrypt keys, secrets, and agent configurations, before storing in URL fragments.

```
https://app.example.com/#eyJlbmMiOiJ4c2Fsc2EyMC1wb2x5MTMwNSIs...
```

The server can serve static files while the client reads the fragment, decrypts, and renders locally.

## Algorithm choice

Consider XSalsa20-Poly1305 (via TweetNaCl) for in-browser symmetric encryption:

```js
const nonce = nacl.randomBytes(24);
const encrypted = nacl.secretbox(message, nonce, key); // key: 32 bytes
const payload = new Uint8Array([...nonce, ...encrypted]);
```

Why this is often preferred in this architecture:

- 24-byte nonce provides a large random space for nonce generation.
- `tweetnacl` is small and dependency-free.
- One focused primitive keeps implementation simple.

Note: AES-GCM can also be secure when used correctly; nonce management and implementation discipline are critical.

## Key derivation from passwords

When using a human-memorable password, derive 32 raw bytes with PBKDF2 and use them as the NaCl secretbox key:

```js
const keyMaterial = await crypto.subtle.importKey(
  'raw',
  new TextEncoder().encode(password),
  'PBKDF2',
  false,
  ['deriveBits']
);

const bits = await crypto.subtle.deriveBits(
  { name: 'PBKDF2', salt, iterations: 150000, hash: 'SHA-256' },
  keyMaterial,
  256
);

const key = new Uint8Array(bits); // 32 bytes for nacl.secretbox
```

Use per-payload random salts and store salt with ciphertext metadata.

## Encrypted local storage

`localStorage` and `sessionStorage` are readable by scripts on the same origin. Encrypt before storing:

```js
localStorage.setItem('state', base64(encrypt(JSON.stringify(state), key)));
```

- `sessionStorage` for ephemeral state
- `localStorage` for persistence
- both encrypted

## For agents

1. Never store plaintext secrets at rest nor in browser storage
2. Use authenticated encryption for all serialized agent state
3. Keep keys out of query parameters and server logs
4. Encrypt full agent configuration where practical: API keys, prompts, model/provider settings, tool config
5. Handle decryption failures safely and recoverably
6. Warn users that browser extensions can access `window.location.hash`
