Sanctum: Cryptographically Deniable Vault System with IPFS Storage

Published: (January 8, 2026 at 06:03 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

Sanctum illustration

🎭 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:

LayerDescription
Decoy LayerInnocent content (family photos, a small wallet with $200)
Hidden LayerReal secrets (whistleblower docs, main crypto wallet)
Panic LayerShows “vault deleted” under duress

The magic? All three are cryptographically identical. An adversary cannot prove which layer is real—or even 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 (256 MB 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();

  store(key: string, value: Uint8Array): void {
    this.keys.set(key, value);
    // Auto‑clear after 5 minutes
    setTimeout(() => this.wipe(key), 300_000);
  }

  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

Attack VectorDefense
Physical DuressReveal decoy passphrase; hidden layer remains indistinguishable.
Disk ForensicsRAM‑only storage; keys never written to disk; auto‑wiped on tab close.
Timing AnalysisRandomized 500‑2000 ms delay on all operations.
Blob Size AnalysisPadded to standard sizes (1 KB, 10 KB, 100 KB, 1 MB, 10 MB, 25 MB).
Brute ForceArgon2id with 256 MB memory makes brute‑force computationally infeasible.

🚀 Quick Start

For Users

Visit sanctumvault.online

  • Configure Pinata or Filebase (free IPFS providers)
  • Create a vault with optional decoy content
  • Set a passphrase for the 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
): Uint8Array {
  const nonce = randomBytes(24); // 192‑bit nonce
  const cipher = xchacha20poly1305(key, nonce);
  return cipher.encrypt(plaintext);
}

XChaCha20‑Poly1305 provides a vastly larger nonce space, eliminating nonce‑reuse concerns and offering authenticated encryption with strong performance in the browser.

// Encryption function
function encryptData(
  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,
    // Authenticated encryption tag (last 16 bytes)
    authTag: ciphertext.slice(-16),
  };
}

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

-- Stage 1: Soft delete (mark inactive)
UPDATE vaults
SET is_active = 0
WHERE expires_at < NOW();
  • ✅ 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:

IssueCloud StorageIPFS
CentralizedProvider can be compelled to hand over dataData replicated across many nodes
CensorableGovernments can block accessContent‑addressed (CID), not location‑based
DeletableProvider can delete your dataImmutable – once uploaded, cannot be modified
CostOngoing feesFree tiers: Pinata (1 GB) + Filebase (5 GB)

🚫 What Sanctum Is NOT

  • ❌ A password manager – Use KeePassXC / Bitwarden for that
  • ❌ A backup solution – IPFS data can be unpinned
  • ❌ A file‑sharing service – Links are permanent, no deletion
  • ❌ 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();

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 a commercial license after 4 years.

  • Live Demo:
  • GitHub:
  • Video Demo:
  • Contact:

💬 Discussion

What do you think? Would you use cryptographic deniability for your sensitive data? What other use cases can you imagine?

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.

Back to Blog

Related posts

Read more Âť

Hello, Newbie Here.

Hi! I'm falling back into the realm of S.T.E.M. I enjoy learning about energy systems, science, technology, engineering, and math as well. One of the projects I...