DEV Community

Teycir Ben Soltane
Teycir Ben Soltane

Posted on

Sanctum: Cryptographically Deniable Vault System with IPFS Storage

🔒 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."
Enter fullscreen mode Exit fullscreen mode

You can't prove the absence of data. Until now.

✨ The Solution: Cryptographic Deniability

Sanctum creates three indistinguishable layers:

  1. Decoy Layer - Innocent content (family photos, small wallet with $200)
  2. Hidden Layer - Real secrets (whistleblower docs, main crypto wallet)
  3. 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)}`;
Enter fullscreen mode Exit fullscreen mode

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);
  }
});
Enter fullscreen mode Exit fullscreen mode

🎯 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

🛡️ 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

  1. Visit sanctumvault.online
  2. Configure Pinata or Filebase (free IPFS providers)
  3. Create vault with optional decoy content
  4. Set passphrase for hidden layer
  5. 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
Enter fullscreen mode Exit fullscreen mode

🔬 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
  };
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

📊 Test Coverage

npm test

# Results:
# Test Suites: 19 passed, 19 total
# Tests:       115 passed, 115 total
# Coverage:    Core crypto, duress layers, storage, vault expiry
Enter fullscreen mode Exit fullscreen mode

🔐 OpSec Best Practices

  1. Fund decoy realistically - $50-500 matching your financial status
  2. Memorize passphrases - 12+ chars (uppercase, lowercase, number, special)
  3. Use Tor Browser - Hides IP, defeats timing attacks
  4. Test before trusting - Verify decoy unlocks, practice plausible deniability
  5. Store links securely - Password manager (KeePassXC/Bitwarden)
  6. 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
Enter fullscreen mode Exit fullscreen mode

🌐 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>();
Enter fullscreen mode Exit fullscreen mode

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' };
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

🔮 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


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