- 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.
1248 lines
32 KiB
Markdown
1248 lines
32 KiB
Markdown
# Zero-Knowledge Encryption Implementation Guide
|
|
|
|
## Table of Contents
|
|
1. [Proton-Style Encryption for MongoDB](#proton-style-encryption-for-mongodb)
|
|
2. [Shareable Links with Embedded Passwords](#shareable-links-with-embedded-passwords)
|
|
3. [Security Best Practices](#security-best-practices)
|
|
4. [Advanced Features](#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:
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
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:
|
|
|
|
```bash
|
|
npm install mongodb-client-encryption
|
|
```
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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)
|
|
|
|
```javascript
|
|
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
|
|
|
|
```javascript
|
|
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)
|
|
|
|
```javascript
|
|
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)
|
|
|
|
```javascript
|
|
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:**
|
|
|
|
```javascript
|
|
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
|
|
|
|
```bash
|
|
# .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:
|
|
```bash
|
|
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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
const share = await createShare(userId, data, {
|
|
maxAccessCount: 5 // Only 5 people can access
|
|
});
|
|
```
|
|
|
|
### 3. Additional Password Protection
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// Restrict access to specific emails
|
|
const share = await createShare(userId, data, {
|
|
allowedEmails: ['user@example.com', 'trusted@domain.com'],
|
|
requireEmailVerification: true
|
|
});
|
|
```
|
|
|
|
### 5. Revocable Shares
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```javascript
|
|
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.
|
|
|
|
```javascript
|
|
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.
|
|
|
|
```javascript
|
|
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).
|
|
|
|
```javascript
|
|
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
|
|
```javascript
|
|
// NEVER DO THIS
|
|
await User.create({
|
|
email,
|
|
password: password // ❌ Defeats the whole purpose
|
|
});
|
|
```
|
|
|
|
#### ❌ Store Encryption Keys on Server
|
|
```javascript
|
|
// NEVER DO THIS
|
|
await User.create({
|
|
email,
|
|
encryptedData: data,
|
|
encryptionKey: key // ❌ Not zero-knowledge
|
|
});
|
|
```
|
|
|
|
#### ❌ Backdoor Encryption
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
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 📚
|
|
|
|
```javascript
|
|
// 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)
|
|
|
|
```javascript
|
|
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)
|
|
|
|
---
|
|
|
|
## 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
|