Skip to content

Encryption Guide

BitMarks implements true end-to-end encryption (E2EE). Your bookmark data is encrypted on your device before it ever leaves, and only you hold the keys to decrypt it.

ComponentAlgorithm
CipherXChaCha20-Poly1305
Key DerivationPBKDF2-SHA256
Iterations100,000
Key Length256 bits (32 bytes)
Nonce Length192 bits (24 bytes)
Auth Tag128 bits (16 bytes)
  • Security: Equivalent security to AES-256-GCM
  • Performance: Faster in software (no hardware acceleration needed)
  • Nonce Safety: 24-byte nonce means safe random nonces (no counter needed)
  • Simplicity: Authenticated encryption in one operation

Your encryption key is derived from your password using PBKDF2:

async function deriveKey(
password: string,
salt: Uint8Array
): Promise<Uint8Array> {
const encoder = new TextEncoder();
const passwordBuffer = encoder.encode(password);
// Import password as key material
const keyMaterial = await crypto.subtle.importKey(
'raw',
passwordBuffer,
'PBKDF2',
false,
['deriveBits']
);
// Derive 256-bit key
const derivedBits = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
256
);
return new Uint8Array(derivedBits);
}
  • A unique salt is generated for each user at registration
  • The salt is stored server-side (it’s not secret)
  • The salt ensures different users with the same password have different keys

Browser Extension:

// Store encrypted in chrome.storage.local
await chrome.storage.local.set({
encryptedKey: await encryptWithDeviceKey(key)
});

Web App:

// Store in sessionStorage (cleared on tab close)
sessionStorage.setItem('key', await encryptForSession(key));

  1. Convert your bookmark to JSON:

    const bookmark = {
    id: 'bm_abc123',
    title: 'Example',
    url: 'https://example.com',
    source: 'bookmarks'
    };
    const plaintext = JSON.stringify(bookmark);
    const data = new TextEncoder().encode(plaintext);
  2. Create a random 24-byte nonce:

    const nonce = crypto.getRandomValues(new Uint8Array(24));
  3. Use XChaCha20-Poly1305 to encrypt:

    import { xchacha20poly1305 } from '@noble/ciphers/chacha';
    const cipher = xchacha20poly1305(key, nonce);
    const ciphertext = cipher.encrypt(data);
    // ciphertext includes the 16-byte auth tag
  4. Combine nonce and ciphertext:

    const encrypted = new Uint8Array(nonce.length + ciphertext.length);
    encrypted.set(nonce, 0);
    encrypted.set(ciphertext, nonce.length);
    // Base64 encode for API
    const base64 = btoa(String.fromCharCode(...encrypted));
  5. Create a hash of the plaintext for verification:

    const hashBuffer = await crypto.subtle.digest('SHA-256', data);
    const hashArray = new Uint8Array(hashBuffer);
    const hash = Array.from(hashArray)
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');

  1. const encrypted = Uint8Array.from(
    atob(base64),
    c => c.charCodeAt(0)
    );
  2. const nonce = encrypted.slice(0, 24);
    const ciphertext = encrypted.slice(24);
  3. import { xchacha20poly1305 } from '@noble/ciphers/chacha';
    const cipher = xchacha20poly1305(key, nonce);
    const plaintext = cipher.decrypt(ciphertext);
    // Throws if auth tag verification fails
  4. const json = new TextDecoder().decode(plaintext);
    const bookmark = JSON.parse(json);

The core package provides high-level encryption utilities:

import { encryptObject, decryptObject } from '@bitmarks.sh/core';
// Encrypt
const bookmark = { id: 'bm_1', title: 'Example', url: 'https://example.com' };
const encrypted = await encryptObject(bookmark, key);
// Returns base64 string
// Decrypt
const decrypted = await decryptObject(encrypted, key);
// Returns original object
import { encryptString, decryptString } from '@bitmarks.sh/core';
const secret = 'My secret note';
const encrypted = await encryptString(secret, key);
const decrypted = await decryptString(encrypted, key);
import { hashData } from '@bitmarks.sh/core';
const data = new TextEncoder().encode('Hello, World!');
const hash = await hashData(data);
// Returns hex string
import { KeyManager } from '@bitmarks.sh/core';
// Create key manager
const keyManager = new KeyManager();
// Derive key from password
const key = await keyManager.deriveKey(password, salt);
// Store key securely
await keyManager.storeKey(key);
// Retrieve stored key
const storedKey = await keyManager.getKey();
// Clear key from memory
keyManager.clearKey();

Exports stored in R2 use Server-Side Encryption with Customer-Provided Keys (SSE-C):

import { uploadEncrypted, downloadEncrypted } from '@bitmarks.sh/core';
// Upload encrypted to R2
await uploadEncrypted(
bucket, // R2Bucket binding
'exports/file.dat', // Key
data, // Uint8Array
encryptionKey // Your key
);
// Download and decrypt from R2
const data = await downloadEncrypted(
bucket,
'exports/file.dat',
encryptionKey
);

  1. Never transmit the key over the network
  2. Never log the key anywhere
  3. Clear from memory when not in use
  4. Use secure storage appropriate for the platform

Each encryption operation must use a unique nonce:

// CORRECT: Generate random nonce each time
const nonce = crypto.getRandomValues(new Uint8Array(24));
// WRONG: Reusing nonces breaks security
const nonce = fixedNonce; // DON'T DO THIS

XChaCha20-Poly1305 provides authenticated encryption:

  • If data is tampered with, decryption fails
  • Always check for decryption errors
try {
const plaintext = cipher.decrypt(ciphertext);
} catch (error) {
// Authentication failed - data was tampered
console.error('Decryption failed: data integrity compromised');
}

Consider rotating keys periodically:

  1. Decrypt all data with old key
  2. Generate new key
  3. Re-encrypt all data with new key
  4. Securely delete old key

  1. Wrong key: Verify the key matches what was used for encryption
  2. Corrupted data: Check if data was modified in transit
  3. Wrong nonce: Ensure nonce extraction is correct (first 24 bytes)
  1. Salt mismatch: Use the same salt for derivation
  2. Encoding issues: Ensure consistent text encoding (UTF-8)
  3. Iteration count: Must match exactly (100,000)

For large datasets:

  • Use Web Workers to avoid blocking UI
  • Process in batches
  • Consider streaming encryption for exports