Your private Dropbox—encrypted end-to-end, even from the server.
Push and pull files through hostile networks, traffic-intercepting proxies, and compromised servers—without revealing a single filename nor bit of data.
A nook is a small, quiet, or sheltered area, such as a cozy corner in a room, an alcove, or a secluded spot in nature. The term often implies privacy, comfort, or a space set aside for a specific purpose, like a "breakfast nook" or a "reading nook."
Nook is a minimal end-to-end-encrypted file store designed to operate correctly in fully untrusted environments, including TLS-intercepting firewalls, corporate MITM proxies, hostile networks, and server compromise scenarios.
No file contents, filenames, directory structure, paths, or filesystem semantics may ever appear outside authenticated encryption.
The server is a semantic null—it understands only random object IDs and ciphertext. All meaning exists exclusively on the client.
One nookd server hosts multiple vaults: credential-gated storage containers provisioned by the server operator. Each vault holds any number of namespaces: independently encrypted volumes whose keys never leave the clients. Access to a vault says nothing about the ability to read its namespaces—that requires the corresponding namespace key.
- Mandatory end-to-end encryption (E2EE): Every file, directory name, and path is encrypted before leaving the client
- Amorphous traffic: All payloads are indistinguishable encrypted blobs; the server cannot differentiate between files, manifests, or metadata
- TLS-MITM resistant: Confidentiality does not rely on TLS; even complete TLS interception reveals nothing
- Atomic updates: Safe concurrent writers using compare-and-swap (CAS) semantics
- Multi-tenant: One server hosts many credential-gated vaults, each holding any number of independently encrypted namespaces, with per-vault storage quotas
- Authenticated requests: Every request is HMAC-signed with the vault credential; the credential itself never travels on the wire
-
Simple deployment: One Rust server binary (
nookd), one Rust CLI binary (nook) - Zero-knowledge server: Server compromise yields only ciphertext
- Not a sync daemon (no background sync)
- Not a version control system (no merge, diff, or conflict resolution)
- Not traffic-analysis resistant (volume and timing remain observable)
- Not a backup system with versioning
Nook is for pushing and pulling complete encrypted snapshots of directory trees between devices you control, through infrastructure you don't trust.
The project is Nook, and the installed commands are still nook and nookd.
On crates.io the package is published as nook-vault because the nook
package name is already taken by another, currently unmaintained package.
- Rust (stable) + Cargo
Install from crates.io:
cargo install nook-vaultThis installs two executables:
nook
nookdFrom the repo root:
cargo build --releaseBinaries will be at:
-
target/release/nook(CLI client) -
target/release/nookd(server daemon)
./target/release/nookd serve --listen 0.0.0.0:8080 --storage ./storageThe server stores only encrypted blobs, nested by vault and namespace, under the storage directory:
storage/
objects/
<vault_id>/
<namespace_id>/
<object_id>
temp/
meta.sqlite
The storage directory is also settable via NOOK_DATA_DIR. A default per-vault storage quota (in
bytes) can be set via --quota-bytes/NOOK_QUOTA_BYTES (unset means unlimited, unless a vault has
its own override — see below); uploads that would exceed a vault's quota are rejected with
507 Insufficient Storage.
Nook is not user-based: nookd manages vaults (server-side storage/access containers) and
namespaces (client-side encrypted volumes inside a vault), not user accounts. A vault is
created locally by the server operator — never over the network, so no anonymous caller can
self-provision unlimited storage:
./target/release/nookd vault create --storage ./storage
# vault_id: <64-char hex>
# vault_credential: <64-char hex>
# (shown exactly once — store it securely; if lost, revoke and create a new vault)Give the printed vault_id/vault_credential to the first user of this vault through a secure
out-of-band channel (in person, a password manager entry, an encrypted message — the same channel
you'd already trust to share a key). That user can then choose to share the same credentials with
collaborators, letting them use the same server-side storage (see "Share a namespace" below for how
they keep their data private from each other regardless).
Other vault management commands:
./target/release/nookd vault list --storage ./storage # usage, never prints credentials
./target/release/nookd vault revoke <vault_id> --storage ./storage # blocks access; data retainedpodman build -f crates/nook-vault/Dockerfile -t nookd .
podman volume create nookd-data
podman run -d --name nookd -p 8080:8080 -v nookd-data:/data nookdThe container declares /data as a VOLUME — the object store and meta.sqlite live there, so a
named volume (as above) or a bind mount keeps data across container recreation. docker works the
same way (swap podman for docker).
To use a host directory instead of a named volume, the directory must be writable by the
container's fixed user (UID/GID 10001). With rootless Podman, set that up via podman unshare
so the ownership is correct inside the container's user namespace:
mkdir -p ./data
podman unshare chown 10001:10001 ./data
podman run -d --name nookd -p 8080:8080 -v ./data:/data:Z nookdWith Docker (no user namespace remapping by default), a plain chown 10001:10001 ./data on the
host is enough.
Using a vault requires the vault_id/vault_credential from the operator (see "Create a vault"
above):
./target/release/nook init \
--server http://127.0.0.1:8080 \
--vault-id <vault_id> \
--vault-credential <vault_credential> \
--root /path/to/filesThis generates a fresh namespace (SPEC-004's replacement for what earlier versions called "the
vault key") and stores it — together with the vault credential — in the OS keychain by default
(macOS Keychain, Windows Credential Manager, or the Secret Service on Linux). If no keychain is
available — headless servers, CI, some Linux setups — nook init falls back to encrypting both
with a passphrase (Argon2id + XChaCha20-Poly1305) and storing the encrypted blob in the client
config. Set NOOK_PASSPHRASE to supply the passphrase non-interactively (scripted/CI use);
otherwise nook init prompts for it.
Client config is written as TOML to the platform config directory (~/.config/nook/config.toml
on Linux). The namespace key and vault credential are never written there in recoverable form —
only a keychain reference or an encrypted blob. vault_id/namespace_id are non-secret and stored
in plain TOML.
Set or view the local root later:
./target/release/nook root --set /path/to/files
./target/release/nook rootEach nook init (without --import-namespace) creates its own private namespace — even two
clients using the same vault credentials cannot read each other's data unless they explicitly share
a namespace key:
# On the sharing device:
./target/release/nook namespace export
# nookns1:<namespace_id>:<base64 namespace key>
# On the receiving device, using the same vault_id/vault_credential:
./target/release/nook init \
--server http://127.0.0.1:8080 \
--vault-id <vault_id> \
--vault-credential <vault_credential> \
--import-namespace <bundle from above> \
--root /path/to/filesBoth devices now have full read/write access to the same namespace — pass the bundle over a secure out-of-band channel, the same way you would a passphrase.
Push uploads files to the namespace. Pushing merges with existing content—files are added or updated, but other files are preserved:
./target/release/nook push # Push entire root directory
./target/release/nook push README.md # Push a single file
./target/release/nook push docs/ # Push a subdirectoryPull downloads and materializes files from the namespace into your local root:
./target/release/nook pull # Pull entire namespace
./target/release/nook pull docs/spec.md # Pull a specific file
./target/release/nook pull images/ # Pull a subdirectoryBoth commands preserve directory structure and support selective sync.
Remove a file or an entire directory subtree from the namespace (local files under your root are never touched):
./target/release/nook rm docs/spec.md # Remove a single file
./target/release/nook rm docs/ # Remove a directory subtreeGarbage collection is automatic — there is no gc command. After every
successful push or rm, the client deletes objects the updated manifest no
longer references: content replaced by that push is reclaimed immediately,
and historical orphans (e.g. residue of an interrupted push) are reclaimed
once older than a grace window (default 24 hours, configurable via
gc_grace_seconds in the client config or the NOOK_GC_GRACE_SECONDS
environment variable). The grace window protects a concurrent pusher's
uploaded-but-not-yet-linked objects from being swept mid-push; object ages
are compared against server-issued timestamps only, so client clock skew
cannot cause data loss. Freed space is subtracted from the vault's quota
immediately. Deletion is final — Nook has no versioning or trash.
Like everything else, cleanup is client-driven: the server cannot tell live
objects from garbage (it only ever sees opaque IDs and ciphertext), so it
never deletes anything on its own initiative. Against an older nookd
without deletion support, push/rm still work and simply warn that space
reclamation was skipped.
Check whether the head object exists on the server:
./target/release/nook statusOverride the server URL per command:
./target/release/nook --server http://other-host:8080 status
./target/release/nook --server http://other-host:8080 pushList the top-level entries stored in the encrypted manifest:
./target/release/nook ls
./target/release/nook ls path/inside/namespace # List a subdirectoryView a recursive tree of the namespace structure:
./target/release/nook tree
./target/release/nook tree docs/ # Tree from a subdirectoryAll discovery happens locally by decrypting the manifest—no server queries reveal structure.
- The server is a semantic null: it stores only ciphertext and vault/namespace/object IDs.
- TLS can be used, but confidentiality does not rely on it; TLS MITM does not expose filenames,
paths, or file contents. Reads and writes do require a valid vault credential regardless of TLS
(see
SECURITY.md). - To use the same namespace on multiple devices, see "Share a namespace" above, then set the local root on each device.
Contributions are welcome: issues, pull requests, critique, and discussion.
For an overview of how the implementation fits together (crates, crypto,
wire protocol, server/client internals, GC), see
TECH-IMPLEMENTATION-GUIDE.md.
This project follows the FOSS Pluralism Manifesto, affirming respect for people, freedom to critique ideas, and space for diverse perspectives.
Copyright (c) 2026 Iwan van der Kleijn
Licensed under the MIT License. See LICENSE for details.

