🔒 Sanctum: When "I Don't Know the Password" Is Actually True
Ever heard of the $5 wrench attack? It's the oldest vulnerability in cryptography: physical coercion. Your encryption might be unbreakable, but you're not. Sanctum solves this with cryptographically sound plausible deniability.
🎭 The Problem: Encryption Isn't Enough
Traditional encrypted storage has a fatal flaw:
Attacker: "Give me the password or else."
You: "I don't have one."
Attacker: *checks encrypted file* "This is clearly encrypted. Try again."
You can't prove the absence of data. Until now.
✨ The Solution: Cryptographic Deniability
Sanctum creates three indistinguishable layers:
- Decoy Layer - Innocent content (family photos, small wallet with $200)
- Hidden Layer - Real secrets (whistleblower docs, main crypto wallet)
- Panic Layer - Shows "vault deleted" under duress
The magic? All three are cryptographically identical. An adversary cannot prove which layer is real or if hidden layers exist.
🏗️ Architecture: Zero-Trust by Design
Client-Side Encryption Flow
// 1. User creates vault with decoy + hidden content
const decoyBlob = encrypt(decoyContent, ''); // Empty passphrase
const hiddenBlob = encrypt(hiddenContent, deriveKey(passphrase));
// 2. XOR both layers (makes them indistinguishable)
const combined = xor(decoyBlob, hiddenBlob);
// 3. Upload to IPFS
const decoyCID = await ipfs.upload(decoyBlob);
const hiddenCID = await ipfs.upload(hiddenBlob);
// 4. Split-key architecture
const keyA = randomBytes(32); // Stays in URL
const keyB = randomBytes(32); // Encrypted in database
const vaultURL = `https://sanctumvault.online/unlock/${vaultId}#${encode(keyA)}`;
Tech Stack
- Frontend: Next.js 15 + React + Web Crypto API
- Cryptography: XChaCha20-Poly1305 + Argon2id (256MB memory, 3 iterations)
- Storage: IPFS via Pinata/Filebase (free tiers)
- Database: Cloudflare D1 (split-key storage only)
- Hosting: Cloudflare Pages (static site)
Security Features
// RAM-only storage (no disk persistence)
class SecureStorage {
private keys = new Map<string, Uint8Array>();
store(key: string, value: Uint8Array): void {
this.keys.set(key, value);
// Auto-clear after 5 minutes
setTimeout(() => this.wipe(key), 300000);
}
wipe(key: string): void {
const data = this.keys.get(key);
if (data) {
data.fill(0); // Overwrite memory
this.keys.delete(key);
}
}
}
// Panic key: Double-press Escape
let escapeCount = 0;
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
escapeCount++;
if (escapeCount === 2) {
wipeAllKeys();
window.location.href = '/';
}
setTimeout(() => escapeCount = 0, 500);
}
});
🎯 Real-World Use Cases
1. Journalist Protecting Sources
Decoy: Published articles, public research notes
Hidden: Confidential source documents, whistleblower communications
Scenario: Device seized at border → reveal decoy, sources stay protected
2. Crypto Holder Under Duress
Decoy: Small wallet with $200 ("this is all I have")
Hidden: Main wallet with life savings
Scenario: $5 wrench attack → hand over decoy wallet, real funds stay safe
3. Activist in Authoritarian Regime
Decoy: Personal photos, innocuous social media content
Hidden: Protest coordination plans, evidence of government abuse
Scenario: Police raid → show decoy layer, cannot prove hidden content exists
🛡️ Attack Resistance
Physical Duress
Attack: Coerced to reveal passphrase
Defense: Reveal decoy passphrase. Adversary cannot prove hidden layer exists.
Disk Forensics
Attack: Device seized, disk analysis performed
Defense: RAM-only storage. Keys never written to disk. Auto-wiped on tab close.
Timing Analysis
Attack: Measure decryption time to detect layers
Defense: Randomized 500-2000ms delay on all operations.
Blob Size Analysis
Attack: Compare encrypted blob sizes
Defense: Padded to standard sizes (1KB, 10KB, 100KB, 1MB, 10MB, 25MB).
Brute Force
Attack: Try all possible passphrases
Defense: Argon2id with 256MB memory makes brute-force computationally infeasible.
🚀 Quick Start
For Users
- Visit sanctumvault.online
- Configure Pinata or Filebase (free IPFS providers)
- Create vault with optional decoy content
- Set passphrase for hidden layer
- Share the link - only you know the passphrase
For Developers
# Clone repository
git clone https://github.com/Teycir/Sanctum.git
cd Sanctum
# Install dependencies
npm install
# Run development server
npm run dev
🔬 Technical Deep Dive
Why XChaCha20-Poly1305?
// AES-GCM: 96-bit nonce (collision risk after 2^48 encryptions)
// XChaCha20: 192-bit nonce (collision risk after 2^96 encryptions)
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
export function encrypt(
plaintext: Uint8Array,
key: Uint8Array
): EncryptionResult {
const nonce = randomBytes(24); // 192-bit nonce
const cipher = xchacha20poly1305(key, nonce);
const ciphertext = cipher.encrypt(plaintext);
return {
ciphertext,
nonce,
authTag: ciphertext.slice(-16) // Authenticated encryption
};
}
Split-Key Architecture
// KeyA: Stays in URL fragment (never sent to server)
// KeyB: Encrypted in database with vault-specific key
const vaultKey = deriveKey(vaultId + salt);
const encryptedKeyB = encrypt(keyB, vaultKey);
// To decrypt IPFS CIDs:
const masterKey = xor(keyA, keyB);
const decoyCID = decrypt(encryptedDecoyCID, masterKey);
const hiddenCID = decrypt(encryptedHiddenCID, masterKey);
30-Day Grace Period
// Two-stage cleanup prevents accidental data loss
// Stage 1: Soft delete (mark inactive)
UPDATE vaults SET is_active = 0 WHERE expires_at < NOW();
// Stage 2: Hard delete (30 days later)
DELETE FROM vaults
WHERE is_active = 0
AND expires_at < NOW() - INTERVAL 30 DAYS;
📊 Test Coverage
npm test
# Results:
# Test Suites: 19 passed, 19 total
# Tests: 115 passed, 115 total
# Coverage: Core crypto, duress layers, storage, vault expiry
🔐 OpSec Best Practices
- Fund decoy realistically - $50-500 matching your financial status
- Memorize passphrases - 12+ chars (uppercase, lowercase, number, special)
- Use Tor Browser - Hides IP, defeats timing attacks
- Test before trusting - Verify decoy unlocks, practice plausible deniability
- Store links securely - Password manager (KeePassXC/Bitwarden)
- Never reveal hidden layers - Act natural, claim "this is all I have"
🐦 Warrant Canary
Sanctum includes a live warrant canary at sanctumvault.online/canary:
✅ NOT received any:
- National Security Letters (NSLs)
- FISA court orders
- Gag orders
- Requests to implement backdoors
✅ Architecture guarantees:
- Zero-knowledge: Cannot decrypt user vaults
- No user logs: No IP addresses or metadata
- No backdoors: All code is open-source
- RAM-only: No persistent storage of keys
🌐 Why IPFS?
Traditional cloud storage has single points of failure:
- Centralized: Provider can be compelled to hand over data
- Censorable: Governments can block access
- Deletable: Provider can delete your data
IPFS provides:
- Decentralized: Data replicated across multiple nodes
- Censorship-resistant: Content-addressed (CID), not location-based
- Immutable: Once uploaded, cannot be modified
- Free: Pinata (1GB) + Filebase (5GB) free tiers
🚫 What Sanctum Is NOT
- ❌ Not a password manager - Use KeePassXC/Bitwarden for that
- ❌ Not a backup solution - IPFS data can be unpinned
- ❌ Not a file sharing service - Links are permanent, no deletion
- ❌ Not a VPN - Use Tor Browser for anonymity
💡 Lessons Learned
1. RAM-Only Storage Is Hard
// ❌ WRONG: localStorage persists to disk
localStorage.setItem('key', encode(key));
// ✅ CORRECT: In-memory only
const keyStore = new Map<string, Uint8Array>();
2. Timing Attacks Are Real
// ❌ WRONG: Instant response reveals wrong passphrase
if (passphrase !== correctPassphrase) {
return { error: 'Invalid passphrase' };
}
// ✅ CORRECT: Constant-time comparison + random delay
const isValid = timingSafeEqual(hash(passphrase), hash(correctPassphrase));
await sleep(randomInt(500, 2000));
return isValid ? { data } : { error: 'Invalid passphrase' };
3. Browser History Is a Leak
// Vault URLs contain KeyA in fragment
// Must clear from browser history
if (window.history.replaceState) {
window.history.replaceState(null, '', '/unlock');
}
🔮 Future Roadmap
- [ ] Shamir Secret Sharing - Split vault access across multiple people
- [ ] Dead Man's Switch - Auto-release after inactivity
- [ ] Steganography - Hide vault in innocent-looking images
- [ ] Hardware Key Support - YubiKey/Ledger integration
- [ ] Mobile Apps - iOS/Android with biometric unlock
🙏 Acknowledgments
- VeraCrypt - Inspiration for plausible deniability
- Cloudflare Pages - Free static site hosting
- Pinata/Filebase - Free IPFS pinning services
- @noble/ciphers - Audited cryptography library
📜 License
Business Source License 1.1 - Free for non-production use. Production use requires commercial license after 4 years.
🔗 Links
- Live Demo: sanctumvault.online
- GitHub: github.com/Teycir/Sanctum
- Video Demo: YouTube
- Contact: teycirbensoltane.tn
💬 Discussion
What do you think? Would you use cryptographic deniability for your sensitive data? What other use cases can you think of?
Drop a comment below or open an issue on GitHub!
Built with ❤️ and 🔒 by Teycir Ben Soltane
Disclaimer: Sanctum is a tool for legitimate privacy needs. Users are responsible for complying with local laws. The developers do not condone illegal activities.

Top comments (0)