# Thunderbolt E2E Encryption Key Hierarchy: How Thunderbird Secures Your Sync Data

> Explore the Thunderbolt E2E encryption key hierarchy. Learn how a single AES-256-GCM content key secures your sync data with unique hybrid envelope protection.

- Repository: [Thunderbird/thunderbolt](https://github.com/thunderbird/thunderbolt)
- Tags: internals
- Published: 2026-04-19

---

**Thunderbolt uses a two-tier key hierarchy where a single AES-256-GCM content key encrypts all user data, and each device receives a unique hybrid envelope combining ECDH P-256 and ML-KEM-768 to protect that content key.**

The **Thunderbolt E2E encryption key hierarchy** powers Thunderbird's zero-knowledge sync architecture, ensuring that user data remains encrypted client-side before reaching Mozilla's servers. This article examines the `thunderbird/thunderbolt` repository to break down how cryptographic keys are generated, wrapped, and stored across devices.

## Understanding the Thunderbolt Key Hierarchy Structure

The hierarchy centers on a single content key protected by device-specific envelopes. This design ensures that compromising one device does not expose the raw content key in plaintext to other devices, while maintaining synchronization capability across a user's ecosystem.

### The Content Key (CK)

The **content key (CK)** is a single AES-256-GCM symmetric key that encrypts all user data columns flagged for encryption. According to the source code in [`src/crypto/primitives.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/crypto/primitives.ts), the CK is generated once during initial setup via the `generateCK` function and remains identical across all devices belonging to the user. This key never leaves a device in unencrypted form.

### Hybrid Device Envelopes

Each device receives a unique **device envelope** that wraps the CK. The envelope uses a **hybrid post-quantum scheme** combining two independent key exchange mechanisms:

- **ECDH P-256**: A classical elliptic-curve Diffie-Hellman key pair
- **ML-KEM-768**: A Module Lattice-based Key Encapsulation Mechanism (post-quantum resistant)

Both mechanisms operate independently; an attacker must break both the classical and post-quantum algorithms to extract the content key.

## Cryptographic Primitives in Thunderbolt

The [`src/crypto/primitives.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/crypto/primitives.ts) file implements the core cryptographic operations that underpin the key hierarchy.

### Device Key Generation

When a user enables sync, the device generates its identity keys. The `generateKeyPair` function creates an ECDH P-256 key pair, while `generateMlKemKeyPair` generates the ML-KEM-768 key pair. Private keys are marked as non-extractable and never leave the device's secure storage.

```typescript
import { generateKeyPair, generateMlKemKeyPair } from './crypto/primitives'
import { storeKeyPair } from './crypto/key-storage'

// ECDH P‑256 key pair
const ecdhPair = await generateKeyPair()

// ML‑KEM‑768 key pair (post‑quantum)
const { publicKey: mlkemPub, secretKey: mlkemSec } = generateMlKemKeyPair()

await storeKeyPair(
  ecdhPair.privateKey,
  ecdhPair.publicKey,
  mlkemPub,
  mlkemSec,
)

```

*(source: [`primitives.ts`](https://github.com/thunderbird/thunderbolt/blob/main/primitives.ts) for generation, [`key-storage.ts`](https://github.com/thunderbird/thunderbolt/blob/main/key-storage.ts) for storage)*

### Content Key Generation

The `generateCK` function creates the AES-256-GCM content key. During initial setup, the key is generated as extractable to allow encoding into a recovery phrase, then re-imported as non-extractable for operational use.

```typescript
import { generateCK, reimportAsNonExtractable } from './crypto/primitives'
import { encodeRecoveryKey } from './crypto/recovery-key'
import { storeCK } from './crypto/key-storage'

// First‑device setup – make CK extractable so we can encode it
const ckExtractable = await generateCK(true)

// Encode as 24‑word mnemonic (shown once to the user)
const recoveryPhrase = await encodeRecoveryKey(ckExtractable)

// Re‑import as non‑extractable before persisting
const ck = await reimportAsNonExtractable(ckExtractable)

await storeCK(ck)

```

*(source: [`primitives.ts`](https://github.com/thunderbird/thunderbolt/blob/main/primitives.ts) for `generateCK` / `reimportAsNonExtractable`, [`recovery-key.ts`](https://github.com/thunderbird/thunderbolt/blob/main/recovery-key.ts) for encoding)*

## How Device Envelopes Work in Thunderbolt E2E Encryption

The hybrid envelope mechanism ensures that the content key remains protected even if one cryptographic primitive is compromised.

### The Hybrid Wrapping Process

When wrapping the CK for a target device, the `wrapCK` function performs the following steps:

1. **Ephemeral ECDH exchange**: Generates an ephemeral P-256 key pair and computes a shared secret with the target device's public key
2. **ML-KEM-768 encapsulation**: Encapsulates a shared secret against the target device's ML-KEM public key, producing a ciphertext
3. **Key derivation**: Feeds both shared secrets into HKDF with the info parameter `"thunderbolt‑hybrid‑ck‑wrap‑v1"`
4. **AES Key Wrap**: Uses the derived key to wrap the CK with AES-KW-256

The resulting envelope format is `[version][ephemeral‑ECDH‑pub][ml‑kem‑ciphertext][wrapped‑CK]`.

```typescript
import { wrapCK } from './crypto/primitives'
import { getKeyPair } from './crypto/key-storage'

// Assume we already have the local CK and the target device’s public keys
const localCK = await getCK()
const targetKeys = await getKeyPair() // e.g., fetched from server for a new device

if (!localCK || !targetKeys) throw new Error('Missing keys')

// Wrap CK for the target device
const envelopeBase64 = await wrapCK(
  localCK,
  targetKeys.ecdhPublicKey,
  targetKeys.mlkemPublicKey,
)

// Send `envelopeBase64` to the backend; the target device will unwrap it.

```

*(source: [`primitives.ts`](https://github.com/thunderbird/thunderbolt/blob/main/primitives.ts) – `wrapCK`)*

### Envelope Unwrapping

The target device uses `unwrapCK` with its private keys to reverse the process:

```typescript
import { unwrapCK } from './crypto/primitives'
import { getKeyPair } from './crypto/key-storage'

// Retrieve own private keys
const myKeys = await getKeyPair()
if (!myKeys) throw new Error('Key pair missing')

// `envelopeBase64` comes from the server
const ck = await unwrapCK(envelopeBase64, myKeys.ecdhPrivateKey, myKeys.mlkemSecretKey)

// Persist the CK for future encrypt/decrypt operations
await storeCK(ck)

```

*(source: [`primitives.ts`](https://github.com/thunderbird/thunderbolt/blob/main/primitives.ts) – `unwrapCK`)*

## Recovery and Key Management

Beyond the active sync keys, Thunderbolt provides mechanisms for disaster recovery and secure storage.

### Recovery Key Export

The content key can be exported exactly once as a **24-word BIP-39 mnemonic**. This recovery key allows users to restore their encrypted data if all devices are lost. The encoding and decoding logic resides in [`src/crypto/recovery-key.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/crypto/recovery-key.ts).

```typescript
import { decodeRecoveryKey } from './crypto/recovery-key'
import { storeCK } from './crypto/key-storage'

const userPhrase = 'abandon ability ...' // 24 words supplied by the user
const extractedCK = await decodeRecoveryKey(userPhrase)

// Store the recovered CK (non‑extractable) for normal use
await storeCK(extractedCK)

```

*(source: [`recovery-key.ts`](https://github.com/thunderbird/thunderbolt/blob/main/recovery-key.ts))*

### Canary Verification

A **canary** value—a fixed plaintext encrypted with the CK—is stored on the server. This serves two purposes: it verifies that a supplied recovery phrase correctly decodes to the right content key, and it signals whether end-to-end encryption has been fully configured for the account. The canary mechanism is documented in [`docs/e2e-encryption.md`](https://github.com/thunderbird/thunderbolt/blob/main/docs/e2e-encryption.md).

### Secure Key Storage

All keys persist locally in **IndexedDB** via [`src/crypto/key-storage.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/crypto/key-storage.ts). The storage layer maintains distinct IDs for the ECDH key pair, ML-KEM keys, and the content key, enabling atomic transactions for full wipes or selective clearing.

## Summary

- **Thunderbolt's E2E encryption key hierarchy** uses a single AES-256-GCM content key (CK) shared across all user devices, wrapped individually for each device.
- **Hybrid post-quantum protection** combines ECDH P-256 and ML-KEM-768 in every device envelope, ensuring resistance against both classical and quantum attacks.
- **Key operations** are implemented in [`src/crypto/primitives.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/crypto/primitives.ts), including `generateCK`, `wrapCK`, and `unwrapCK` for the hybrid envelope scheme.
- **Recovery mechanism** exports the CK as a 24-word BIP-39 mnemonic via [`src/crypto/recovery-key.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/crypto/recovery-key.ts), allowing account restoration without devices.
- **Local persistence** uses IndexedDB through [`src/crypto/key-storage.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/crypto/key-storage.ts), storing device keys and the CK with non-extractable flags for security.

## Frequently Asked Questions

### What happens if I lose all my devices but have my recovery phrase?

You can restore access to your encrypted data by using the 24-word recovery phrase. The `decodeRecoveryKey` function in [`src/crypto/recovery-key.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/crypto/recovery-key.ts) converts the mnemonic back into the original AES-256-GCM content key, which you can then store locally using `storeCK` to resume sync operations.

### Why does Thunderbolt use both ECDH and ML-KEM-768?

Thunderbolt implements a **hybrid post-quantum** approach to protect against "harvest now, decrypt later" attacks. While ECDH P-256 provides proven classical security, ML-KEM-768 (Module Lattice-based Key Encapsulation Mechanism) offers protection against future quantum computing attacks. An attacker must break both algorithms simultaneously to compromise the content key.

### Where are my private keys stored?

Private keys never leave your device. They are stored in the browser's **IndexedDB** via [`src/crypto/key-storage.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/crypto/key-storage.ts), marked as non-extractable to prevent JavaScript exfiltration. The storage layer maintains separate entries for your ECDH private key, ML-KEM secret key, and the content key, allowing atomic operations for secure deletion or rotation.

### How does Thunderbolt verify my recovery phrase is correct without storing it?

Thunderbolt uses a **canary** value—a fixed plaintext encrypted with the content key and stored on the server. When you enter a recovery phrase, the system derives the candidate content key, attempts to decrypt the canary, and verifies the resulting plaintext matches the expected value. This confirms the phrase correctly recovers the original content key without ever transmitting the phrase itself to the server.