- Initialize Normogen health tracking platform - Add comprehensive project documentation - Add zero-knowledge encryption implementation guide - Set up .gitignore for Rust/Node.js/mobile development - Create README with project overview and roadmap Project is currently in planning phase with no implementation code yet.
32 KiB
Zero-Knowledge Encryption Implementation Guide
Table of Contents
- Proton-Style Encryption for MongoDB
- Shareable Links with Embedded Passwords
- Security Best Practices
- Advanced Features
Proton-Style Encryption for MongoDB
Architecture Overview
Application Layer (Client Side)
├── Encryption/Decryption happens HERE
├── Queries constructed with encrypted searchable fields
└── Data never leaves application unencrypted
MongoDB (Server Side)
└── Stores only encrypted data
Implementation Approaches
1. Application-Level Encryption (Recommended)
Encrypt sensitive fields before they reach MongoDB:
// Using Node.js with crypto
const crypto = require('crypto');
class EncryptedMongo {
constructor(encryptionKey) {
this.algorithm = 'aes-256-gcm';
this.key = encryptionKey;
}
encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
data: encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
decrypt(encryptedObj) {
const decipher = crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(encryptedObj.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(encryptedObj.authTag, 'hex'));
let decrypted = decipher.update(encryptedObj.data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// Example usage
const encryptedMongo = new EncryptedMongo(userEncryptionKey);
// Storing encrypted data
await db.collection('users').insertOne({
_id: userId,
email: encryptedMongo.encrypt('user@example.com'),
ssn: encryptedMongo.encrypt('123-45-6789'),
// Non-sensitive fields can remain plaintext for queries
username: 'johndoe', // plaintext for searching
createdAt: new Date()
});
// Retrieving and decrypting
const user = await db.collection('users').findOne({ _id: userId });
const decryptedEmail = encryptedMongo.decrypt(user.email);
2. Deterministic Encryption for Searchable Fields
For fields you need to query (like email), use deterministic encryption:
const crypto = require('crypto');
function deterministicEncrypt(text, key) {
const hmac = crypto.createHmac('sha256', key);
const iv = hmac.update(text).digest();
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
}
// Same input always produces same output
const encryptedEmail1 = deterministicEncrypt('user@example.com', key);
const encryptedEmail2 = deterministicEncrypt('user@example.com', key);
console.log(encryptedEmail1 === encryptedEmail2); // true
// Now you can query by encrypted email
await db.collection('users').findOne({
emailSearchable: deterministicEncrypt('user@example.com', userKey)
});
3. Field-Level Encryption Strategy
// Document structure example
{
_id: ObjectId("..."),
// Public/indexable fields (plaintext)
username: "johndoe",
userId: "unique-id-123",
createdAt: ISODate("2026-01-07"),
// Deterministically encrypted (searchable but not readable)
email_searchable: "a1b2c3d4...", // for equality queries
username_searchable: "e5f6g7h8...",
// Randomly encrypted (not searchable, most secure)
sensitiveData: {
encrypted: true,
data: "x9y8z7...",
iv: "1a2b3c...",
authTag: "4d5e6f..."
}
}
Key Management Strategy
// Never store the master key in MongoDB!
class KeyManager {
constructor() {
this.masterKey = process.env.MASTER_ENCRYPTION_KEY; // Environment variable
}
// Derive unique key per user/document
deriveUserKey(userId) {
return crypto
.createHash('sha256')
.update(`${this.masterKey}:${userId}`)
.digest();
}
// For additional security, use KDF (scrypt/argon2)
async deriveKeyWithScrypt(userId) {
return new Promise((resolve, reject) => {
crypto.scrypt(
this.masterKey,
`salt:${userId}`,
32,
(err, derivedKey) => {
if (err) reject(err);
else resolve(derivedKey);
}
);
});
}
}
MongoDB Schema Example with Mongoose
const mongoose = require('mongoose');
const encryptedFieldSchema = new mongoose.Schema({
data: { type: String, required: true },
iv: { type: String, required: true },
authTag: { type: String, required: true }
});
const userSchema = new mongoose.Schema({
// Public fields
username: { type: String, index: true },
userId: { type: String, unique: true },
// Searchable encrypted fields
emailEncrypted: { type: String, index: true },
// Fully encrypted data
profile: encryptedFieldSchema,
financialData: encryptedFieldSchema,
createdAt: { type: Date, default: Date.now }
});
// Middleware to encrypt before save
userSchema.pre('save', async function(next) {
if (this.isModified('profile')) {
const encrypted = encryptionService.encrypt(this.profile);
this.profile = encrypted;
}
next();
});
MongoDB Field Level Encryption (Official Alternative)
MongoDB offers official client-side field level encryption:
npm install mongodb-client-encryption
const { MongoClient } = require('mongodb');
const { ClientEncryption } = require('mongodb-client-encryption');
const encryption = new ClientEncryption(
client,
{
keyVaultNamespace: 'encryption.__keyVault',
kmsProviders: {
local: { key: masterKey }
}
}
);
// Auto-encrypt specific fields
const encryptedValue = await encryption.encrypt(
'sensitive-data',
'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'
);
Shareable Links with Embedded Passwords
Architecture Overview
1. User has encrypted data in MongoDB
2. Creates a public share with a password
3. Generates a shareable link with encrypted password
4. External user clicks link → password extracted → data decrypted
Implementation
1. Share Link Structure
https://yourapp.com/share/{shareId}?key={encryptedShareKey}
Components:
- shareId: References the shared data in MongoDB
- key: Encrypted version of the decryption password (self-contained in URL)
2. MongoDB Schema for Shared Data
const mongoose = require('mongoose');
const shareSchema = new mongoose.Schema({
shareId: { type: String, unique: true, required: true },
// Reference to original encrypted data
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
documentId: { type: mongoose.Schema.Types.ObjectId },
collectionName: { type: String },
// The encrypted data (encrypted with share-specific password)
encryptedData: {
data: { type: String, required: true },
iv: { type: String, required: true },
authTag: { type: String, required: true }
},
// Metadata
createdAt: { type: Date, default: Date.now },
expiresAt: { type: Date },
accessCount: { type: Number, default: 0 },
maxAccessCount: { type: Number }, // Optional: limit access
// Optional: Additional security
allowedEmails: [String], // Restrict to specific emails
requireEmailVerification: { type: Boolean, default: false },
isRevoked: { type: Boolean, default: false },
revokedAt: Date
});
const Share = mongoose.model('Share', shareSchema);
3. Creating a Shareable Link
const crypto = require('crypto');
class ShareService {
constructor() {
this.algorithm = 'aes-256-gcm';
}
// Generate a random share password
generateSharePassword() {
return crypto.randomBytes(32).toString('hex');
}
// Generate unique share ID
generateShareId() {
return crypto.randomBytes(16).toString('hex');
}
// Encrypt data with share password
encryptData(data, password) {
const salt = crypto.randomBytes(32);
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, key, iv);
let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
data: encrypted,
iv: iv.toString('hex'),
salt: salt.toString('hex'),
authTag: authTag.toString('hex')
};
}
// Create shareable link
async createShare(userId, documentData, options = {}) {
// Generate a unique share password
const sharePassword = this.generateSharePassword();
// Encrypt the data with this password
const encryptedData = this.encryptData(documentData, sharePassword);
// Create share record
const shareId = this.generateShareId();
const share = await Share.create({
shareId,
userId,
encryptedData,
expiresAt: options.expiresAt,
maxAccessCount: options.maxAccessCount
});
// Generate the shareable link with embedded password
const shareLink = this.generateShareLink(shareId, sharePassword);
return {
shareId,
shareLink,
expiresAt: share.expiresAt
};
}
// Generate share link with encrypted password
generateShareLink(shareId, password) {
// Encrypt the password with a server-side master key
// This way the password is in the URL but not readable
const masterKey = process.env.SHARE_LINK_MASTER_KEY;
const encryptedPassword = this.encryptPasswordInUrl(password, masterKey);
return `https://yourapp.com/share/${shareId}?key=${encodeURIComponent(encryptedPassword)}`;
}
encryptPasswordInUrl(password, masterKey) {
// Use URL-safe base64 encoding
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, masterKey, iv);
let encrypted = cipher.update(password, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// URL-safe format: iv:authTag:encrypted
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
}
4. Accessing Shared Data (Backend)
class ShareAccessService {
constructor() {
this.algorithm = 'aes-256-gcm';
this.masterKey = process.env.SHARE_LINK_MASTER_KEY;
}
// Decrypt password from URL
decryptPasswordFromUrl(encryptedPassword) {
const parts = encryptedPassword.split(':');
const iv = Buffer.from(parts[0], 'hex');
const authTag = Buffer.from(parts[1], 'hex');
const encrypted = parts[2];
const decipher = crypto.createDecipheriv(this.algorithm, this.masterKey, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Decrypt share data
decryptShareData(encryptedData, password) {
const salt = Buffer.from(encryptedData.salt, 'hex');
const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
const iv = Buffer.from(encryptedData.iv, 'hex');
const authTag = Buffer.from(encryptedData.authTag, 'hex');
const decipher = crypto.createDecipheriv(this.algorithm, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedData.data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
// Access shared data
async accessShare(shareId, encryptedPassword) {
// Find share record
const share = await Share.findOne({ shareId });
if (!share) {
throw new Error('Share not found');
}
// Check if revoked
if (share.isRevoked) {
throw new Error('Share has been revoked');
}
// Check if expired
if (share.expiresAt && new Date() > share.expiresAt) {
throw new Error('Share has expired');
}
// Check access limit
if (share.maxAccessCount && share.accessCount >= share.maxAccessCount) {
throw new Error('Maximum access count reached');
}
// Decrypt password from URL
const password = this.decryptPasswordFromUrl(encryptedPassword);
// Decrypt data
const decryptedData = this.decryptShareData(share.encryptedData, password);
// Increment access count
share.accessCount += 1;
await share.save();
return {
data: decryptedData,
expiresAt: share.expiresAt
};
}
}
5. Express.js API Endpoint
const express = require('express');
const router = express.Router();
// POST /api/share - Create a new share
router.post('/share', async (req, res) => {
try {
const { userId, data, options } = req.body;
const shareService = new ShareService();
const result = await shareService.createShare(userId, data, options);
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// GET /api/share/:shareId - Access shared data
router.get('/share/:shareId', async (req, res) => {
try {
const { shareId } = req.params;
const { key } = req.query; // Encrypted password from URL
if (!key) {
return res.status(400).json({ error: 'Missing decryption key' });
}
const accessService = new ShareAccessService();
const result = await accessService.accessShare(shareId, key);
res.json(result);
} catch (error) {
res.status(404).json({ error: error.message });
}
});
module.exports = router;
6. Frontend: Share Access Page (React)
import React, { useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
function SharedDataView() {
const { shareId } = useParams();
const [searchParams] = useSearchParams();
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
try {
const key = searchParams.get('key');
if (!key) {
throw new Error('Invalid share link');
}
const response = await fetch(`/api/share/${shareId}?key=${encodeURIComponent(key)}`);
if (!response.ok) {
throw new Error('Failed to access shared data');
}
const result = await response.json();
setData(result.data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchData();
}, [shareId, searchParams]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h1>Shared Data</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
export default SharedDataView;
7. Creating a Share (Frontend)
async function createShare() {
const dataToShare = {
name: 'John Doe',
email: 'john@example.com',
// ... other fields
};
const response = await fetch('/api/share', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 'current-user-id',
data: dataToShare,
options: {
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
maxAccessCount: 10
}
})
});
const result = await response.json();
// result.shareLink is ready to share!
// Example: https://yourapp.com/share/a1b2c3d4...?key=e5f6g7h8...
return result.shareLink;
}
Security Best Practices
Key Management
-
Never store encryption keys in MongoDB
- Use environment variables
- Consider key management services (AWS KMS, HashiCorp Vault)
- Hardware Security Modules (HSMs) for production
-
Use different encryption keys per:
- User
- Document type
- Environment (dev/staging/prod)
-
Implement key rotation:
async function rotateKey(oldKey, newKey, collection) {
const documents = await collection.find().toArray();
for (const doc of documents) {
const decrypted = decrypt(doc.encryptedField, oldKey);
const reencrypted = encrypt(decrypted, newKey);
await collection.updateOne(
{ _id: doc._id },
{ $set: { encryptedField: reencrypted } }
);
}
}
Environment Variables
# .env
MASTER_ENCRYPTION_KEY=<random-64-character-hex-key>
SHARE_LINK_MASTER_KEY=<random-64-character-hex-key>
MONGODB_URI=mongodb://localhost:27017/yourdb
Generate master keys:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Backup Strategy
- Backup encrypted data with separate key backups
- Store keys in different physical locations
- Test restore procedures regularly
Limitations of Encrypted Database
- No range queries on encrypted numeric fields
- No regex/full-text search on encrypted text
- Indexing only works with deterministic encryption (less secure)
- Performance overhead from encryption/decryption
Advanced Features
1. Password Expiration
// Set expiration when creating share
const share = await createShare(userId, data, {
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
});
2. Access Limits
const share = await createShare(userId, data, {
maxAccessCount: 5 // Only 5 people can access
});
3. Additional Password Protection
// Require a separate password (not in URL)
const shareSchema = new mongoose.Schema({
// ... other fields
passwordRequired: { type: Boolean, default: false },
passwordHash: String
});
// User must enter password on page load
// Password is verified before decrypting data
4. Email Verification
// Restrict access to specific emails
const share = await createShare(userId, data, {
allowedEmails: ['user@example.com', 'trusted@domain.com'],
requireEmailVerification: true
});
5. Revocable Shares
// Middleware to check revocation
shareSchema.pre('save', function(next) {
if (this.isModified('isRevoked') && this.isRevoked) {
this.revokedAt = new Date();
}
next();
});
// Revoke a share
async function revokeShare(shareId) {
await Share.findOneAndUpdate(
{ shareId },
{ isRevoked: true }
);
}
Summary
This implementation provides:
✅ Zero-knowledge: MongoDB never stores unencrypted shared data
✅ Self-contained links: Password embedded in URL
✅ Access controls: Expiration, access limits, email restrictions
✅ Revocability: Users can revoke shares anytime
✅ Security: AES-256-GCM encryption with PBKDF2 key derivation
✅ User experience: One-click access for recipients
✅ Per-user encryption: Each user's data encrypted with unique keys
✅ Searchable encryption: Deterministic encryption for queryable fields
Dependencies
{
"dependencies": {
"mongoose": "^8.0.0",
"express": "^4.18.0",
"crypto": "built-in",
"mongodb-client-encryption": "^6.0.0"
}
}
Password Recovery in Zero-Knowledge Systems
The Core Problem
In a zero-knowledge system:
- User's password (or derived key) encrypts their data
- Server never sees the password in plaintext
- If password is lost, the data is mathematically unrecoverable
This is actually a feature, not a bug - it's what makes the system truly zero-knowledge!
Solution Approaches (Ranked by Security)
1. Secure Recovery Phrase (Recommended) ⭐
Give users a recovery phrase during signup that they must save securely.
class UserRegistration {
async registerUser(email, password) {
// Generate a random recovery key
const recoveryKey = crypto.randomBytes(32).toString('hex');
// Encrypt recovery key with user's password
const encryptedRecoveryKey = this.encryptWithPassword(
recoveryKey,
password
);
// Store encrypted recovery key in database
await User.create({
email,
passwordHash: await hashPassword(password), // For authentication only
encryptedRecoveryKey,
dataEncryptionKey: this.deriveKeyFromPassword(password)
});
// Display recovery key ONCE - never show again!
return {
userId: user.id,
recoveryKey: recoveryKey, // Show this to user
warning: "Save this key securely. You'll need it to recover your account."
};
}
}
// Recovery process
async function recoverAccount(email, recoveryKey, newPassword) {
const user = await User.findOne({ email });
// Decrypt the stored encrypted recovery key
// This is encrypted with the OLD password, but we have the recovery key
// So we can re-encrypt it with the NEW password
const dataEncryptionKey = this.decryptDataEncryptionKey(
user.encryptedRecoveryKey,
recoveryKey
);
// Re-encrypt data encryption key with new password
user.encryptedRecoveryKey = this.encryptWithPassword(
recoveryKey,
newPassword
);
user.dataEncryptionKey = this.deriveKeyFromPassword(newPassword);
await user.save();
return { success: true, message: "Account recovered successfully" };
}
Pros:
- ✅ Maintains zero-knowledge property
- ✅ User has full control
- ✅ No backdoors for attackers
Cons:
- ❌ Users might lose the recovery key
- ❌ Requires user education
2. Multi-Share Secret Splitting (Shamir's Secret Sharing) 🔐
Split the recovery key into multiple parts. User needs a threshold of parts to recover.
const secrets = require('secrets.js'); // npm install secrets.js
class ShamirRecovery {
async createUserWithShamirBackup(email, password) {
// Generate master encryption key
const masterKey = crypto.randomBytes(32);
// Split into shares (e.g., 5 shares, need 3 to recover)
const shares = secrets.share(
masterKey.toString('hex'),
3, // threshold
5 // total shares
);
// Store shares in different locations
const recoverySetup = {
userShare: shares[0], // Given to user
emailShare: shares[1], // Emailed to user
trustedContactShare: shares[2], // Sent to trusted contact
backupShare1: shares[3], // Stored in secure backup
backupShare2: shares[4] // Stored in separate location
};
// Encrypt master key with user's password
const encryptedMasterKey = this.encryptWithPassword(
masterKey,
password
);
await User.create({
email,
encryptedMasterKey,
shamirShares: {
userShare: recoverySetup.userShare,
// Other shares stored elsewhere
}
});
return {
recoveryShare: recoverySetup.userShare,
emailShare: recoverySetup.emailShare,
instructions: "Keep at least 3 of these 5 shares safe"
};
}
async recoverAccount(email, providedShares, newPassword) {
const user = await User.findOne({ email });
// User provides 3+ shares
if (providedShares.length < 3) {
throw new Error('Need at least 3 shares to recover');
}
// Combine shares to recover master key
const masterKeyHex = secrets.combine(providedShares);
const masterKey = Buffer.from(masterKeyHex, 'hex');
// Re-encrypt with new password
user.encryptedMasterKey = this.encryptWithPassword(
masterKey,
newPassword
);
await user.save();
return { success: true };
}
}
Pros:
- ✅ No single point of failure
- ✅ Flexible recovery options
- ✅ Still maintains zero-knowledge
Cons:
- ❌ More complex to implement
- ❌ Users might find it confusing
3. Trusted Contact Recovery 👥
Allow trusted contacts to help recover access.
class TrustedContactRecovery {
async setupTrustedContact(userId, contactEmail, userPassword) {
// Generate a recovery key for this contact
const recoveryKey = crypto.randomBytes(32);
// Encrypt recovery key with contact's public key
// (Contact would need to have an account too)
const encryptedRecoveryKey = crypto.publicEncrypt(
contactPublicKey,
recoveryKey
);
// Store the encrypted recovery share
await TrustedContact.create({
userId,
contactEmail,
encryptedRecoveryKey,
createdAt: new Date()
});
}
async initiateRecovery(userId) {
// Notify all trusted contacts
const contacts = await TrustedContact.find({ userId });
for (const contact of contacts) {
await sendEmail({
to: contact.contactEmail,
subject: 'Account Recovery Request',
body: 'Click to approve account recovery',
approvalLink: `/approve-recovery?token=${generateToken()}`
});
}
}
async recoverWithContactApproval(recoveryToken, newPassword) {
// Once enough contacts approve (e.g., 2 of 3)
const approvals = await RecoveryApproval.find({ token: recoveryToken });
if (approvals.length >= 2) {
// Combine encrypted shares and decrypt with contact private keys
// Then re-encrypt with new password
const masterKey = this.combineContactShares(approvals);
await this.resetPassword(userId, masterKey, newPassword);
return { success: true };
}
}
}
Pros:
- ✅ Social recovery mechanism
- ✅ No need to remember complex keys
- ✅ Still technically zero-knowledge
Cons:
- ❌ Requires trusted contacts to also use the service
- ❌ Social engineering risks
4. Time-Locked Recovery ⏰
Encrypt recovery keys with a future decryption (using blockchain or time-lock crypto).
class TimeLockedRecovery {
async createTimeLockedBackup(userId, password, lockDays) {
// Generate recovery key
const recoveryKey = crypto.randomBytes(32);
// Encrypt recovery key with user's password
const encryptedRecoveryKey = this.encryptWithPassword(
recoveryKey,
password
);
// Create a time-locked backup
// In production, use something like Drand or blockchain timelock
const timeLock = {
recoveryKey,
unlockDate: new Date(Date.now() + lockDays * 24 * 60 * 60 * 1000),
encryptedBackup: this.encryptForFuture(recoveryKey, lockDays)
};
await TimeLockBackup.create({
userId,
encryptedRecoveryKey,
timeLock,
createdAt: new Date()
});
}
async recoverWithTimeLock(userId) {
const backup = await TimeLockBackup.findOne({ userId });
if (new Date() < backup.timeLock.unlockDate) {
throw new Error('Backup not yet available');
}
// Decrypt using time-lock release
const recoveryKey = await this.decryptFromFuture(
backup.timeLock.encryptedBackup
);
return recoveryKey;
}
}
Pros:
- ✅ Automatic recovery after time period
- ✅ Prevents impulsive data loss
Cons:
- ❌ User must wait for lock period
- ❌ Complex implementation
What NOT to Do ❌
❌ Store Passwords in Plaintext
// NEVER DO THIS
await User.create({
email,
password: password // ❌ Defeats the whole purpose
});
❌ Store Encryption Keys on Server
// NEVER DO THIS
await User.create({
email,
encryptedData: data,
encryptionKey: key // ❌ Not zero-knowledge
});
❌ Backdoor Encryption
// NEVER DO THIS
const key = deriveKey(password);
const backdoorKey = process.env.BACKDOOR_KEY; // ❌ Huge security risk
Hybrid Approach (Practical Recommendation)
Combine multiple methods for different scenarios:
class HybridRecoverySystem {
async setupUserRecovery(email, password) {
const userId = await this.createUser(email, password);
// 1. Generate recovery phrase (primary method)
const recoveryPhrase = this.generateRecoveryPhrase();
// 2. Setup 2 trusted contacts (secondary method)
await this.setupTrustedContact(userId, 'contact1@email.com', password);
await this.setupTrustedContact(userId, 'contact2@email.com', password);
// 3. Create time-locked backup (last resort)
await this.createTimeLockedBackup(userId, password, 30); // 30 days
return {
userId,
recoveryPhrase,
trustedContacts: ['contact1@email.com', 'contact2@email.com'],
timeLockDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
};
}
async recoverAccount(email, method, ...args) {
switch (method) {
case 'phrase':
return this.recoverWithPhrase(email, ...args);
case 'contacts':
return this.recoverWithContacts(email, ...args);
case 'timelock':
return this.recoverWithTimeLock(email, ...args);
default:
throw new Error('Invalid recovery method');
}
}
}
User Education is Key 📚
// During registration
const registrationInfo = {
title: 'Account Recovery Setup',
message: `
IMPORTANT: Save your recovery phrase securely!
Your recovery phrase: ${recoveryPhrase}
Store it in:
- A password manager
- A safe deposit box
- Written down and stored securely
Without this phrase, your data cannot be recovered if you forget your password.
We also recommend setting up trusted contacts as a backup recovery method.
`,
checkboxRequired: true,
checkboxLabel: 'I understand that without my recovery phrase, my data cannot be recovered'
};
UI/UX Example (React)
function RecoverySetup({ onComplete }) {
const [recoveryPhrase, setRecoveryPhrase] = useState('');
const [confirmed, setConfirmed] = useState(false);
useEffect(() => {
// Generate and show recovery phrase
const phrase = generateRecoveryPhrase();
setRecoveryPhrase(phrase);
}, []);
return (
<div className="recovery-setup">
<h2>Account Recovery Setup</h2>
<div className="warning">
<Alert severity="warning">
Save this recovery phrase securely. You won't be able to see it again!
</Alert>
</div>
<div className="recovery-phrase">
<TextField
value={recoveryPhrase}
multiline
readOnly
label="Your Recovery Phrase"
helperText="Copy and store this securely"
/>
<Button onClick={() => navigator.clipboard.writeText(recoveryPhrase)}>
Copy to Clipboard
</Button>
</div>
<FormControlLabel
control={
<Checkbox
checked={confirmed}
onChange={(e) => setConfirmed(e.target.checked)}
/>
}
label="I have securely stored my recovery phrase"
/>
<Button
disabled={!confirmed}
onClick={onComplete}
variant="contained"
>
Complete Setup
</Button>
</div>
);
}
Recovery Methods Comparison
| Method | Security | UX | Complexity | Zero-Knowledge? |
|---|---|---|---|---|
| Recovery Phrase | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ✅ Yes |
| Shamir's Secret Sharing | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ✅ Yes |
| Trusted Contacts | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ✅ Yes |
| Time-Locked | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ✅ Yes |
| Server Key Storage | ⭐ | ⭐⭐⭐⭐⭐ | ⭐ | ❌ No |
Bottom Line
There is no way to recover data without some form of backup. The question is: which backup method aligns with your security requirements and user experience goals?
For most applications, I recommend:
- Primary: Recovery phrase (most secure)
- Secondary: Trusted contacts (user-friendly)
- Fallback: Time-locked backup (last resort)
Related Concepts
- Proton Mail/Drive: Zero-knowledge encryption services
- End-to-end encryption: Data encrypted at rest and in transit
- PBKDF2: Password-based key derivation function
- AES-256-GCM: Advanced Encryption Standard with authentication
- Deterministic encryption: Same plaintext produces same ciphertext
- Key rotation: Periodic replacement of encryption keys
- Shamir's Secret Sharing: Cryptographic method for splitting secrets
- Social recovery: Using trusted contacts for account recovery