CipherFS
In development · unreleased · format may change

Requirements

Updated 2026-10-18. This page holds the target requirements and acceptance criteria; the README describes current behaviour only, and the gap between what is done and what is not is in the roadmap. The project is unreleased; formats and APIs may change to meet this page.

Core wrap-up scope (2026-10-17): this round delivers the existing Core, Sync, Rust API and persistent formats, fixes real defects and completes the original full regression. Apps, platforms, new interfaces and the extreme scale targets remain separate backlog items; listing them here does not mean they are implemented or accepted.

Terms

  • Vault: a set of files and their history sharing the same data.
  • Device: a client that holds the vault keys and can read and write plaintext. All devices of the same user have equal rights.
  • Store: a location that only stores and serves opaque ciphertext objects, such as S3-compatible object storage, a local directory, or a folder synchronized by iCloud/OneDrive/Dropbox. Stores hold no keys.
  • File: an object with a stable identity; its path is an attribute. Directories are derived from path prefixes, following the S3 key model, and are not separate objects.

Encryption

File contents, file names and sensitive metadata are encrypted before they leave the device; stores and the network see only ciphertext. Reads verify integrity; wrong passwords, tampering and corruption fail explicitly. Keys and plaintext never enter logs or stores.

Threat model

  • Stores and the network can see object sizes, counts, read/write times and the number of devices; they cannot see contents, names or directory structure.
  • A store can lose data or deny service; availability comes from multiple copies, not cryptography.
  • When a store replays an old state, devices do not go backwards: a device remembers the commits it has seen and only accepts progress; multiple sources are cross-checked.
  • Revoking a device cannot take back content it already obtained; any record it publishes after revocation is rejected.
  • Revoking store credentials (cloud drive accounts, S3 keys) is the user's responsibility; a revoked device that still holds credentials can download ciphertext but cannot read content from the new epoch.

Shared index

Every device keeps the complete shared index; after sync it converges, and offline it keeps the last known state. The index contains: current files, every unretired version of each file (metadata and content references), deletion state, conflict state, the information needed to decide causal order, device membership records (name, public key, role, the commit position it has synced to and when), key epochs, the vault retention policy and retirement records. The index holds only metadata and references, no bodies.

Body sync and device retention

By default bodies sync to every device along with the index. Each device has a role:

  • Full: keeps the bodies of all unretired versions (default).
  • Partial: keeps only pinned directories and content it has read, fetching the rest on demand; for space-constrained devices such as phones, and also for moving rarely used files off the local disk.

When a read finds no local body, it is fetched from any device or store that holds it; small files are fetched whole, large files only the blocks the read range touches, chosen automatically. Every interface (mounts included) connects to the configured stores and peers at run time to fetch, without requiring the user to start a separate sync process. A Full device never automatically drops unretired bodies to free space.

A Partial device has a local body budget. Over budget it automatically drops bodies confirmed to exist on at least one Full device or store, preferring to keep pinned directories and recently read content; local writes are not dropped before confirmation, and bodies referenced by open handles are not dropped. Dropping affects only the local cache; it is not a synced deletion.

Commitment: the body of every unretired version exists on every Full device that has completed sync and in every store.

Safe to remove

For each version you can find which devices and stores hold it, derived from the roles and synced positions in the membership records. The vault can list the versions held "only on this device"; when that list is empty the device can be removed safely. A Full device that is still catching up is treated as Partial until it has caught up.

First sync

A newly joined Full device first obtains the index, then the bodies, newest versions first; transfers can be interrupted and resumed.

Stores

A vault can configure zero or more stores, which carry the index and ciphertext bodies. A store only needs to: write immutable objects, read by name (range reads preferred), list by prefix, and delete. It does not need locks, atomic overwrite, conditional updates or immediate visibility; objects are content-addressed and append-only, each device publishes its own commit head, and other devices merge along the causal chain. Incomplete or not-yet-visible objects count as "not arrived", not as corrupt.

Devices of the same vault can use P2P connections and stores at the same time; when two devices are not online together, they exchange data through a store.

Stores connect in two ways: folder — reads and writes only a local directory that the iCloud/OneDrive/Dropbox client moves around; no authorization, but only usable on computers with that client installed; API — direct HTTP access with credentials, usable from phones, browsers and servers. Implementation order: S3-compatible and local directories (including cloud-drive sync folders); then WebDAV (covering self-hosted and several cloud drives); OneDrive/Dropbox/Google Drive private APIs only when the mobile clients are built.

Requirements for all stores:

  • Remote objects are packed to reduce the object count; remote packs target a smaller size than local ones (on the order of 4–8 MiB), so that both whole-pack downloads and range reads pay off.
  • Object names include the length or hash; an inconsistent read counts as not arrived and is retried, not reported as corrupt.
  • After retirement takes effect, store objects are reclaimed after a further short delay to cope with eventual consistency; content addressing means a mistaken deletion only needs a re-upload.
  • Fetch by block when range reads are available; otherwise fetch whole objects.
  • Credentials are issued per device, and writer devices have no delete right by default: each device holds its own store credentials with write, read and list rights only; deletion belongs only to the role that runs GC or is granted explicitly by the user. Where supported, enable bucket object lock or version retention with a retention period no shorter than the GC delay, so a compromised device or ransomware cannot delete ciphertext. Revoking a device prompts rotation of that device's credentials. Credentials come from pluggable sources (environment variables, files, systemd credentials, the system keychain, a remote key service) and are never written to the vault or logs.
  • Terms: buckets, directories and cloud drives are stores and see only ciphertext; the side of the daemon that speaks S3 to applications is the front door and sees plaintext. The two are never mixed up.

File metadata

Modification time is metadata that is committed to sync. Other attributes such as the executable bit are kept on a best-effort basis: the index always records them and the API can always read them, but a mounted filesystem may not present them. Permissions, owners, xattrs, ACLs and hard links are not promised.

Versions, history, trash and conflicts

Each file's versions form a causal chain. Deletion is a tombstone version; moves and renames are versions that change the path attribute and produce no history or trash entry. Concurrent modifications follow the file identity and do not resurrect at the old path because the other side moved the file. Directories exist through their files; deleting a directory deletes every file under it. Empty directories do not sync; interfaces maintain them locally.

History, trash and conflicts are three views of the same version chain; all are in the shared index and visible to every device:

  • History: earlier versions of a file.
  • Trash: files whose current version is a tombstone, shown folded by prefix; a whole prefix can be restored.
  • Conflict: a file with several heads that have no causal order.

Every version records its source device and time.

Current version

When there is a causal order, the later version wins. For true concurrency: a content version beats a deletion; otherwise the later publish time wins, then the change identifier breaks ties. The rule only decides which version the directory shows; all heads are kept.

Different files competing for the same path concurrently (concurrent creation of the same name, a move onto an existing path) are handled as a conflict on that path: the path shows one version, the rest are conflict heads of that path, and resolution keeps one or renames. The key model allows a and a/b to coexist; when mounted the directory wins, and the file a is visible through the conflict view.

Presenting and resolving conflicts

Directories and mounts show only the current version and never create conflict copy files. list/head, the CLI and the Web show the number of conflicts and each head's source device and time; mounts offer a read-only virtual directory to inspect each head.

A conflict ends when some change has all heads as parents. An explicit resolution picks one head without changing content; when a device that has seen all heads writes to the file, the new change automatically has all heads as parents and the conflict is resolved. The losing heads become history versions.

Restore

Restoring a history version or taking a file back from the trash publishes a new version whose content reference points at the old body, without re-uploading the body; the result takes part in sync.

Retirement and cleanup

A version leaves the shared history in only two ways, both synced operations:

  • Explicit purge: emptying the trash, purging the history of given versions or files.
  • Vault retention policy: synced with the index, such as retention days for versions and trash and a minimum number of versions kept per file. The default retention is 365 days. Any online device evaluates it and publishes retirement records; other devices accept them rather than computing their own.

Retirement first enters a pending retirement state that any device can cancel during the grace period (default 30 days); only after it expires do all devices and stores drop the bodies and reclaim objects. Retirement records published by a revoked device after its revocation are always rejected.

Never retired automatically: current versions, unsynced local edits, and conflict heads. An explicit purge cannot delete the current version. A version referenced by a local open handle keeps its body locally until the handle closes. Device-level settings only decide whether a body is kept locally; they cannot make a version disappear from the shared history.

Keys, devices and revocation

  • The vault has a randomly generated master key; content keys are derived per key epoch and subtree key, and each version records its epoch. Subtree keys are reserved for a future read-only share of a directory; all files currently use the root subtree.
  • Every device has a long-term device key pair and a human-readable name. Joining the vault wraps the current epoch key to that device's public key and writes it into the membership record; device-to-device authentication follows the membership records.
  • Pairing: the new device shows a short code or QR code, and confirming it on any joined device admits it; when no joined device is present, it joins with the initialization information and the recovery key.
  • Revocation: opens a new epoch whose key is wrapped only to the remaining devices; the revoked device cannot read content written afterwards and other devices no longer accept its connections. Existing content is not re-encrypted. Any enrolled device can revoke; concurrent revocations take effect in causal order, first one wins. In vaults with stores, revoking a device prompts the user to rotate that device's store credentials.
  • Periodic epoch rotation: the vault policy can set a rotation period (off by default, 90 days suggested); when due, any online device opens a new epoch and wraps it to every unrevoked device. A leaked epoch key only decrypts content up to that epoch. Disclosed content cannot be taken back. The rotation period and the notice period below live in one shared vault policy record; any device with management rights can change it, and the causally latest change wins.
  • Management rights of a recovery device (notice period): a device that joined with the recovery key can read and write immediately, but revocation, approving pairings, rotating the recovery key and ratifying other recovery joins need either ratification by an independent existing member or the notice period: the device publicly publishes a management claim that takes effect after the notice period set by the vault policy (default 7 days, minimum 1 day). If any existing member (joined before the claim, not revoked) objects during that time, the claim is void, and the objector may then revoke the device and rotate the recovery key. An existing member may object at any time before it accepts the "claim activated" record; when a program meets an activation record on which the local device has not yet decided, it must ask the user first. Unattended services by default accept automatically only after a full notice period has passed since they first saw the claim. Objection is always an explicit action. The notice period protects control: a stolen recovery key means the content is already readable, and the notice period does not change that. The notice period is implemented in Core, background sync, the CLI, HTTP and the Rust/C/Python/Go bindings. recovery claim|claims|accept|object|activate provide the notice and explicit decisions; recovery auto-accept false makes unattended mode warn only, and policy set recovery-notice-days N changes the duration of later notices. The duration is fixed at the claim's causal position; later policy changes do not shorten a notice in progress. /health shows the number of notices awaiting a local decision; while React is frozen, notices are handled through the CLI, HTTP and runtime logs, and browser banners and desktop notifications arrive with the corresponding new interfaces.
  • The device list shows name, role, last sync time and synced position, and is the entry point for revocation and "safe to remove".

Local unlock, sealing and recovery

  • The local storage key and device seed are wrapped separately by a set of unlock sources; wrappings can coexist and be added or removed individually, changing only local state and never shared state: password; recovery key; service key (file, environment variable, systemd credential); remote key service (KMS, Secrets Manager, self-hosted HTTPS, with cloud platform identity or a revocable token); system keychain and secure hardware (Secure Enclave, TPM, Android Keystore, for biometric unlock). Passwords do not sync; changing a password only updates the local wrapping, does not re-encrypt files and does not affect other devices.
  • Sealed is a first-class vault state: the process is running but the keys are not in memory. Seal on start, seal after an idle timeout, seal when a phone goes to the background; operators or users unseal through one interface; servers configured with a remote or service key source unseal automatically after restart.
  • The device private key can optionally be bound to secure hardware so that signatures never leave the hardware and a copied disk image cannot impersonate the device; a recovery join then creates a new device identity instead of keeping the old one, and the documentation explains this trade-off.
  • In-process key material is zeroized, memory is locked against swapping, core dumps are disabled, and keys never enter logs; tests or reviews cover these key requirements.
  • A table states what each unlock source protects against and what it does not: disk image or backup leaks (password, remote unsealing and hardware binding protect; a service key file does not); root access or memory reads while running (none protect); the store provider (all protect); a virtual machine provider reading memory (none protect).
  • The recovery key is generated once when the vault is created and kept by the user; it unlocks any device's local wrapping and can join a new device with the initialization information. It can be rotated: each device re-wraps when it syncs the rotation record, and the old recovery key stops working with the old epoch.
  • A forgotten local password is reset with the recovery key; the device identity stays the same.

Format and algorithm upgrades

Field and algorithm evolution of shared objects never rewrites the original bytes of existing authenticated records; the local database evolves through explicit schemas and transactional migrations. Shared wire format 1 is frozen now that the recovery notice records have landed; local metadata schema 2 (CAS still 1) migrates transactionally from metadata schema 1 and does not read earlier development formats.

  • Encoding: index segments, snapshots, upload manifests, pairing messages, local database values and vault configuration use protobuf. Fields are only added, never removed or redefined; retired field numbers are reserved and never reused. Ordinary unknown fields may be skipped in projections, but unknown, missing or mixed record types reject the whole segment; filtering them and advancing the frontier is not allowed. Keeping unknown authenticated body bytes does not mean unknown semantics can be applied.
  • Identity is bytes: hashed or signed records carry the original bytes the writer encoded once; readers authenticate first and then decode, and never re-encode to compute identities. Reopening, snapshots and forwarding preserve them verbatim. Versions, retirements, member enrollment and authority proofs follow this rule; derived views of member names, roles and capabilities do not modify the enrollment preimage. Pairing remains a keyed BLAKE3 MAC/SAS over the original transcript, with no separate signature.
  • Algorithm identifiers: hashes, signatures, public keys, KDFs, AEADs and chunking use separate registry numbers. Public digest/public key/signature text is algorithm.<Bitcoin-base58>, such as b3., ed25519., x25519.; random 16-byte FileId, SubtreeId, StoreId and VaultId use unprefixed base58 and do not pose as hashes. Public JSON identities use the canonical strings and reject legacy hex, wrong categories, whitespace and invalid encodings. The registry records active/deprecated status and cutoff times; unknown, unimplemented-for-the-purpose or expired algorithms must be rejected, with no downgrade.
  • Vault-pinned algorithms: the Genesis record pins the vault's algorithm set; non-conforming records are rejected. Switching algorithms while running needs a new signed policy record; the mechanism is defined in an ADR and not implemented yet.
  • Format numbers and capability announcements: the CFSSEG02 and CFSSNP03 headers carry the format and algorithm numbers, authenticated by the signature/AAD; the currently supported maximum is 1. Enrollment binds max_format >= 1, and a device may update its own current capability; writes use the minimum of the local actual maximum and the announcements of all unrevoked members. After full outer signature verification, a higher format fails explicitly and stops before candidate key processing and application; ordinary imports from an already authorized author persist a block at that position and on later segments, while other independent devices continue. Snapshots and bootstrap cannot skip the block, nor can a block be established from an unauthenticated header.
  • Local database: the protobuf settings of metadata and CAS store the schema number; only an explicitly new file initializes the schema (metadata 2, CAS 1). A missing, corrupt or zero schema in an existing database is reported as corruption, a higher schema as unsupported; nothing is rebuilt silently. The migration framework only runs registered step-by-step migrations and rolls back as one transaction; metadata registers the 1→2 notice-state table migration; there is no migration for earlier development databases. The existing encrypted and unencrypted boundaries stay, and the configuration labels its KDF explicitly.
  • Object names: follow the current architecture, using canonical algorithm text, hashes and lengths; snapshot names include the publishing device, and the raw frontier bytes and digest stay inside the authenticated payload. Objects with unknown algorithms are logged as diagnostics and ignored; they cannot authorize anything or justify GC deletion.

Scale targets

The target is 1,000,000 current files, 1 TB of bodies across all unretired versions, 10 devices and a 100 GB single file. GB/TB are decimal: 1 GB = 1,000,000,000 bytes, 1 TB = 1,000,000,000,000 bytes; KiB/MiB/GiB are binary, 1 GiB = 1,073,741,824 bytes. The following are acceptance budgets that must be measured, not executed results; the million-file and 100 GB long-running acceptance runs are still in the backlog.

Reference configurationResources and storage
Partial mobile-class devicearm64, 4 CPU cores, 4 GiB RAM, 256 GB storage; keeps the complete index; during acceptance pinned-directory bodies are at most 8 GiB and the read cache budget is 8 GiB; storing 1 TB of bodies is not required. Protected unconfirmed writes and open handles still follow the body retention rules.
Full lab device8 CPU cores, 16 GiB RAM, SSD; each device's capacity must cover 1 TB of unretired bodies, the full local index and staging overhead; stores, other Full devices, Partial caches and optional export copies on the same disk count separately, with 20% headroom reserved. The three copies of the standalone 100 GB scenario need at least 300 GB; in practice at least 360 GB must be free and satisfy the ciphertext/index/staging capacity formula.

The reference link for timing is a shared 1 Gbit/s (1,000,000,000 bit/s) total bandwidth between source and target, round-trip time at most 10 ms, no injected loss and no server-side throttling; storage sustains at least 150 MB/s sequential read/write (MB is decimal). The exact CPU, OS, disk, limits and measured link must be recorded; a high-end host or a VM that is only resource-limited is not execution evidence for phone hardware.

ItemAcceptance ceiling and boundary
Device memoryDuring cold start, index sync, page queries and body sync/verification, each device's process-tree peak RSS and measured cross-thread live heap each stay within 512 MiB (536,870,912 bytes); the sum of process-tree RSS for 10 devices running at once stays within 5 GiB, with store services counted and reported separately. Index, runtime, password unsealing and body selection metadata may not be subtracted.
Cold startWith the complete million-file index already local, the network off, and process and OS page cache cold: from process start to unlock completed and the first limit=256 directory page returned, at most 30 s.
Page queriesOn the static scale fixture, each limit=256 page call for directories, object key prefixes and history takes at most 5 s; scan CPU time is recorded separately. Bounded results do not mean O(limit) scan cost.
First index syncWith the complete fixture already published to the store and the target holding no index/body cache: from starting a configured new device to the complete index and exact frontier converging, at most 1,800 s; Partial and Full are accepted separately, and bodies may not be downloaded wholesale in advance. With 10 devices online at once (1 published source and 9 targets joining concurrently) sharing the link, all indexes converge within 3,600 s.
Full first body syncFrom first index convergence until all 1 TB of unretired bodies are available, streaming digest verification passes and missing_units=0: at most 21,600 s; both phases of a single target's first sync together at most 23,400 s, newest versions first, resuming after interruption.
100 GB sync through a storeExactly 100,000,000,000 bytes generated as a real stream, transferred through an independent source vault, a store and an empty Full target vault: source body import at most 1,800 s, publish at most 3,600 s, target index at most 300 s, target bodies at most 3,600 s, streaming read-back verification at most 1,800 s, the whole run at most 14,400 s. Directory Range and the S3 whole-pack fallback when Range is ignored are accepted separately.

Fixtures, timing start and end points, cold/warm caches, process/heap measurement, capacity calculation, resumption and failure conditions are in the scale budget acceptance procedure. Streaming body windows, single-object limits and page result limits cannot replace the whole-process budgets of this section; current limits such as the 32 MiB authority proofs/snapshots may make scale acceptance fail, and these targets cannot be claimed as met before the long-running acceptance has run.

Server scenario

An application uses the vault as its storage layer: the local copy is only a hot cache, the full data lives in stores and on other devices, and missing bodies are fetched on demand. It shares the same core, the same owner-process model and the same sync with the personal multi-device line; only the shell differs.

  • Daemon cipherfs serve: a separate process holds the vault and serves it over a Unix socket or TCP; a configuration file declares the listen addresses, each vault's path and unlock sources, the stores and their credential sources; it provides readiness and health endpoints and can serve several vaults. Application processes never touch keys; multi-process applications share one daemon. Embedding in-process is only for desktop apps and single-process tools.
  • S3 front door: the daemon speaks S3 to applications, which use existing SDKs and migrate by changing the endpoint. Buckets map to vaults or prefixes, keys to paths, Range GET to range reads, multipart uploads to streaming writes; the versioning API exposes history and concurrent heads, the ETag policy is configurable, and presigned URLs are supported. It listens on loopback only by default; listening elsewhere must be enabled explicitly and requires TLS; every request is SigV4-signed, the Host header is checked, and Basic authentication is not supported; front-door keys are issued per application, rotatable, and unrelated to store credentials. The front door sees plaintext; the machine running the daemon is the trust boundary. Vault-specific capabilities (write durability acknowledgement, pinning, device and store management) are offered through a JSON API.
  • Write durability acknowledgement: a write can specify a wait level — locally persisted, acknowledged by at least one store, acknowledged by at least N Full devices — and returns only once it is met.
  • Application-grade retention policy: very short retirement and grace times are allowed, so deleted attachments can be reclaimed quickly.
  • Pinning and prefetch: pin individual files and prefetch by recent access heat to reduce cold-read latency.
  • Direct read/write by FileId, so applications need not maintain a path mapping.
  • A server is a device with its own identity and the same choice of roles as other devices; a daemon without a configured role defaults to Partial with a budget. A compromised server is handled as a device revocation.

Application access

Applications access the vault by paths and bytes. Rust SyncedVault's put_from_reader, get_to_writer and get_range_to_writer bound the body transfer buffers; put borrows bytes the caller already has, and get/get_range return a Vec whose memory is O(object or requested range size). Core's counterparts are write_from_reader, read_to_writer, read_range_to_writer and write_file/read_file. Bounded body buffers are not the same as a fixed whole-process memory footprint.

Lists and history provide bounded result pages: Core list_page lists a directory's direct children (directories before files of the same name), list_file_entries_page and SyncedVault::list_page list files by literal object key prefix; Core file_history_page and SyncedVault::history_page list history in descending time and VersionId order. Each page holds at most 1024 items and limit must be in 1..=1024; the continuation value is bound to the vault and directory/prefix, and for history also to the path and FileId; it holds no database, file handle or server-side cursor and can be serialized and used across close and reopen. Each call reads the current index; consecutive pages do not promise a cross-page snapshot; concurrent inserts, deletions or retirements change later results, and a history continuation fails explicitly when the path is reused by another FileId. On a static vault the concatenated pages equal the order and results of the corresponding convenience call.

The Vec convenience calls stay; the Vec results of list, history, trash and conflicts use O(number of returned items) memory. Page calls only bound the result items kept (during a scan at most limit+1 items plus one record being decrypted); encrypted index ordering may require rescanning for each page. The full membership/index state, visited IDs and pending parent versions of history traversal, file heads/path contenders and the body unit references of a single version still have costs that grow with the data. C/Go provide streaming callbacks, Python provides reader/writer, and Web HTTP downloads can stream; their bytes/Blob and list convenience calls still allocate the full result, and they currently lack the Rust list/history page calls above.

With sync enabled, background tasks sync, reconnect and retry automatically. A successful write means local persistence is complete, with remote replication following; cancelling a reader upload before EOF publishes no partial object, while a commit accepted after EOF may still complete even if the waiter cancels. Buffered reads that fail return Err and never return a partial object as success; writer/HTTP streams may output an authenticated prefix before failing, and the caller must discard the output or abort publishing the target.

Access audit

Design finalized 2026-10-18, not yet implemented; the current signed index, version history, runtime logs and HTTP tracing do not constitute an access audit. The audit is a file access log: which device (and on a server, which application) did which operation on which VersionId of which FileId, when, with what result and how many bytes. It is a server-line capability; the personal multi-device line shares the same capability but has it off by default. The full design (fields, transaction boundaries, container, acceptance) is in a design document in the development repository and becomes an ADR when implemented.

  • The audit is a unified Core capability reused by every interface; it covers access through Core, the Rust API, the CLI, bindings, FUSE, the Web and the later S3 front door, including cache hits, history/conflict version reads, failed attempts and refusals in the sealed state; background sync, prefetch, retirement and GC are recorded separately under a system identity.
  • Stored like bodies: each device writes its own stream, with a key derived from the vault epoch key and the device ID, signed by the device private key and chained, sealed into immutable objects and published to a separate prefix in the configured stores. It does not enter the shared index and does not affect FileId, VersionId, member rights, file retention or the frontier; index sync and GC neither reference nor delete it.
  • Every unrevoked device in the vault can read it, by file (who touched this file) or by device (what this device did); revoked devices cannot read logs from the new epoch; the recovery key can read the whole history. There is no separate audit recipient or collection server.
  • Only IDs are recorded: the target is the FileId and VersionId; directory operations record the directory's FileId; a lookup of a non-existent name records only the parent directory and one miss. Events contain only typed allow-listed fields and never record passwords, keys, credentials, tokens, request bodies, file names, paths or free-form error text.
  • Three modes, configured per entry point: off is explicitly disabled; best_effort never samples and may degrade but must show loss counts and gaps; required refuses new business access when the audit intent or commit fact cannot be persisted, and never degrades silently. The daemon's HTTP/JSON and S3 front door default to required, mounts provided by the daemon default to best_effort, and the embedded library and CLI default to off. A file change and its commit record are written in the same local transaction.
  • Log retention is independent of file history retention; cleanup is done only by devices holding credentials with delete rights, and writer devices cannot delete published logs.
  • External status must state the mode, the covered entry points, unfinished operations, backlog, loss count and the last persisted/published sequence numbers. Signatures and the chain let any member verify that a device's stream has not been rewritten or had segments removed; an unpublished local tail is not promised to be tamper-proof, and an attacker controlling the running process can forge later records; the design claims nothing stronger.
  • Acceptance follows a checkable entry-point coverage table: partial entry points, best_effort, or tests through ordinary logs alone do not count as a complete audit delivery.

Interfaces

The core contains encryption, the file model, storage transactions, version and sync rules; interfaces only adapt: they do not copy the database, do not define their own rules, and do not report unimplemented operations as successful.

Main interfaces: Rust API, CLI, FUSE mount, Web. C/Python/Go bindings follow the Rust API and add no features of their own. WebDAV, the S3 gateway and the TUI are demoted to examples and are outside the acceptance scope.

On Partial devices, mounts keep system indexers, previews and backup tools from scanning everything and pulling every body back to the local device; files not held locally are listed as usual and fetched only when read.

No lock-in

A Full device that has completed body sync relative to its locally accepted index frontier, with a complete local cache, can export selected current files and optional unretired history/conflict bodies offline without any other device or store online; this does not mean it has seen new changes not yet synced to it. A Full device still catching up and a Partial device can only export the bodies they hold locally offline; export_plaintext explicitly lists the missing target paths and cannot promise a complete export; retired bodies are not guaranteed to be recoverable.

The export target must be a new directory that does not exist, its parent path must exist and contain no symbolic links, and the selected keys and archive paths must be representable losslessly by the target's native filesystem; a alongside a/b, case collisions or native name/path collisions fail explicitly, with no automatic renaming. ExportOptions selects history and conflicts independently; older non-head versions are archived under their original paths, and extra heads under the current export path of their FileId; once selection completes, the versions and bodies are kept until the export ends, and concurrent changes or retirements do not change that selection. Bodies are streamed out, and selection metadata grows with the export size; corruption, authentication and I/O errors abort; the missing report only counts unavailable bodies, and every successful file has complete content.

Self-describing vault

Data must be recoverable without this software. The vault carries everything needed to read it, on by default and optionally off when the vault is created; turning it off only affects whether the description files are written, not the format.

  • Specification: the vault root holds a plaintext FORMAT.md, exact to the byte: directory layout and object naming, the base58 alphabet; the byte layout of container headers such as CFSSEG02/CFSSNP03 and which bytes enter the signature and which enter the AEAD AAD; every step of the key chain (password → Argon2id parameters → local wrapping → master key → epoch key → subtree key → content key) and the literal text of every HKDF label; the public standard reference for each ID in the algorithm registry; the complete cipherfs.proto source; and the step-by-step procedure to recover the current file list and bodies holding only the password or the recovery key. The document is written from the reader's point of view, not reverse-engineered from the implementation.
  • Test vectors: the vault root holds public golden vectors and their expected outputs, covering every step of the specification, for implementers to check step by step.
  • Reference decryptor: a single-file script in a general-purpose language other than Rust, depending only on implementations of public standard algorithms, limited to listing files and exporting plaintext with the password or recovery key, with no sync or writes. It ships with the vault and with release packages and is the executable form of the specification.
  • Consistency enforced by tests: the FORMAT.md in the vault is byte-identical to the specification file in the code repository; magic numbers, HKDF labels and algorithm parameters in the specification are compared one by one with code constants, never copied by hand; a format upgrade rewrites the files in the vault and keeps the old specification; full verification must use the reference decryptor, without Rust code, to decrypt the vector vault and match the plaintext.
  • Stores can optionally keep the same specification and decryptor (on by default, optionally off when adding a store). This plaintext only describes the format and contains no keys or user data; object names already reveal that a store belongs to this software, so it adds no leak.

Explicitly not promised

  • Under true concurrency the default current version depends on device clocks; it only affects presentation and loses no data.
  • In a P2P-only vault without stores, two devices that are not online at the same time cannot exchange data.
  • No multi-user sharing or permissions; every device in a vault has the same rights.
  • Versions retired after the grace period ends are no longer guaranteed recoverable from any device.
  • When a Partial device reads a file it does not hold and no device or store holding the body is reachable, the read fails explicitly and does not hang. Buffered calls return Err and never return partial content as success; writer/HTTP streams may already have output an authenticated prefix before a late missing-body or other error, and after failure the caller must discard the output or abort publishing the target.

Source file in the repository