Status: implemented alpha; externally unvalidated. Do not rely on this for a real legal matter yet. habitable is a working reference implementation with automated tests. It has not received an independent security/legal review, real pilot, or court validation. The default development timestamp authority is explicitly non-production. Until a release says otherwise, treat habitable as a prototype for evaluation, not as a tool to protect a tenant who is actually in a fight with a landlord. This document is part of how we earn the right to drop that warning, by saying plainly what is protected, what is not, and where the residual risk sits.
This document is the companion to the Hard rules and Honest limits sections of the README. It states the adversary we design against, the assets we protect, the trust boundaries and what each party can see, what is and is not protected, and the residual risk that remains after each mitigation.
The threat model is a landlord who retaliates, together with their lawyer, and treated as an adversary with resources and motive — not a casual snoop.
We assume the adversary may:
- Retaliate against a tenant for documenting conditions or organizing — with eviction filings, rent disputes, harassment, or selective enforcement — and will use anything they learn to do it.
- Gain physical access to a device. A phone or laptop may be seized, borrowed, repaired, searched at a doorstep, or examined during an eviction or a building-access dispute. Assume the adversary can, at some point, hold the hardware.
- Pressure or subpoena third parties. A lawyer can subpoena a company, an ISP, or an infrastructure operator, and can pressure a less-careful organizer or tenant. The design goal is that there is no third party holding a tenant's contents to subpoena — the only optional network parties (a relay and a timestamp authority) never hold the contents in the first place.
- Contest the evidence. Even a landlord with no access to the data will, in court, attack the credibility of a bare photo — "you could have taken that anytime," "you could have edited it." Much of the design exists to answer that attack with something independently checkable.
We do not claim to defend against every adversary. A state-level actor, a targeted device exploit, or a sufficiently capable forensic lab can defeat parts of this model; the limits are called out explicitly in §5 and §6.
| Asset | Why it matters |
|---|---|
| Tenant identity and location | A home address tied to a tenant who is organizing is exactly what enables retaliation. Originals carry EXIF GPS and capture time. Packet shared-media copies strip embedded metadata by default; retention settings, embedded originals, sync, or organizer sharing can disclose it and must be reviewed before handoff. |
| Case contents | The photos, video, condition notes, and timeline are sensitive both as private home imagery and as the substance of a legal position the landlord wants to know and weaken. |
| Integrity of the evidence | The whole value of the tool is that a record was not altered after capture and existed by a given time. If integrity can be quietly broken, the evidence is worthless or worse. |
| Organizer identities | Who is helping whom — which organizer works which building — is sensitive. Exposure invites targeted retaliation and chills organizing. Custody logs record who did what to an item; that "who" must stay inside the union. |
habitable has three parties that can ever touch a case, in decreasing order of trust. The design goal is that trust decreases sharply as you move away from the device, and that the parties outside the device never see contents at all.
The device is the trusted base. It is the only place that ever holds plaintext or key material.
- A vault is a directory holding one case (
vault.py). Sealed originals, the CRDT case document, the chain of custody, the device identity, and the deferred-timestamp queue are all encrypted at rest under a data-encryption key (DEK). - The DEK is a random 32-byte key used with ChaCha20-Poly1305 (an AEAD: confidentiality plus
integrity). It is itself wrapped under a key-encryption key (KEK) derived from the user's
passphrase with scrypt (
crypto.py). - Inside the vault tree, persistent case contents —
case.enc,custody.enc,identity.enc,deferred.enc, sealedoriginals/, and consolidated timestamp-token sidecars — are ciphertext. A sidecar's deterministicsha256(capture_id)filename leaks equality/linkability; its ciphertext length approximates token volume, and filesystemmtime/ctimeexpose update timing. There is no padding or filesystem-metadata hiding. Tokens become public by design in sync/packet formats; at-rest AEAD is confidentiality and integrity, not proof that a TSA or token is authentic.config.tomlremains plaintext, including timestamp-authority names/URLs and other user-edited policy/template values; the passphrase-wrappedkeyfile.jsonis also visible. - On first successful unlock, legacy primary/additional/archive token JSON is strictly validated, encrypted, flushed, atomically published, and reread before the exact plaintext inodes are unlinked. Partial cleanup resumes after a crash and disagreement fails closed. Unlinking is not secure erasure, directory flushes are unavailable on some filesystems, and concurrent writers or hostile/lying storage remain outside the guarantee.
- Token child operations are descriptor-relative beneath one no-follow
tokens/handle whose attachment is rechecked before success. A directory-swap attack fails rather than following a replacement symlink. Platforms without the required descriptor-relative primitives are unsupported for the whole vault; create/open fail closed. - Browser upload and packet-sanitization tools sometimes require a filesystem path. Their
plaintext working copy is a random file in a short-lived OS temporary directory outside the
vault, never the old vault
_incomingpath. On POSIX the directory/file are explicitly0700/0600; generic names reveal no client filename or case/capture id; partial writes and downstream exceptions clean the whole workspace. App startup removes the legacy reserved_incomingpath without following symlinks. - The device also holds the Ed25519 signing key (signs custody entries, sync messages, packets) and the X25519 key-agreement key (receives sealed sync deltas).
What an adversary with the device gets: the ciphertext, the keyfile, and the config. Without the passphrase they do not get the contents. With the passphrase (or while the vault is unlocked) they get everything — see §6 (physical access / duress).
A union that cannot sync device-to-device can run a relay (relay.py, RelayClient in sync.py).
It is deliberately a dumb mailbox: ciphertext in, ciphertext out.
- Every sync message is sealed to the recipient's public key before it leaves the sender
(
seal_toincrypto.py), so the relay only ever stores opaque blobs per room and hands them back. It cannot read anything. - A sender key is not implicitly trusted. Peers exchange signed, recipient-sealed,
case-bound pairing material first; import requires the complete allowlisted
identity, pairing-key authentication, matching message/state case ids, and an
unseen replay id. See
sync-threat-model.md. - The relay writes no per-request logs by default; per-request access logging is opt-in
(
HABITABLE_RELAY_LOG=json). Its logs are always metadata-only — a structured JSON line never carries a peer address, a room id (the logged route is redacted to/rooms/{room}), or message contents. The server error hook replaces the stdlib's peer-address/traceback stderr dump: unauthenticated connection errors are silent, and unexpected faults emit only a fixed{ts,level,msg}failure event. Aggregate passthrough, retained-state, capacity-rejection, and rejected-journal counts remain exposed only via/healthz; no room id, token, path, or body is included. Those retained-state counts describe process memory, so/healthzalso reports astartup_replaystate: an operator must not readrooms: 0as "holding nothing" when the relay refused to read its own opt-in journal (seerelay-operator-self-audit.md§4.7–§4.8).
What the relay can nonetheless see — connection metadata: because it forwards traffic, it
necessarily observes who connects, to which room, when, and roughly how much data moves. That
is metadata, not contents, but it is real (see §5 and §6). The mitigations are: a no-log,
self-hostable relay (a union runs its own), and pure peer-to-peer sync with no relay at all
(LocalDirTransport over a shared directory / USB / AirDrop-style transfer), which removes the
party entirely.
To prove a record existed by a certain time, the tool sends a timestamp request to an RFC 3161
authority (tsa.py).
- The authority receives only the SHA-256 hash of the content (the message imprint), never the file and never any case metadata. It returns a signed token that says "this exact digest existed no later than this time."
- A hash discloses nothing about the photo: the authority cannot tell what was photographed, who took it, or where. Multiple authorities can be configured so the proof does not rest on one party.
What the authority can see: that someone asked it to stamp some 32-byte hash at a given time, plus whatever the network exposes (the requester's IP, unless the request is proxied). It learns nothing about the contents.
| Property | How it is achieved | Where |
|---|---|---|
| Confidentiality at rest | Every vault blob and sealed original is encrypted with ChaCha20-Poly1305 under the DEK, which is wrapped under a scrypt-derived KEK from the user's passphrase. | crypto.py, vault.py |
| End-to-end encryption and peer authorization in sync | Each sync message is sealed to the recipient's X25519 key, signed by the sender's Ed25519 key, and HMAC-authenticated with signed-and-sealed, case-bound pairing material. Exact allowlists, mandatory case/recipient checks, and replay ids fail closed before merge. | pairing.py, sync.py, crypto.py |
| Tamper-evidence (content) | Originals are hashed (SHA-256) at capture and sealed byte-for-byte; every read re-checks the hash, so silent corruption or tampering surfaces as a FixityError instead of a quietly altered exhibit. |
evidence.py (verify_fixity), vault.py (read_original) |
| Tamper-evidence (sequence) | The chain of custody is an append-only, hash-linked log: each entry commits to the previous entry's hash, so any insertion, deletion, or reordering breaks the chain detectably. Entries can also be Ed25519-signed. | evidence.py (CustodyLog) |
| Upper-bound timestamps | A SHA-256 hash is sent to an RFC 3161 authority, which returns a signed token proving the content existed no later than that time. Tokens travel inside the packet for offline verification, and the verifier checks the signature and certificate chain. | tsa.py (Rfc3161HttpTSA, verify_token) |
| Proof durability over time | Archive (re-)timestamping re-stamps over each capture's most recent token before the issuing authority's certificate or hash algorithm ages out (RFC 4998-style chaining). The existence proof stays anchored at the primary token's time while staying verifiable under a current authority; the standalone verifier walks the chain and fails closed on any break. | tsa.py (retimestamp, verify_archive_chain), capture.py (retimestamp_all) |
| Default packet metadata minimization | The sealed original keeps EXIF (capture time, GPS) for evidentiary integrity; packet shared-media copies strip embedded metadata by default. Signed disclosures state the configured handling and whether originals were embedded. | README Hard rules #4; exif.py; packet.py |
| Custody-identity minimization in packets | Encrypted in-vault entries hold the clear actor, salt, commitment, and signature. The public packet proof drops the clear actor, salt, and per-entry signature, retains the salted actor commitment, and re-hashes the identity-stripped chain. | evidence.py (public_payload, redacted, integrity_proof) |
| No telemetry / no analytics / no phone-home | The tool collects no analytics and contacts no servers it is not told to. The relay logs only aggregate ciphertext-passthrough counts. There is no account system and no central database. | README Hard rules #1, #5; relay.py |
Being precise about the boundaries is part of being credible. A tool that overpromises in a courtroom fails the people relying on it.
- A timestamp proves when, not who or what. An RFC 3161 token bounds the time the content existed — an upper bound on creation. It does not prove who created the content or that the content depicts what a tenant says it depicts. Tamper-evidence and a timestamp strengthen a true record; neither manufactures a case the facts do not support.
- The local custody log is tamper-evident, not tamper-proof, against the device owner. The hash-linked chain makes after-the-fact alteration detectable by anyone who verifies it. It does not prevent the holder of the vault key from discarding the whole log and writing a new internally-consistent one before any external party has seen the head hash. Detection depends on an external anchor (a counterpart who already holds the chain head, or a timestamp over it). The chain answers "was this record altered after the fact?" — it cannot bind a hostile keyholder.
- Relay metadata is only partly hidden. Even a no-log relay observes who syncs with whom and
when, and — without extra measures — roughly how much moves. The opt-in
PaddingTransport(sync.py, EXP-12) reduces two of these: it pads every message to block-sized buckets and posts fixed-size cover batches, so per-message size and real-message count (up to the batch size) stop tracking the real payload. It does not hide the room id, the peer IP addresses, or that a room is active and roughly when, and it is not an anonymity network (no cross-sender mixing, no IP-correlation defence); it also has not yet had the external traffic-analysis review such a claim requires. The only way to remove relay metadata entirely is to not use a relay (pure peer-to-peer). Seerelay-observability-matrix.md§4.5 for the exact residual. - The duress-safe state is planned, not implemented. It is described here as a future mitigation;
a
grepforduress/panic/decoyacrosssrc/andapp/returns nothing today. When built, opening the app to a duress-safe state will hide case contents from someone glancing at the screen or coercing a quick unlock. Even then it will not be a guarantee against a sufficiently capable coercing adversary (who can compel the real passphrase) or a forensic adversary (who images the device and analyzes storage at rest). It is planned as a harm-reduction mitigation with documented limits, not a safe. - Lost keys with no backup mean lost data. There is no operator, no account recovery, and no
one who can read or reset a union's data. A lost passphrase with no recovery blob (
crypto.pyexport_recovery_blob) and no surviving synced peer means the data is unrecoverable — by design. The flip side of "no one can be subpoenaed for it" is "no one can recover it for you." - The development TSA is non-production.
DevTSAintsa.pysigns with a local Ed25519 key and is reported as an untrusted chain; its tokens self-describe as non-production.LocalRfc3161TSAissues real RFC 3161 tokens but from a self-signed authority for tests and demos. Neither is a trusted third-party time source. Real evidence requires a real public RFC 3161 authority (Rfc3161HttpTSA) whose certificate chains to a trusted root. - Not legal advice, and no guarantee of admissibility. habitable produces well-documented evidence. Whether a court or agency admits it, or how much weight it carries, is a legal question this tool cannot answer.
- Endpoint compromise defeats everything. Confidentiality at rest protects a locked vault on a clean device. Malware on an unlocked device, a keylogger capturing the passphrase, or a screen recorder defeats the encryption entirely. The cryptography assumes a trustworthy endpoint.
- Temporary cleanup is not secure erasure. Browser JSON/base64 and decoded media exist in
process memory, and path-based libraries briefly need an OS temporary file. An abrupt power loss
or
SIGKILLcan prevent ordinary cleanup; unlinking does not defeat swap, filesystem snapshots, SSD remanence, privileged malware, or forensic recovery. Keep the endpoint and its temporary volume under full-disk encryption; habitable does not claim to sanitize free space.
| Threat | Mitigation | Residual risk |
|---|---|---|
| Device seized while locked | ChaCha20-Poly1305 at rest under a scrypt-wrapped DEK; contents and identity are ciphertext. | Offline guessing of a weak passphrase; future cryptographic breaks; the keyfile and ciphertext are in the adversary's hands for as long as they keep the device. |
| Device seized while unlocked, or passphrase coerced | Passphrase rotation; recovery blob under an independent passphrase. (A duress-safe open state to hide case contents is planned, not yet implemented.) | Not a guarantee against coercion or forensic imaging; an unlocked vault exposes plaintext; a compelled passphrase reveals everything. |
| Plaintext media working copy recovered | Browser/packet path files use random names, an owner-only workspace outside the vault, and cleanup on partial writes, normal completion, and exceptions; old _incoming is purged without following symlinks. |
Memory, OS temp, swap, crash remnants, snapshots, and storage forensics remain in the endpoint trust boundary; unlink is not secure erasure. |
| Relay operator or its subpoena | Messages sealed to recipient keys before leaving the sender; no-log, self-hostable relay; fixed live/startup bounds; pure peer-to-peer option removes the party entirely. | Connection metadata (who/when/how-much) is visible to any relay; opt-in persistence also writes ciphertext plus room/token/time metadata. Journal/temp unlink is not secure erasure, persistence is not fsync-backed delivery, and only peer-to-peer sync avoids the relay. |
| Timestamp authority compromised, colluding, or subpoenaed | Authority sees only a SHA-256 hash; multiple authorities configurable; tokens verified offline against their certificate chain. | A single TSA could backdate or refuse; a hash leak still reveals nothing about contents; trust in any one TSA is reduced, not eliminated, by using several. |
| Evidence altered after capture | SHA-256 fixity re-checked on every read; append-only hash-linked custody; RFC 3161 upper-bound timestamps, kept durable by archive re-timestamping before an authority ages out; standalone verifier. | Custody is tamper-evident only: a hostile keyholder can rewrite the whole local chain before any external anchor exists; detection needs a counterpart or a timestamp over the head. |
| Tenant location leaked through sharing | Packet shared-media copies strip embedded metadata by default, and signed disclosures state metadata/original choices. | A retention policy, --include-originals, sync/organizer share, screenshot, or forwarded original can carry location. Recipients can make further copies. |
| Organizer identity exposed via packet records | Public custody drops the clear actor, salt, and per-entry signature but retains the salted actor commitment. | A breached vault exposes the clear actor and salt; commitment correlation across packets or out-of-band knowledge can still re-identify. |
| Tracking via telemetry | No analytics, no telemetry, no phone-home; relay logs only aggregate counts. | Network-level observation (ISP, Wi-Fi operator) of connections is outside the app's control; use of Tor/VPN is the user's responsibility. |
| Lost access to data | Encrypted recovery blob; multi-peer sync replicates the case; encrypted backup. | No operator-side recovery: lost passphrase + no recovery blob + no surviving peer = permanent data loss, by design. |
habitable concentrates trust on the device, keeps the relay to ciphertext plus unavoidable connection metadata, and shows the timestamp authority only a hash. It protects confidentiality at rest, end-to-end encryption in sync, content and sequence tamper-evidence, default packet metadata minimization, and custody-identity minimization, with no telemetry. It does not protect against a hostile keyholder rewriting the local chain before any external anchor, relay metadata, a coercing or forensic adversary with the unlocked device (the duress-safe state that would blunt this is planned, not yet implemented), lost keys with no backup, or an endpoint that is already compromised — and a timestamp proves only when, never who or what.
This is alpha / concept-stage software. It must not be relied on for a real legal matter yet. When that changes, this document and the README will say so explicitly.