A research project exploring deniable steganographic storage.
This is a research prototype, not a production tool. It has not been externally audited. Do not rely on it as your sole protection for sensitive data. The cryptographic properties described here are design goals being tested β not guarantees.
darkfs is a FUSE-based encrypted filesystem designed so that the entire disk image is statistically indistinguishable from random data. No headers, no magic bytes, no metadata, no partition table β nothing should reveal whether the image is in use or was simply filled with /dev/urandom.
The only secret is a passphrase. Everything else β block locations, encryption keys, directory structure β is deterministically derived from it.
Can software be truly invisible? Not just encrypted, but indistinguishable from nothing.
Existing tools like VeraCrypt and LUKS protect the contents of your data, but they announce its existence β headers, partition tables, magic bytes. An adversary doesn't need to break the crypto if they can simply prove you're hiding something.
This project explores whether a filesystem can be built where the storage medium appears to be random noise. Where any passphrase produces a valid (empty) result. Where multiple independent filesystems coexist without a detectable boundary. Where "is there data here?" has no provable answer.
The current implementation demonstrates that this is feasible. The SECURITY.md documents what works, what doesn't, and what remains to be solved.
passphrase βββΊ Argon2id βββΊ master_secret
β
βββββββββββββββΌββββββββββββββ
βΌ βΌ βΌ
superblock session_secret void mask
(5-of-9 (per-block keys) (ChaCha20
shards) noise layer)
β β β
βΌ βΌ βΌ
ββββββββββββββββββββββββββββββββββββ
β Image: indistinguishable from β
β /dev/urandom β
ββββββββββββββββββββββββββββββββββββ
Every block on disk: XChaCha20-Poly1305(data) XOR ChaCha20(mask) β two independent encryption layers. In internal testing, the image passes statistical randomness tests (chi-squared, entropy, serial correlation). See SECURITY.md for details and caveats.
- Deniable: A static image passes statistical randomness tests in internal testing. Multi-snapshot analysis reveals write patterns (see Threat Model).
- Multi-passphrase: Multiple independent filesystems can coexist on the same image. Each passphrase reveals a different filesystem. Under the single-snapshot threat model, an adversary cannot determine how many passphrases exist.
- Wrong passphrase = empty filesystem: Any passphrase "works" -- most just show an empty directory. No "wrong passphrase" error is ever returned.
- Modern crypto: XChaCha20-Poly1305, Argon2id, HKDF-SHA256, HMAC-SHA256.
- Memory safe: Written in Rust with
#![deny(unsafe_code)].
# Without FUSE (CLI only: put/get/ls/rm)
cargo build --release
# With FUSE (adds mount/unmount β requires macFUSE or libfuse3)
cargo build --features fuse --releasePrerequisites: Rust 1.70+. For FUSE: macOS needs macFUSE, Linux needs apt install libfuse3-dev pkg-config.
darkfs create vault.img 1GThis fills a file with random noise. Right now it's just noise. A passphrase will give it meaning.
darkfs put vault.img secret.pdf # enter passphrase when prompted
darkfs put vault.img taxes.xlsx
darkfs put vault.img --name /docs/notes.txt notes.txt
darkfs mkdir vault.img /photosdarkfs ls vault.img # enter same passphrase
darkfs get vault.img secret.pdf . # writes to current directory
darkfs info vault.img # file count, storage used, treedarkfs ls vault.img # enter a DIFFERENT passphrase
# β (empty)
# No error. No "wrong password." Just... nothing.This is the key moment. The system can't tell the difference between a wrong passphrase, a fresh vault, and a random file from /dev/urandom. All three look the same. That's the deniability.
darkfs rm vault.img secret.pdf # overwritten with random noise β gonedarkfs mount vault.img ~/private # enter passphrase once
cp document.pdf ~/private/ # no passphrase needed
ls ~/private/ # no passphrase needed
darkfs unmount ~/private # keys wiped from memoryEach CLI command (put, get, ls, rm, info, mkdir) prompts for the passphrase, derives keys, performs the operation, then wipes everything from memory and exits. This is the safest mode β keys exist only for milliseconds.
If entering the passphrase repeatedly is impractical, you have two options:
# Option 1: FUSE mount β enter passphrase once, use as a normal folder
darkfs mount vault.img ~/private
# Now use cp, mv, cat, etc. freely β no passphrase prompts
darkfs unmount ~/private # keys wiped on unmount
# Option 2: Environment variable β for scripts (use with caution)
DARKFS_PASSPHRASE="mypass" darkfs put vault.img file.txt
# The variable is cleared from the process environment after reading,
# but may still be visible in shell history or process listings.The same vault can hold multiple independent filesystems β one per passphrase. They don't know about each other. Nobody can prove more than one exists.
# Passphrase "vacation2024" β harmless decoy files
darkfs put vault.img beach.jpg
darkfs put vault.img recipes.txt
# Passphrase "kJ7$mQ9!xR2&nP4" β real secrets
darkfs put vault.img accounts.xlsx
darkfs put vault.img classified.pdf
# If compelled to reveal a passphrase, give "vacation2024".
# The adversary sees beach photos and recipes.
# Under single-snapshot analysis, the adversary cannot determine
# if another passphrase exists.There is no password database. Nothing is stored. The passphrase doesn't unlock anything β it constructs the filesystem.
passphrase β Argon2id β master_secret β HMAC β block positions + keys
Every passphrase deterministically generates a unique set of block positions and encryption keys. The correct passphrase generates positions that happen to contain encrypted data. A wrong passphrase generates positions that contain random noise β which is indistinguishable from "no data."
This means:
- No "wrong password" error β the system genuinely can't tell
- No password recovery β there's nothing to recover from
- No proof of existence β the vault looks identical whether it has 0 or 100 filesystems
The golden rule: remember your passphrase. There is no reset. There is no recovery. There is no proof it ever existed. That's the point.
These illustrate the deniability properties being explored, not endorsements for specific use:
- Portable media. A vault on a USB stick looks like a corrupted or wiped drive β random data with no filesystem signature. Forensic tools find nothing to analyze.
- Cloud storage. A vault file uploaded to any cloud service is just a blob of random bytes. The provider cannot identify its purpose.
- Multi-passphrase. The same image can hold independent filesystems under different passphrases. Under the single-snapshot threat model, there is no detectable boundary between them.
- Single-snapshot disk seizure: In internal testing, an attacker with a single copy of the image file cannot distinguish it from random noise using standard statistical tests.
- Compelled disclosure: A decoy passphrase reveals a decoy filesystem. Under the single-snapshot model, the attacker cannot determine if other passphrases exist.
- Forensic analysis: No headers, magic bytes, or partition tables. Internal statistical testing shows no anomalies, but this has not been verified by an external auditor.
- Active surveillance: An attacker watching your system while darkfs is running can observe I/O patterns, memory allocation (Argon2id uses 256 MiB), and the process name.
- Multi-snapshot analysis: An attacker comparing two copies of the image can see which blocks changed. Decoy writes and tier-based I/O padding limit what is revealed, but approximate file size (within tier boundaries) is still observable.
- Weak passphrases: Use 12+ characters with high entropy. The deterministic salt (derived from image size) means dictionary attacks are amortized across all users with the same image size.
- Rubber-hose cryptanalysis: darkfs provides plausible deniability, not resistance to physical coercion.
| Component | Algorithm | Purpose |
|---|---|---|
| KDF | Argon2id (256 MiB, t=4) | Passphrase to master secret |
| Block encryption | XChaCha20-Poly1305 | Authenticated encryption per block |
| Key derivation | HKDF-SHA256 | Per-block key + nonce from master secret |
| Block location | HMAC-SHA256 | Deterministic mapping of (path, block) to disk offset |
Benchmarks on Apple M-series (release build):
| Operation | Throughput |
|---|---|
| KDF (dev preset) | ~2.2 ms |
| Block encrypt (4080 B) | ~7 us |
| Block decrypt (4080 B) | ~9 us |
| File write (64 KB) | ~115 MiB/s |
| File read (64 KB) | ~152 MiB/s |
| Approach | darkfs | VeraCrypt Hidden Volume | StegFS | Artifice |
|---|---|---|---|---|
| Deniability model | Image IS noise | Hides inside another volume | Hides in free space | Hides in free space |
| Multiple passphrases | Unlimited | 2 (outer + hidden) | Limited | Limited |
| Detectable structure | None | Header exists | Requires cover FS | Requires cover FS |
| Crypto | XChaCha20/Argon2id | AES/PBKDF2 | Varies | AES |
| Language | Rust | C/C++ | C | C |
- Rename is O(file_size): Block locations are derived from file paths, so renaming requires re-encrypting all blocks. Currently not implemented (returns ENOSYS).
- No journaling: A crash during write can leave files in an inconsistent state. Block 0 is written last as a basic commit marker.
- Collision risk: With 16-slot cuckoo hashing, practical fill rate is ~50-60%. Keep usage below 50% for safety.
- No file locking: Concurrent access to the same image will corrupt data.
- macOS extended attributes: Finder creates
._*files on non-HFS filesystems. These are harmless encrypted data but consume blocks.
- ARCHITECTURE.md β Technical design, key derivation chain, block layout, collision resolution
- SECURITY.md β Security audit findings, threat model, known limitations
- CONTRIBUTING.md β Development setup, code style, testing guidelines
- CHANGELOG.md β Release history
MPL-2.0