Chrome ExtensionDocument PiPZero-Knowledge Sync

Note It Down 📝

A note-taking Chrome Extension leveraging the modern Document Picture-in-Picture (PiP) API to create floating, borderless, always-on-top text editors.

The Backstory

"I know, the world didn't need another notepad. We have 4 billion of them, half VC-funded, all asking 'Sign up to continue' before you've typed your grocery list.

I just wanted notes to sync between my work laptop and personal laptop without using my personal account at work, my work account at home, or making Yet Another Account on Yet Another SaaS tool that'll eventually pivot to 'AI-powered productivity' and start emailing me.

So I built Note It Down. It's a notepad. Not reinventing anything, it just syncs without logging in, which felt rare enough to post about."

Specs & Info

  • Environment:Chrome Sandbox (Sidebar)
  • PiP Library:@pip-it-up/react
  • Security Layer:AES-GCM-256 E2EE
  • Relay DB:Cloudflare Workers + KV

Core Implementation

Floating PiP Window

Pops out any active note into a native, always-on-top Document Picture-in-Picture canvas. This strips away default browser margins, headers, and footers, creating a clean full-bleed editor pane to prevent alt-tabbing.

Shadow DOM Isolation

The sidebar launcher script mounts the React editor container within an isolated Shadow DOM context on host pages (like Reddit or GitHub), completely preventing host CSS sheets from breaking styles or leaking resets.

Zero-Knowledge Storage

A secure system using derived AES-GCM-256 keys. Notes are encrypted client-side using WebCrypto APIs before sync transmission. The worker server only receives encrypted blobs, keeping credentials private.

Cloudflare Edge KV Relay

Notes sync directly to your self-hosted Cloudflare Worker. Cloudflare's free tier provides 1GB storage and 100,000 requests/day, making cross-device relays fast, private, and free to host.

🧩 Powered by pip-it-up

This extension was designed to demonstrate how effortlessly you can wrap any React component in a native PiP frame using the `@pip-it-up/react` library.

Here is the exact controlled integration from the `EditorOverlay.tsx` file inside our codebase:

import { PipWrapper } from '@pip-it-up/react'

// ...inside the component...

<PipWrapper
  width={380}
  height={360}
  open={activeNoteId !== null}
  onOpenChange={(openState) => {
    if (!openState) {
      setActiveNoteId(null)
    }
  }}
  placeholder={<div style={{ display: 'none' }} />}
>
  {activeNoteId && (
    <NoteEditor
      key={activeNoteId}
      noteId={activeNoteId}
      onClose={() => setActiveNoteId(null)}
      theme={theme}
    />
  )}
</PipWrapper>

Zero-Knowledge Cryptography

Server-blind database read/write validation using WebCrypto

To achieve complete privacy, the extension employs a zero-knowledge structure using WebCrypto APIs:

  1. Deterministic Key Derivation (HKDF): From a single sync token, the extension derives three independent cryptographic values via HKDF-SHA-256 (using a fixed 32-byte zero salt):
    • `address`: The database lookup key (used as the URL segment on the worker).
    • `encryptionKey`: An AES-GCM 256-bit key used locally to encrypt/decrypt note database payloads.
    • `verifyKey`: Stored on the worker during the first sync to authenticate future writes.
  2. Write Token Derivation (HMAC): A `writeToken` is derived by computing an HMAC-SHA256 signature of `"nid-write-auth"` using the derived `verifyKey` bytes as the key.
  3. Zero-Knowledge Boundary: The client only transmits the `address`, the `writeToken`, and the encrypted `blob`. The `encryptionKey` and the raw `token` **never leave your device**.
  4. Worker Verification: The worker verifies subsequent writes by re-computing the HMAC signature using the stored `verifyKey` and comparing it to the incoming `writeToken`. The worker is content-blind (cannot read notes) and token-blind (does not know the raw token).
sequenceDiagram autonumber participant UI as Extension UI participant Sync as sync.ts (Orchestrator) participant Crypto as crypto.ts (WebCrypto) participant Storage as storage.ts (Chrome Local) participant Merge as merge.ts (Resolution) participant Worker as Cloudflare Worker & KV UI->>Sync: User clicks 'Save & Sync' activate Sync Sync->>Crypto: Derive address, encryptionKey, verifyKey, & writeToken Crypto-->>Sync: Keys returned (derived via HKDF & HMAC-SHA256) Sync->>Worker: GET /v1/:address (Retrieve remote database) alt Remote data exists Worker-->>Sync: 200 OK with { blob, blobHash } Sync->>Crypto: Decrypt remote blob using encryptionKey Crypto-->>Sync: Decrypted remote notes (JSON) else First-time sync (no remote data) Worker-->>Sync: 404 Not Found Sync->>Sync: Set isFirstWrite = true end Sync->>Storage: Retrieve current local notes Storage-->>Sync: Return local notes array Sync->>Merge: mergeNotes(local, remote) Note over Merge: Deterministically merges notes by version/updatedAt (creates conflict copies on divergence). Merge-->>Sync: Merged notes array returned Sync->>Storage: Save merged notes back to browser storage Sync->>Crypto: Encrypt merged notes using encryptionKey (AES-GCM-256) Crypto-->>Sync: Return new Encrypted Blob Sync->>Worker: PUT /v1/:address { blob, writeToken, verifyKey?, previousHash } Note over Worker: Worker validates writeToken against verifyKey and performs a Compare-and-Swap (CAS) check. alt 409 Conflict (Remote was updated during sync) Worker-->>Sync: 409 Conflict Note over Sync: Sync conflict detected! Start retry loop (Max 2 attempts) Sync->>Worker: GET /v1/:address (Fetch fresh remote) Worker-->>Sync: 200 OK with new { blob, blobHash } Sync->>Crypto: Decrypt new remote blob Sync->>Merge: Re-merge local database with new remote database Sync->>Storage: Save re-merged notes Sync->>Crypto: Encrypt new merged database Sync->>Worker: PUT /v1/:address { new blob, writeToken, previousHash: newHash } end Worker-->>Sync: 200 OK (Write Successful) deactivate Sync Sync-->>UI: Return SyncResult (success/syncedAt)

Technical Challenges & Solutions

ChallengeBrowser PolicyProblemSolution
1. Sandboxed Extension FrameExtensions block documentPictureInPicture in popup/sidebar frames.requestWindow() throws security exceptions in default extension overlays.Migrated UI mount targets to a webpage-injected sidebar drawer element.
2. Gesture Token ExpirationDocument PiP requires active user click tokens.Async messages (popup to script) expire user click context.Mounted UI directly in host page DOM so clicks act as native events.
3. Content Security Policy (CSP)Strict host pages block lazy scripts.Lazy load chunk scripts trigger CSP exceptions on GitHub/Google.Configured Vite packaging configurations to bundle build into a single IIFE script.
4. CSS IsolationInjected scripts are isolated from style namespaces.Copying document.styleSheets into PiP results in blank components.Injected the absolute CSS link of the extension to PiP head on instantiation.
5. Host Style BleedingHost global resets leak onto injected elements.Sites like Reddit overwrite margins/font alignments in our sidebar launcher.Encapsulated React app mounting container root inside an isolated Shadow DOM.
Explore GitHub Source

Other Selected Work Case Studies