Encryption Guide
Encryption Guide
Section titled “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.
Encryption Overview
Section titled “Encryption Overview”Algorithm
Section titled “Algorithm”| Component | Algorithm |
|---|---|
| Cipher | XChaCha20-Poly1305 |
| Key Derivation | PBKDF2-SHA256 |
| Iterations | 100,000 |
| Key Length | 256 bits (32 bytes) |
| Nonce Length | 192 bits (24 bytes) |
| Auth Tag | 128 bits (16 bytes) |
Why XChaCha20-Poly1305?
Section titled “Why XChaCha20-Poly1305?”- 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
Key Management
Section titled “Key Management”Key Derivation
Section titled “Key Derivation”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);}Salt Storage
Section titled “Salt Storage”- 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
Key Storage
Section titled “Key Storage”Browser Extension:
// Store encrypted in chrome.storage.localawait chrome.storage.local.set({ encryptedKey: await encryptWithDeviceKey(key)});Web App:
// Store in sessionStorage (cleared on tab close)sessionStorage.setItem('key', await encryptForSession(key));Encryption Process
Section titled “Encryption Process”-
Prepare Data
Section titled “Prepare Data”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); -
Generate Nonce
Section titled “Generate Nonce”Create a random 24-byte nonce:
const nonce = crypto.getRandomValues(new Uint8Array(24)); -
Encrypt
Section titled “Encrypt”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 -
Package
Section titled “Package”Combine nonce and ciphertext:
const encrypted = new Uint8Array(nonce.length + ciphertext.length);encrypted.set(nonce, 0);encrypted.set(ciphertext, nonce.length);// Base64 encode for APIconst base64 = btoa(String.fromCharCode(...encrypted)); -
Hash for Integrity
Section titled “Hash for Integrity”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('');
Decryption Process
Section titled “Decryption Process”-
Decode Base64
Section titled “Decode Base64”const encrypted = Uint8Array.from(atob(base64),c => c.charCodeAt(0)); -
Extract Nonce
Section titled “Extract Nonce”const nonce = encrypted.slice(0, 24);const ciphertext = encrypted.slice(24); -
Decrypt
Section titled “Decrypt”import { xchacha20poly1305 } from '@noble/ciphers/chacha';const cipher = xchacha20poly1305(key, nonce);const plaintext = cipher.decrypt(ciphertext);// Throws if auth tag verification fails -
Parse Data
Section titled “Parse Data”const json = new TextDecoder().decode(plaintext);const bookmark = JSON.parse(json);
Using @bitmarks.sh/core
Section titled “Using @bitmarks.sh/core”The core package provides high-level encryption utilities:
Encrypting Objects
Section titled “Encrypting Objects”import { encryptObject, decryptObject } from '@bitmarks.sh/core';
// Encryptconst bookmark = { id: 'bm_1', title: 'Example', url: 'https://example.com' };const encrypted = await encryptObject(bookmark, key);// Returns base64 string
// Decryptconst decrypted = await decryptObject(encrypted, key);// Returns original objectEncrypting Strings
Section titled “Encrypting Strings”import { encryptString, decryptString } from '@bitmarks.sh/core';
const secret = 'My secret note';const encrypted = await encryptString(secret, key);const decrypted = await decryptString(encrypted, key);Hashing Data
Section titled “Hashing Data”import { hashData } from '@bitmarks.sh/core';
const data = new TextEncoder().encode('Hello, World!');const hash = await hashData(data);// Returns hex stringKey Manager
Section titled “Key Manager”import { KeyManager } from '@bitmarks.sh/core';
// Create key managerconst keyManager = new KeyManager();
// Derive key from passwordconst key = await keyManager.deriveKey(password, salt);
// Store key securelyawait keyManager.storeKey(key);
// Retrieve stored keyconst storedKey = await keyManager.getKey();
// Clear key from memorykeyManager.clearKey();R2 Storage Encryption
Section titled “R2 Storage Encryption”Exports stored in R2 use Server-Side Encryption with Customer-Provided Keys (SSE-C):
import { uploadEncrypted, downloadEncrypted } from '@bitmarks.sh/core';
// Upload encrypted to R2await uploadEncrypted( bucket, // R2Bucket binding 'exports/file.dat', // Key data, // Uint8Array encryptionKey // Your key);
// Download and decrypt from R2const data = await downloadEncrypted( bucket, 'exports/file.dat', encryptionKey);Security Considerations
Section titled “Security Considerations”Key Protection
Section titled “Key Protection”- Never transmit the key over the network
- Never log the key anywhere
- Clear from memory when not in use
- Use secure storage appropriate for the platform
Nonce Uniqueness
Section titled “Nonce Uniqueness”Each encryption operation must use a unique nonce:
// CORRECT: Generate random nonce each timeconst nonce = crypto.getRandomValues(new Uint8Array(24));
// WRONG: Reusing nonces breaks securityconst nonce = fixedNonce; // DON'T DO THISAuthentication
Section titled “Authentication”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');}Forward Secrecy
Section titled “Forward Secrecy”Consider rotating keys periodically:
- Decrypt all data with old key
- Generate new key
- Re-encrypt all data with new key
- Securely delete old key
Troubleshooting
Section titled “Troubleshooting”Decryption Fails
Section titled “Decryption Fails”- Wrong key: Verify the key matches what was used for encryption
- Corrupted data: Check if data was modified in transit
- Wrong nonce: Ensure nonce extraction is correct (first 24 bytes)
Key Derivation Issues
Section titled “Key Derivation Issues”- Salt mismatch: Use the same salt for derivation
- Encoding issues: Ensure consistent text encoding (UTF-8)
- Iteration count: Must match exactly (100,000)
Performance
Section titled “Performance”For large datasets:
- Use Web Workers to avoid blocking UI
- Process in batches
- Consider streaming encryption for exports