Server-Blind Sync Layer
Building a zero-knowledge, end-to-end encrypted note sync relay using client-side key derivations and serverless database storage.
The Architectural Problem
When sync layers are designed, developers typically store credentials (passwords, auth sessions) and decryptable data directly in a centralized database. This exposes user notes to breaches and data snooping.
To address this on Note It Down, we built a zero-knowledge boundary: a serverless relay where your sync token and encryption keys never leave the browser. The database (Cloudflare KV) only stores encrypted bytes, and the edge worker verifies write requests without knowing the underlying raw credentials.
Sync Sequence Flow
Step-by-step key derivation, decryption, merging, and compare-and-swap write validation
1. Key Derivation Pipeline (HKDF)
To prevent key reuse (using the same key for authentication and encryption), we run your raw sync token through **HKDF-SHA-256** (HMAC-based Extract-and-Expand Key Derivation Function). This derives three separate keys with distinct domain tags:
Derived using the tag `"nid-db-address"`. Since HKDF is deterministic, the same sync token always generates the same 32-byte address, acting as the database lookup row identifier.
Derived using `"nid-encryption-key"`. Creates a unique 256-bit AES key used on-device to encrypt note arrays using the AES-GCM authenticated cipher.
Derived using `"nid-verification-key"`. Transmitted to the Cloudflare Worker *only on the first database write* to authenticate future write attempts.
2. Client-Side WebCrypto Implementation
Below is the TypeScript code running in the Chrome Extension background context to derive these cryptographic components using the native WebCrypto API:
async function deriveSyncKeys(rawToken: string) {
const encoder = new TextEncoder();
const tokenBytes = encoder.encode(rawToken);
// Import raw token bytes into an HKDF-capable CryptoKey object
const baseKey = await crypto.subtle.importKey(
"raw",
tokenBytes,
{ name: "HKDF" },
false,
["deriveBits", "deriveKey"]
);
// 1. Derive lookup address (32 bytes)
const addressBuffer = await crypto.subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array(32), // 32-byte zero salt
info: encoder.encode("nid-db-address")
},
baseKey,
256 // 32 bytes * 8 bits
);
const address = arrayBufferToHex(addressBuffer);
// 2. Derive AES-GCM Encryption Key
const encryptionKey = await crypto.subtle.deriveKey(
{
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array(32),
info: encoder.encode("nid-encryption-key")
},
baseKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
// 3. Derive Verification Key (used for HMAC write token generation)
const verifyKey = await crypto.subtle.deriveKey(
{
name: "HKDF",
hash: "SHA-256",
salt: new Uint8Array(32),
info: encoder.encode("nid-verification-key")
},
baseKey,
{ name: "HMAC", hash: "SHA-256", length: 256 },
false,
["sign"]
);
// 4. Compute Write Signature Auth Token (HMAC signature of static tag)
const signatureBuffer = await crypto.subtle.sign(
"HMAC",
verifyKey,
encoder.encode("nid-write-auth")
);
const writeToken = arrayBufferToHex(signatureBuffer);
return { address, encryptionKey, writeToken };
}3. Server-Blind Worker Verification
The Cloudflare Worker accepts a `PUT /v1/:address` request containing the encrypted `blob` and the client's `writeToken`. The worker operates blindly:
// Inside Cloudflare Worker routing controller...
export async function handlePutRequest(request: Request, env: Env) {
const { address } = request.params;
const { blob, writeToken, verifyKeyHex, previousHash } = await request.json();
const record = await env.NOTES_KV.get(address, { type: "json" });
if (!record) {
// First-time write: store the verifyKey and the encrypted blob
await env.NOTES_KV.put(address, JSON.stringify({
blob,
verifyKey: verifyKeyHex,
hash: await sha256(blob)
}));
return new Response("Created", { status: 201 });
}
// Subsequent writes: Import verifyKey and verify client's writeToken
const verifyKey = await importHmacKey(record.verifyKey);
const isValid = await verifyHmacSignature(verifyKey, writeToken, "nid-write-auth");
if (!isValid) {
return new Response("Unauthorized Signature", { status: 401 });
}
// Compare-and-Swap (CAS) check to prevent overwrite race conditions
if (record.hash !== previousHash) {
return new Response("Conflict: database updated elsewhere", { status: 409 });
}
// Update blob and update current hash
await env.NOTES_KV.put(address, JSON.stringify({
blob,
verifyKey: record.verifyKey,
hash: await sha256(blob)
}));
return new Response("OK", { status: 200 });
}Platform Benefits
By offloading all cryptographic processes to the client and deploying an authentication-blind KV worker, users secure full ownership of their data. Even in the event of a database breach on Cloudflare, the attacker only acquires encrypted blobs and verification signatures, which cannot be decrypted without the local device key.
Scalability
The edge worker architecture operates without state, VMs, or persistent databases. With Cloudflare KV edge caching, read requests resolve globally near-instantly, scaling note synchronization at zero cost within Cloudflare's generous free tier limitations.