normogen/encryption.md
goose e72602d784 Initial commit: Project setup and documentation
- 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.
2026-02-14 11:11:06 -03:00

32 KiB

Zero-Knowledge Encryption Implementation Guide

Table of Contents

  1. Proton-Style Encryption for MongoDB
  2. Shareable Links with Embedded Passwords
  3. Security Best Practices
  4. 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

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'
);

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

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);
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

  1. Never store encryption keys in MongoDB

    • Use environment variables
    • Consider key management services (AWS KMS, HashiCorp Vault)
    • Hardware Security Modules (HSMs) for production
  2. Use different encryption keys per:

    • User
    • Document type
    • Environment (dev/staging/prod)
  3. 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)

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:

  1. Primary: Recovery phrase (most secure)
  2. Secondary: Trusted contacts (user-friendly)
  3. Fallback: Time-locked backup (last resort)

  • 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