Device Approval Flow for End-to-End Encryption in Thunderbolt: Complete Technical Guide
The device approval flow for end-to-end encryption in Thunderbolt ensures only explicitly trusted devices can access the Content Key (CK) through a hybrid ECDH-ML-KEM wrapping mechanism with canary-based proof of possession.
Thunderbolt implements a zero-knowledge architecture where a single Content Key (CK) encrypts all user data, yet this key never leaves trusted devices. The device approval flow governs how new devices gain access to this critical key while preventing unauthorized access through cryptographic proof mechanisms.
Overview of the Encryption Architecture
Content Key and Device Envelopes
Thunderbolt centers its encryption around one AES-256-GCM Content Key (CK) shared across all trusted devices. Each device maintains a unique device envelope that wraps this CK using the device's public keys.
According to the source code in src/crypto/primitives.ts, devices generate:
- An ECDH-P-256 key pair for elliptic-curve Diffie-Hellman operations
- An ML-KEM-768 key pair for post-quantum key encapsulation
The private keys never leave the device. The public keys transmit to the server during registration and encrypt the CK via hybrid wrapping (ECDH + ML-KEM) implemented in src/services/encryption.ts.
Device Status States
The approval flow manages three distinct device states defined in backend/src/db/powersync-schema.ts:
APPROVAL_PENDING: Device registered but awaiting approval from a trusted deviceTRUSTED: Device approved and possesses the wrapped CK envelopeREVOKED: Access terminated, envelope removed, device cannot sync
Step-by-Step Device Approval Flow
First Device Registration and Setup
When a user enables end-to-end encryption on their first device, the flow initializes the CK and establishes the initial trust chain:
-
Device Registration: The
registerThisDevicefunction insrc/services/encryption.tssends the device's public keys toPOST /devices. The backend creates a device row withstatus = TRUSTEDsince this is the inaugural device. -
CK Generation:
completeFirstDeviceSetupgenerates an extractable CK temporarily to create a recovery key mnemonic. This recovery key represents the only way to restore access if all devices are lost. -
Canary Creation: The system encrypts a known plaintext (canary) with the CK. This canary serves as proof of CK possession in subsequent approval operations.
-
Enveloping: The
wrapCKfunction hybrid-encrypts the CK using the device's own ECDH and ML-KEM public keys. The resulting envelope and canary upload toPOST /devices/:deviceId/envelope. -
Local Storage: The CK converts to non-extractable form and stores in IndexedDB via
src/crypto/key-storage.ts.
// First device onboarding (simplified)
import { getHttpClient } from '@/contexts';
import { registerThisDevice, completeFirstDeviceSetup } from '@/services/encryption';
async function onboardFirstDevice() {
const http = getHttpClient();
await registerThisDevice(http); // Creates TRUSTED row
const recoveryKey = await completeFirstDeviceSetup(http);
// Display recoveryKey to user exactly once
}
Adding a New Device (Approval Pending)
When a user attempts to add a second device, the approval flow ensures no device can self-authorize:
-
New Device Registration: The new device executes
registerThisDevice, sending its public keys toPOST /devices. The backend detects existing devices and setsstatus = APPROVAL_PENDING. -
Polling for Envelope: The new device repeatedly calls
GET /devices/me/envelope. Initially, this returns 404, and the UI displays "Waiting for approval." -
Trusted Device Notification: The PowerSync synchronization pushes the new pending device to all trusted devices, displaying an approval prompt to the user.
Trusted Device Approval Process
The critical security step occurs when a trusted device authorizes the pending device:
-
Fetch Own Envelope: The trusted device retrieves its wrapped CK via
GET /devices/me/envelope. -
Extract Canary: Using
extractCanarySecretinsrc/services/encryption.ts, the device decrypts the canary stored on the server. This proves possession of the CK without transmitting the key itself. -
Re-wrap CK: The
rewrapCKfunction (implemented insrc/crypto/primitives.ts) hybrid-encrypts the CK using the pending device's ECDH-P-256 and ML-KEM-768 public keys. -
Submit Envelope: The trusted device posts the new envelope to
POST /devices/:deviceId/envelope, including the canary secret as proof of authorization. -
Status Update: The backend verifies the canary secret, stores the envelope, and updates the device status to
TRUSTED.
// Approving a pending device from a trusted device
import { approveDevice } from '@/services/encryption';
async function approvePendingDevice(pendingId, ecdhPubB64, mlkemPubB64) {
const http = getHttpClient();
// This handles canary extraction and re-wrapping internally
await approveDevice(http, pendingId, ecdhPubB64, mlkemPubB64);
}
Device Denial and Revocation
If a user rejects a pending device or revokes an existing one:
-
Denial: The trusted device calls
denyDeviceWithProof(lines 86-93 insrc/services/encryption.ts), submitting the canary secret to prove authority. The server marks the deviceREVOKED. -
Revocation: When a device is revoked (either manually or via account deletion), the backend clears the envelope and sets
revoked_at. Clients detect this via PowerSync synchronization and execute a global reset:setSyncEnabled(false),localStorage.clear(), and removal of all local keys.
Source: docs/powersync-account-devices.md – "Revoking a device" section.
Security Guarantees and Canary Proof
The device approval flow implements several zero-knowledge security properties:
Zero-Knowledge Architecture: Private keys (ECDH and ML-KEM) and the Content Key never leave the device. The server only stores wrapped envelopes and public keys.
Canary-Based Proof of Possession: Every approval or denial request must present the canary secret—a decryption of known plaintext using the CK. This prevents spoofing attacks where a malicious actor might know a device ID but not possess the actual key.
Hybrid Post-Quantum Security: The use of ECDH-P-256 combined with ML-KEM-768 ensures protection against both classical and quantum computing attacks when wrapping the Content Key.
Implementation Examples
The following TypeScript functions demonstrate the complete device approval flow for end-to-end encryption in Thunderbolt:
import { getHttpClient } from '@/contexts';
import {
registerThisDevice,
completeFirstDeviceSetup,
approveDevice,
denyDeviceWithProof,
} from '@/services/encryption';
// 1️⃣ First device onboarding
async function onboardFirstDevice() {
const http = getHttpClient();
await registerThisDevice(http); // creates TRUSTED row
const recoveryKey = await completeFirstDeviceSetup(http);
// recoveryKey is shown to the user once
}
// 2️⃣ Adding a new device (on the new device side)
async function waitForApproval() {
const http = getHttpClient();
await registerThisDevice(http); // creates APPROVAL_PENDING row
// UI polls `GET /devices/me/envelope` until envelope appears
}
// 3️⃣ Approving a pending device (on an already-trusted device)
async function approvePending(pendingId, ecdhPubB64, mlkemPubB64) {
const http = getHttpClient();
await approveDevice(http, pendingId, ecdhPubB64, mlkemPubB64);
}
// 4️⃣ Denying a pending device
async function denyPending(pendingId) {
const http = getHttpClient();
await denyDeviceWithProof(http, pendingId);
}
Summary
- Single Content Key: Thunderbolt uses one AES-256-GCM Content Key (CK) for all data, wrapped per-device using hybrid ECDH-P-256 and ML-KEM-768 encryption.
- Explicit Trust Required: New devices register with
APPROVAL_PENDINGstatus and cannot access the CK until a trusted device explicitly approves them viaapproveDevice. - Cryptographic Proof: The approval flow requires presenting a canary secret (decrypted known plaintext) to prove possession of the CK, preventing spoofing attacks.
- Zero-Knowledge Design: Private keys and the CK never leave the device; the server only stores wrapped envelopes and public key material.
- Revocation Support: Devices can be denied or revoked via
denyDeviceWithProof, triggering immediate removal of the envelope and local data cleanup.
Frequently Asked Questions
What cryptographic standards does Thunderbolt use for device approval?
Thunderbolt implements a hybrid post-quantum approach using ECDH-P-256 for elliptic-curve Diffie-Hellman key exchange and ML-KEM-768 (Kyber) for quantum-resistant key encapsulation. The Content Key uses AES-256-GCM for symmetric encryption. These implementations are located in src/crypto/primitives.ts and src/services/encryption.ts.
How does the canary secret prevent spoofing attacks?
The canary secret is a decryption of known plaintext encrypted with the Content Key, stored on the server as a "canary." When approving or denying a device, the trusted device must present this secret to prove it actually possesses the CK and is not merely spoofing a device ID. This prevents attackers who might intercept device IDs from approving unauthorized devices without the actual encryption key.
What happens when a device is revoked in Thunderbolt?
When a device is revoked (either manually by the user or via account deletion), the backend clears the device's envelope from POST /devices/:deviceId/envelope, sets the revoked_at timestamp, and updates the status to REVOKED. The client detects this change via PowerSync synchronization and executes a global reset: disabling sync via setSyncEnabled(false), clearing localStorage, and removing all local cryptographic keys. This ensures revoked devices immediately lose access to encrypted data.
Can the server access the Content Key during the approval flow?
No. The server operates on a zero-knowledge principle and never possesses the unwrapped Content Key. During the device approval flow, the trusted device downloads its own wrapped envelope, unwraps the CK locally using its private keys, then re-wraps the CK using the pending device's public keys before uploading the new envelope. The server only facilitates storage of these wrapped envelopes and validation of the canary secret—it cannot decrypt the data itself.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →