From d50a0c90a714f79d62c9a1d12217e3c560d6384f Mon Sep 17 00:00:00 2001 From: Wessel Verheij Date: Sun, 13 Sep 2026 18:26:46 +0200 Subject: [PATCH] docs(sync): the relay never carried the ledger, and half of what it holds is readable Two sentences about the relay have been in this specification since the founding commit and neither is true of the product. The first is that op-log entries wait in the relay when a peer is asleep. They do not. An operation travels only inside a mutually-authenticated session between two paired devices; the mobile sync pass reaches a relay for key material only, and says so in its own comment. Where neither device can reach the other, the changes wait on the device that made them. The relay setting's own help string has said exactly that since 2026-09-05, and a test pins it -- including that it must not promise a pull the phone cannot make. Since then the specification and the shipped string have been contradicting each other outright, and the shipped string was the honest one. The second is that what a relay holds is opaque ciphertext. Half of it is. An epoch wrap is sealed to the receiving device's X25519 key and cannot be opened. A pairing frame is not sealed at all: it is JSON handed to the mailbox as written, and between the two frame types and the addressing that carries them a relay operator reads both device identifiers, the responder's Ed25519 and X25519 public keys, the pairing token's hash, and the name the responder's device goes by -- on a phone often one a person chose. Only the confirming frame is signed; the accepting one is not, so "signed but not encrypted" is true of one of the two and not of both. None of that is a defect in the design. A pairing frame is public-key material exchanged before any shared secret exists, and the safety number both readers compare is what makes a swapped key fail. It is a defect in the description: a reader who believes a relay holds only ciphertext concludes that nominating one reveals nothing. ADR-0033 records the ruling and retires the term "zero-knowledge relay" rather than redefining it -- a house definition cannot repair a word whose outside meaning is the overclaim, and the property that is actually true is narrower, checkable and already written down: no cryptography on a blob, and never looking inside one. The thing is a pairing relay. ADR-0016 and ADR-0004 keep their bodies and get notes naming the claims that no longer hold; ADR-0016 keeps its filename too, because its title is a fact about what was decided in June. Seventeen further pages carried the claim downstream of that term, including the FAQ the public site renders live and the roadmap line it pulls into its milestones. E3-R24 pins what may cross a relay and that both halves of what it holds must be described; G1-R22 pins saying so in the product's own copy. No existing requirement changes meaning: E3-R7 and E3-R14 were accurate and stand as written. Signed-off-by: Wessel Verheij --- .../decisions/0004-local-only-hosting.md | 11 +- ...16-noise-transport-zero-knowledge-relay.md | 15 +- ...he-relay-carries-pairing-not-the-ledger.md | 170 ++++++++++++++++++ 00-overview/decisions/README.md | 3 +- 00-overview/glossary.md | 5 +- 00-overview/roadmap.md | 9 +- 00-overview/vision.md | 23 ++- .../features/e-sync/e2-device-pairing.md | 10 +- 10-functional/features/e-sync/e3-transport.md | 62 +++++-- .../features/e-sync/e5-mobile-peer.md | 7 +- 10-functional/features/g-ux/g1-privacy.md | 14 +- .../features/g-ux/g5-plain-language.md | 4 +- 10-functional/journeys/j2-daily-use.md | 4 +- 10-functional/journeys/j5-adding-a-device.md | 13 +- 20-architecture/data-flow.md | 6 +- 20-architecture/system-context.md | 2 +- 30-repos/website.md | 2 +- 40-quality/security.md | 6 +- 60-brand/surface-mapping.md | 3 +- 90-appendix/data-retention.md | 2 +- 90-appendix/faq.md | 14 +- 21 files changed, 323 insertions(+), 62 deletions(-) create mode 100644 00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md diff --git a/00-overview/decisions/0004-local-only-hosting.md b/00-overview/decisions/0004-local-only-hosting.md index fdfa395..2483b0e 100644 --- a/00-overview/decisions/0004-local-only-hosting.md +++ b/00-overview/decisions/0004-local-only-hosting.md @@ -1,9 +1,18 @@ # ADR-0004: Local-only hosting; no cloud, telemetry, or remote logging -**Status:** Accepted +**Status:** Accepted; the relay's "ciphertext only" claim superseded by +[ADR-0033](0033-the-relay-carries-pairing-not-the-ledger.md) **Date:** 2026-05-27 **Graduated from:** product-repo Phase 17, decision D-32 +> **One claim in this record no longer holds.** "The optional relay holds +> ciphertext only and can decrypt nothing" is true of the key handovers and +> false of the pairing frames, which are not sealed — and no operation crosses a +> relay at all +> ([ADR-0033](0033-the-relay-carries-pairing-not-the-ledger.md)). What this +> record decides is unaffected: there is still no cloud, no telemetry and no +> remote logging, and a relay is still off until the reader nominates one. + ## Context The data Beatrax processes — full bank-account history, credit-card statements, diff --git a/00-overview/decisions/0016-noise-transport-zero-knowledge-relay.md b/00-overview/decisions/0016-noise-transport-zero-knowledge-relay.md index 3b9a40e..d1ae36e 100644 --- a/00-overview/decisions/0016-noise-transport-zero-knowledge-relay.md +++ b/00-overview/decisions/0016-noise-transport-zero-knowledge-relay.md @@ -2,7 +2,9 @@ **Status:** Accepted; the sealed-epoch channel precondition and the relay's "no cryptography at all" claim superseded by -[ADR-0031](0031-a-signed-wrap-is-independent-of-its-channel.md) +[ADR-0031](0031-a-signed-wrap-is-independent-of-its-channel.md); its +store-and-forward role and its "opaque ciphertext blobs" claim superseded by +[ADR-0033](0033-the-relay-carries-pairing-not-the-ledger.md) **Date:** 2026-06-15 > **Two claims in this record no longer hold.** A sealed epoch wrap now carries a @@ -15,6 +17,17 @@ > ([ADR-0031](0031-a-signed-wrap-is-independent-of-its-channel.md)). Everything > else below stands: the Noise IK/XX transport and its framing, the discovery > ladder, blob opacity, expiry and caps, and the relay being off by default. +> +> **Two more do not either, and one of them is in the note above.** The relay +> never carried op-log entries. Operations cross only a mutually-authenticated +> session between two paired devices; where neither can reach the other, the +> changes wait on the device that made them. And blob opacity is the half of the +> note above that does not stand: a group-key epoch is sealed to its recipient, +> but a pairing frame is plaintext JSON naming both device identifiers, the +> responder's public keys and the name its device goes by. The discovery +> ladder's third rung is a road to a peer for pairing and key material, not a +> road for the ledger +> ([ADR-0033](0033-the-relay-carries-pairing-not-the-ledger.md)). ## Context diff --git a/00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md b/00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md new file mode 100644 index 0000000..f998a5f --- /dev/null +++ b/00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md @@ -0,0 +1,170 @@ +# ADR-0033: The relay carries pairing material, not the ledger, and "zero-knowledge" is retired + +**Status:** Accepted +**Date:** 2026-09-13 +**Supersedes:** two claims in +[ADR-0016](0016-noise-transport-zero-knowledge-relay.md) — that the relay is the +store-and-forward fallback that holds op-log entries until an offline peer comes +back, and that it holds opaque ciphertext blobs — and the same second claim +where [ADR-0004](0004-local-only-hosting.md) restates it. Everything else in both +records stands: the Noise IK/XX transport and its framing, the discovery ladder, +expiry and caps, the per-device drain credential, and the relay being off by +default. + +## Context + +Two sentences about the relay have been in this specification since it was +written, and both are false of the product it describes. + +**One: that op-log entries wait in the relay when a peer is asleep.** They do +not. An operation travels only inside a mutually-authenticated session between +two paired devices ([E3](../../10-functional/features/e-sync/e3-transport.md)). +A relay holds blobs, not sessions, and no format exists for sealing an op-log +entry to a mailbox. The mobile sync pass dials the local network, then reaches +the relay for key material only; its own comment says the relay "carries no ops +— only epoch wraps". When neither device can see the other, the changes wait on +the device that made them. The product's own copy has said so on the relay +setting since 2026-09-05, when the sweep of screens that stated one condition +and enforced another reached it, and a test pins the sentence — including that +it must not promise a pull the phone cannot make. Since then the specification +and the shipped string have been contradicting each other outright, and the +shipped string was the honest one. + +**Two: that what the relay holds is opaque ciphertext.** Half of it is. The key +handovers are sealed to the receiving device's public key, and whoever runs the +relay cannot open them. The pairing frames are not sealed at all: they are JSON, +handed to the mailbox as written. Between the accepting frame, the confirming +frame and the mailbox addressing that carries either, a relay operator can read + +- both device identifiers — the accepting frame names the responder, the + confirming frame names both, and every mailbox row is addressed from one to + the other regardless; +- the responder's Ed25519 signing key and X25519 key-agreement key, as hex; +- the pairing token's hash; +- the responder's device name, which on a desktop is a neutral platform label + and on a phone is the name the operating system reports for it — often one a + person chose, and sometimes one carrying their own name. + +Only the confirming frame is signed. The accepting frame is not, so "signed but +not encrypted" is true of one of the two and would be a second overstatement if +said of both. + +None of this is a defect. A pairing frame is public-key material and the name of +a device, exchanged before any shared secret exists, and the safety number the +two readers compare is what makes a swapped key fail. It is a defect in the +*description*: a reader who believes the relay holds only ciphertext concludes +that nominating one reveals nothing, and that conclusion is wrong. + +### The term is the third problem + +"Zero-knowledge relay" is what the glossary and ADR-0016's own title call it. +The name is doing work the design does not do. Whatever a reader takes +"zero-knowledge" to mean — the proof systems it names in cryptography, or the +industry's looser "the operator cannot read it even if it wants to" — neither +survives contact with a plaintext pairing frame. The relay's real discipline is +narrower and stronger for being checkable: it performs no cryptographic +operation on a blob and never looks inside one, asserted by test +([E3-R7](../../10-functional/features/e-sync/e3-transport.md#acceptance-criteria), +[ADR-0031](0031-a-signed-wrap-is-independent-of-its-channel.md)). That is a +statement about what the relay *does*. "Zero-knowledge" is a statement about +what it *could* do, and it is not true. + +A term whose outside meaning is the overclaim cannot be repaired with a house +definition. The glossary already retires two words on exactly this ground — +"cloud sync" and, for the relay, "server" — because the connotation promises +more than the thing delivers. This is the same move, applied to a word the +specification was making the promise with rather than one it was avoiding. + +## Decision + +**A relay's job is pairing, and only pairing.** + +- An op-log entry MUST NOT be delivered over a relay. Operations cross only a + mutually-authenticated session between two paired devices. +- A relay exists so that two devices that cannot reach each other directly can + complete the pairing ceremony and exchange group-key epochs. +- Where a peer is unreachable, changes wait on the device that made them until + both are awake on the same network. That is the answer, and it is stated + rather than implied. + +**What a relay holds is described in both halves, always.** + +- Key handovers are sealed to the receiving device and cannot be read by the + relay. Their envelope is not sealed: it names both device identifiers and the + epoch, and carries the sender's signature. +- Pairing frames are not sealed. Any description of the relay that names the + sealed half MUST name the unsealed one in the same breath, together with the + metadata a relay sees — sizes, timing, and which device identifiers exchange + traffic. + +**The term "zero-knowledge relay" is retired.** + +- The thing is a **pairing relay**, or simply a relay. The word names the job, + which is what makes "a transaction never crosses it" read as a description + rather than a surprise. +- "Zero-knowledge" is added to the glossary's deliberately-not-used list, in + both its relay and its sync forms. +- **ADR-0016 keeps its title and its filename.** The record is immutable and its + title is a fact about what was decided on 2026-06-15. Renaming it would edit + the history this repository keeps decisions for. + +## Alternatives considered + +| Option | Why it lost | +|--------|-------------| +| **Redefine "zero-knowledge" in the glossary to mean what the relay actually does** | Publishes a private meaning for a public term. The glossary exists to stop a reader importing an outside definition; it cannot do that for a word whose outside definition *is* the overclaim. A reader who never opens the glossary is the one being misled, and that reader is most of them. | +| **Correct the sentences and keep the term** | The sentences are downstream of the name. Every surface that inherited "zero-knowledge relay" inherited the claim with it, which is how one phrase in one ADR reached the public website. Leaving the source in place schedules the next recurrence. | +| **Say less about the relay** | The tempting option and the worst one. A page that goes quiet about what a relay holds has not become accurate, it has become unfalsifiable — and this product's whole argument is that its privacy claims are checkable. [G1](../../10-functional/features/g-ux/g1-privacy.md) and [G5](../../10-functional/features/g-ux/g5-plain-language.md) already forbid it. | +| **Seal the pairing frames and keep the claim true** | A code change, and one that buys little: the frames carry public keys and a device label, and a receiver with no shared secret yet has nothing to unseal with. Sealing to the recipient's X25519 key would be possible for the confirming frame and is worth considering on its own merits — but it would not be a reason to describe today's relay as though it were already done. | +| **Edit ADR-0016 and ADR-0004 in place** | Forbidden, and for the same reason [ADR-0031](0031-a-signed-wrap-is-independent-of-its-channel.md) refused it: the record of changing our mind is the valuable part. Both records get a note naming the claims that no longer hold and pointing here. | + +## Consequences + +### Positive + +- The specification and the product's own copy agree. The in-app relay help, + which was already accurate, stops being the only honest description in the + organisation. +- A reader deciding whether to nominate a relay can now decide on what it would + actually learn: that two identified devices paired, when, and how big the + frames were. +- "Pairing relay" makes the scope self-evident. The old name invited the + question "so my transactions go through it?", and the honest answer needed a + paragraph. + +### Negative + +- Removing "zero-knowledge" removes a phrase that read well. The replacement is + longer everywhere it appears, because the truth has two halves and both have + to be said. +- ADR-0016's filename and title keep a term the rest of the specification no + longer uses. That is a deliberate inconsistency and is recorded here so it is + not later mistaken for an oversight. + +### Neutral + +- No requirement changes meaning. [E3-R7](../../10-functional/features/e-sync/e3-transport.md#acceptance-criteria) + (no cryptography on a blob, never look inside one) and + [E3-R14](../../10-functional/features/e-sync/e3-transport.md#acceptance-criteria) + (document the metadata, claim no traffic-analysis resistance) were accurate + and stay as written. `E3-R24` and `G1-R22` are added for what crosses a relay + and for saying so in the product's own copy, neither of which anything + previously required. +- Nothing changes about LAN-direct sync, which is where operations have always + travelled. + +## Revisit if + +- Relay delivery of op-log entries is ever built, which would need a sealed + op-envelope format and would be a new decision rather than an extension of + this one. +- Pairing frames gain a seal, at which point the unsealed half of the + description shrinks to the mailbox addressing and the metadata. + +## Related + +- [ADR-0016](0016-noise-transport-zero-knowledge-relay.md) — the record this supersedes in part +- [ADR-0004](0004-local-only-hosting.md) — the same claim, restated +- [ADR-0031](0031-a-signed-wrap-is-independent-of-its-channel.md) · [ADR-0015](0015-multi-master-p2p-sync.md) +- [E2 Device pairing](../../10-functional/features/e-sync/e2-device-pairing.md) · [E3 Encrypted transport, LAN-direct and relay](../../10-functional/features/e-sync/e3-transport.md) +- [G1 Privacy](../../10-functional/features/g-ux/g1-privacy.md) · [40-quality/security.md](../../40-quality/security.md) diff --git a/00-overview/decisions/README.md b/00-overview/decisions/README.md index 7ccf369..29f7b65 100644 --- a/00-overview/decisions/README.md +++ b/00-overview/decisions/README.md @@ -64,6 +64,7 @@ of them invent a decision that was not already made. | [0030](0030-the-tag-governs-the-workflow-not-what-it-reads.md) | The major tag governs the workflow definition, not what the workflow reads | Accepted | New | | [0031](0031-a-signed-wrap-is-independent-of-its-channel.md) | A signed epoch wrap is trusted on its signature, not on its channel | Accepted | Supersedes part of [0016](0016-noise-transport-zero-knowledge-relay.md) | | [0032](0032-all-four-stores-additive-to-direct-download.md) | All four stores, and direct download is not retired | Accepted | Supersedes part of [0006](0006-nativephp-desktop-shell.md) and [0019](0019-asymmetric-release-publish.md) | +| [0033](0033-the-relay-carries-pairing-not-the-ledger.md) | The relay carries pairing material, not the ledger, and "zero-knowledge" is retired | Accepted | Supersedes part of [0016](0016-noise-transport-zero-knowledge-relay.md) and [0004](0004-local-only-hosting.md) | > **Not here:** the spec's own governance rules. Those are not architectural — > no component's design depends on them. They live in @@ -117,7 +118,7 @@ Then the sync stack, which is the largest single body of design in the project: 8. [ADR-0015 — Multi-master peer-to-peer sync](0015-multi-master-p2p-sync.md) 9. [ADR-0014 — Op-log and CRDT merge engine](0014-op-log-crdt-merge-engine.md) -10. [ADR-0016 — Noise transport and zero-knowledge relay](0016-noise-transport-zero-knowledge-relay.md) +10. [ADR-0016 — Noise transport and the relay fallback](0016-noise-transport-zero-knowledge-relay.md) 11. [ADR-0018 — Amounts plaintext at rest](0018-amounts-plaintext-at-rest.md) Then the domain-specific calls and the org-level ones as you need them. diff --git a/00-overview/glossary.md b/00-overview/glossary.md index 3054f34..f0e2822 100644 --- a/00-overview/glossary.md +++ b/00-overview/glossary.md @@ -59,7 +59,7 @@ narrow Beatrax meaning, the narrow one is what the spec means. | **Safety number** | A short human-verifiable fingerprint derived from two devices' public identities, shown on both screens during pairing. | | **Word code** | The BIP39-wordlist typed fallback when a QR code cannot be scanned. | | **Noise XX / Noise IK** | The two handshake patterns used for the mutually-authenticated, forward-secret transport session. | -| **Zero-knowledge relay** | The store-and-forward fallback for offline peers. Holds opaque ciphertext blobs, performs no cryptography, and can address a mailbox but never read one. | +| **Pairing relay** | The optional hop two devices use to finish pairing and hand over key epochs when they cannot reach each other directly. No operation crosses it. It performs no cryptography on a blob and never looks inside one — but only half of what it holds would resist looking: an epoch wrap is sealed to its recipient, a pairing frame is not ([ADR-0033](decisions/0033-the-relay-carries-pairing-not-the-ledger.md)). | | **GDK** | Group Data Key. The per-user symmetric key that encrypts sensitive columns at rest. Wrapped per device; released by the app-lock. | | **Epoch** | One generation of the GDK. Rotation mints a new epoch; the keyring is append-only so older ciphertext stays readable. | | **Rekey / revocation** | Removing a device: revoke its trust, mint a fresh epoch, and re-wrap that epoch to every remaining confirmed device. | @@ -83,6 +83,7 @@ narrow Beatrax meaning, the narrow one is what the spec means. |-------|-------------|-----| | "open source" | "source-available" | The Hippocratic License 3.0 is not OSI-approved. Saying otherwise sets a false expectation. See [the rationale](../90-appendix/license-rationale.md). | | "cloud sync" | "peer-to-peer device sync" | There is no cloud that can read anything. | -| "server" (for the relay) | "relay" | It stores and forwards ciphertext; calling it a server implies it holds state it can use. | +| "server" (for the relay) | "relay" | It stores and forwards on a reader's behalf; calling it a server implies it holds state it can use. | +| "zero-knowledge relay", "zero-knowledge sync" | "pairing relay", "end-to-end encrypted sync" | Whatever a reader takes zero-knowledge to mean, a plaintext pairing frame is not it. The property that is true is narrower and checkable: no cryptography on a blob, and never looking inside one ([ADR-0033](decisions/0033-the-relay-carries-pairing-not-the-ledger.md)). | | "AI categorisation" | "rule and memory categorisation" | There is no model. The matchers are deterministic. | | "diederik" | "Beatrax" | The internal codename appears in some historical artefacts and command names. The product is Beatrax. | diff --git a/00-overview/roadmap.md b/00-overview/roadmap.md index d5c38e1..0d1f0bc 100644 --- a/00-overview/roadmap.md +++ b/00-overview/roadmap.md @@ -137,7 +137,7 @@ downstream committed. | Op-log / CRDT merge-layer spike validated against the live schema | — | [ADR-0014](decisions/0014-op-log-crdt-merge-engine.md) | | Change capture + CRDT merge engine: signed append-only op-log, HLC ordering, SQLite as a deterministic materialised view | [E1](../10-functional/features/e-sync/e1-change-capture.md) | [ADR-0014](decisions/0014-op-log-crdt-merge-engine.md), [ADR-0015](decisions/0015-multi-master-p2p-sync.md) | | Device identity + pairing: Ed25519 / X25519, QR and word-code, safety numbers | [E2](../10-functional/features/e-sync/e2-device-pairing.md) | [ADR-0015](decisions/0015-multi-master-p2p-sync.md) | -| Encrypted transport: Noise XX/IK, mDNS LAN-direct, zero-knowledge relay fallback | [E3](../10-functional/features/e-sync/e3-transport.md) | [ADR-0016](decisions/0016-noise-transport-zero-knowledge-relay.md) | +| Encrypted transport: Noise XX/IK, mDNS LAN-direct, a pairing relay for devices that cannot reach each other | [E3](../10-functional/features/e-sync/e3-transport.md) | [ADR-0016](decisions/0016-noise-transport-zero-knowledge-relay.md), [ADR-0033](decisions/0033-the-relay-carries-pairing-not-the-ledger.md) | | At-rest encryption per device, device revocation, group-key rotation and re-wrap | [E4](../10-functional/features/e-sync/e4-at-rest-encryption.md) | [ADR-0018](decisions/0018-amounts-plaintext-at-rest.md) | | Sync status and health surfaces | [E6](../10-functional/features/e-sync/e6-sync-status.md) | — | @@ -188,9 +188,10 @@ done" — was true of the phases and false of the requirements. ### 1 — Mobile client as a fully synced peer Ten of eleven plans are complete. The mobile client already holds its own -encrypted on-device copy, dials out over LAN with relay fallback, unlocks -biometrically, pairs camera-first with a word-code fallback, runs a blocking -resumable initial sync, and shows sync status. +encrypted on-device copy, dials out over the local network and reaches a relay +for pairing and key material, unlocks biometrically, pairs camera-first with a +word-code fallback, runs a blocking resumable initial sync, and shows sync +status. **Outstanding:** the final plan — full surface-parity smoke test, the as-is invariant, and real-device UAT on iPhone and Android. diff --git a/00-overview/vision.md b/00-overview/vision.md index b850188..b4ed3a8 100644 --- a/00-overview/vision.md +++ b/00-overview/vision.md @@ -64,13 +64,20 @@ Full rationale: [ADR-0004](decisions/0004-local-only-hosting.md). Multi-device sync is the one place where the local-only promise is under genuine pressure, and it is where most products quietly give up. Beatrax does -not. Devices sync peer-to-peer over the LAN; when a peer is offline the fallback -is a store-and-forward relay that only ever holds ciphertext it cannot decrypt. - -"Encrypted at rest on our servers" is not the bar. The bar is that the relay -operator — including the maintainer — learns nothing but message sizes and -timing. See [ADR-0016](decisions/0016-noise-transport-zero-knowledge-relay.md) -and [E3](../10-functional/features/e-sync/e3-transport.md). +not. Devices sync peer-to-peer over the local network, end-to-end encrypted, and +that is the only road the ledger travels. When a peer is asleep its changes wait +on the device that made them; nothing else is holding a copy on their behalf. + +"Encrypted at rest on our servers" is not the bar, and neither is a promise +about what an intermediary chooses not to read. The bar is that no operation +reaches an intermediary at all. A relay is optional, nominated by the reader, +and has one job: letting two devices finish pairing and hand over key epochs +when they cannot reach each other. What it holds is worth stating in full — +sealed key handovers it cannot open, pairing frames it can read, and the sizes, +timing and device identifiers of both. See +[ADR-0033](decisions/0033-the-relay-carries-pairing-not-the-ledger.md), +[ADR-0016](decisions/0016-noise-transport-zero-knowledge-relay.md) and +[E3](../10-functional/features/e-sync/e3-transport.md). ### P3 — Imports are idempotent, history is permanent @@ -144,7 +151,7 @@ the reasoning, not an issue. | Non-goal | Why | |----------|-----| -| **Cloud sync that can read the data** | Contradicts P1 and P2 outright. Sync is end-to-end encrypted and zero-knowledge or it does not ship. | +| **Cloud sync that can read the data** | Contradicts P1 and P2 outright. Sync is end-to-end encrypted, device to device, or it does not ship — and no intermediary carries an operation at all. | | **Telemetry, even opt-in** | The presence of the SDK is the leak. See [ADR-0004](decisions/0004-local-only-hosting.md). | | **A remote error reporter** | Stack traces carry local variable contents — balances, merchant names, IBAN fragments. Scrubbing leaves too much residual risk. | | **Acting on the user's behalf** | Auto-cancel, auto-switch, payment initiation. Violates P7. | diff --git a/10-functional/features/e-sync/e2-device-pairing.md b/10-functional/features/e-sync/e2-device-pairing.md index 349a56c..39bf2d7 100644 --- a/10-functional/features/e-sync/e2-device-pairing.md +++ b/10-functional/features/e-sync/e2-device-pairing.md @@ -154,9 +154,13 @@ re-wraps that epoch to every remaining confirmed device — see ### Pairing across a relay Where the two devices cannot see each other directly, the pre-confirmation -handshake propagates over the zero-knowledge relay ([E3](e3-transport.md)) so -two devices with separate databases can still complete the ceremony. The relay -carries the frames; it cannot read them. +handshake propagates over a relay ([E3](e3-transport.md)) so two devices with +separate databases can still complete the ceremony. The relay carries the +frames, and it can read them: they are not sealed, and they expose both device +identifiers, the responder's public keys and the name its device goes by. What +they do not give it is anything to act on — a swapped sealing key fails the +safety number both readers compare, and the confirming frame's signature commits +to both sealing keys. ### Where the guarantee ends diff --git a/10-functional/features/e-sync/e3-transport.md b/10-functional/features/e-sync/e3-transport.md index 82e2dbd..0210dc5 100644 --- a/10-functional/features/e-sync/e3-transport.md +++ b/10-functional/features/e-sync/e3-transport.md @@ -44,14 +44,30 @@ delivers. identifier. 2. **A manually entered host and port**, for networks where discovery is blocked — guest networks and client-isolated wireless are common. -3. **The relay.** - -### The relay knows nothing - -The relay stores and forwards opaque blobs addressed to a recipient device -identifier. It performs **no cryptography at all** — the invariant is that its -code path contains no cryptographic call, and that is asserted by test rather -than described in a comment. +3. **A relay**, for pairing frames and key material only — never for an + operation. + +### A relay carries pairing, not the ledger + +A relay is optional, off until the reader nominates one, and exists for a single +job: letting two devices finish the pairing ceremony ([E2](e2-device-pairing.md)) +and hand over group-key epochs ([E4](e4-at-rest-encryption.md)) when they cannot +reach each other directly. + +**No operation crosses a relay.** Op-log entries travel only inside a +mutually-authenticated session between two paired devices. Where neither device +can reach the other, the changes wait on the device that made them until both +are awake on the same network. There is no second road, and the product's own +copy says so rather than leaving a reader to assume there is +([ADR-0033](../../../00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md)). + +It stores and forwards blobs addressed to a recipient device identifier, and +performs **no cryptographic operation on a blob and never looks inside one** — +no libsodium call and no decode of a blob's contents anywhere in its code path, +asserted by test rather than described in a comment. The digest and +constant-time comparison that authorise a drain touch the credential, never the +payload +([ADR-0031](../../../00-overview/decisions/0031-a-signed-wrap-is-independent-of-its-channel.md)). - Draining a mailbox requires a credential derived **per device**. A credential scoped to one device cannot drain or delete another's mailbox. A single @@ -72,11 +88,27 @@ than described in a comment. configures one, and its address is a setting rather than a constant. Its credential is stored with owner-only file permissions. -### What the relay can still see +### What a relay can read + +Not looking is a discipline, not a guarantee — and only half of what a relay +holds would resist looking. + +| What it holds | Opaque to it? | +|---------------|---------------| +| A group-key epoch wrap | The key is sealed to the receiving device and cannot be opened. Its envelope is not: it names both device identifiers and the epoch, and carries the sender's signature. | +| A pairing frame | No. It is JSON as written — the pairing token's hash, the responder's Ed25519 and X25519 public keys, and the name its device goes by. The accepting frame names the responder and is **not** signed; the confirming frame names both devices and is. | +| Every row's addressing | Both device identifiers, whatever the frame inside says. | +| Sizes and timing | Observable on all of it. Traffic analysis is **not** defended against, and that is documented rather than implied. | + +None of this is a defect. A pairing frame is public-key material exchanged +before any shared secret exists, and the safety number both readers compare is +what makes a swapped key fail. Describing it as ciphertext would be +([ADR-0033](../../../00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md)). -Message sizes, timing, and which device identifiers exchange traffic. Traffic -analysis is **not** defended against, and that is documented rather than -implied. +The device name is the platform's own — a neutral label such as "Mac" or "PC" on +a desktop, and on a phone the name the operating system reports, which is often +one a person chose and can carry their name. It is deliberately never the +machine's hostname. ### Key material over the transport @@ -115,7 +147,8 @@ client never listens — it dials out only ([E5](e5-mobile-peer.md)). | A blob nobody collects | Expires after the undelivered window. | | A forged or relay-delivered epoch wrap | Adopted only if its signature verifies against the sender's confirmed device key; otherwise refused, logged, not thrown. | | A failed unseal | Strictly checked and rejected; logged, not thrown. | -| No relay configured | Sync works on the local network only. | +| No relay configured | Sync works on the local network only, and two devices that cannot see each other cannot complete pairing. | +| A peer that cannot be reached at all | The changes wait on the device that made them. Nothing is handed to a relay to hold on the peer's behalf. | | The app locked when key material arrives | Logged and returned; never thrown. | ## Acceptance criteria @@ -145,10 +178,11 @@ client never listens — it dials out only ([E5](e5-mobile-peer.md)). | **E3-R21** | The relay's deliver endpoint MUST rate-limit per source and reject a burst, so no participant can flood a mailbox. | | **E3-R22** | A drain credential MUST be bound to the device identifier it drains in a way the relay can verify without prior state, so a credential that is not about that identifier is refused even for a mailbox that has never been drained. | | **E3-R23** | A drain credential MUST NOT be shared between the local users of one install. | +| **E3-R24** | An op-log entry MUST NOT be delivered over a relay; a relay MUST carry only pairing frames and sealed key-epoch wraps. What a relay holds MUST be documented in both halves — the wraps, sealed to the receiving device, and the pairing frames, which are not sealed and expose the device identifiers, the public keys and the device name they carry. | ## Related -- [ADR-0016](../../../00-overview/decisions/0016-noise-transport-zero-knowledge-relay.md) +- [ADR-0016](../../../00-overview/decisions/0016-noise-transport-zero-knowledge-relay.md) · [ADR-0031](../../../00-overview/decisions/0031-a-signed-wrap-is-independent-of-its-channel.md) · [ADR-0033](../../../00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md) - [E1 Change capture](e1-change-capture.md) — what travels - [E2 Device pairing](e2-device-pairing.md) — the identities - [E4 At-rest encryption](e4-at-rest-encryption.md) — the key material diff --git a/10-functional/features/e-sync/e5-mobile-peer.md b/10-functional/features/e-sync/e5-mobile-peer.md index 2b7c413..66ae686 100644 --- a/10-functional/features/e-sync/e5-mobile-peer.md +++ b/10-functional/features/e-sync/e5-mobile-peer.md @@ -101,9 +101,10 @@ arrive, not a gate nobody can pass. ### Ongoing sync -A sync attempt tries the local network first with one bounded retry, then falls -back to the relay. By default it syncs on any network; the user can opt into -pausing on mobile data. +A sync attempt tries the local network first with one bounded retry, then +reaches a relay if one is configured — for key material and pairing frames, not +for operations, which cross only the direct session ([E3](e3-transport.md)). By +default it syncs on any network; the user can opt into pausing on mobile data. A background pull runs on the platform's own scheduler, fanning out over the users on the device, and **skips cleanly when no identity is unlocked** rather diff --git a/10-functional/features/g-ux/g1-privacy.md b/10-functional/features/g-ux/g1-privacy.md index 43791eb..d955152 100644 --- a/10-functional/features/g-ux/g1-privacy.md +++ b/10-functional/features/g-ux/g1-privacy.md @@ -26,7 +26,7 @@ and if the enumeration is maintained as a requirement rather than as marketing. | **Exchange-rate fetch** | Off | Refreshes rates; a bundled snapshot works offline ([B10](../b-ledger/b10-multi-currency.md)). | Yes — it is off until enabled | | **Open-banking aggregator** | Off | Fetches booked transactions from the user's own aggregator account ([A6](../a-ingestion/a6-open-banking.md)). | Yes — it is off until enabled | | **Sync peers** | Off | Peer-to-peer exchange with the user's own devices ([E3](../e-sync/e3-transport.md)). | Yes — no peers until paired | -| **Sync relay** | Off | Ciphertext-only store-and-forward, to a relay the user configures. | Yes — none until configured | +| **Sync relay** | Off | Pairing frames and sealed key epochs, to a relay the user configures. No operation crosses it ([E3](../e-sync/e3-transport.md)). | Yes — none until configured | | **External-link opening** | On demand | Opens a link the user clicked, gated by an allow-list ([C9](../c-insight/c9-community-corpus.md)). | Only by not clicking | **With every optional feature off, the only outbound call is the update check — @@ -68,8 +68,13 @@ Three things must be stated plainly in the product's own copy, not only here: ([ADR-0018](../../../00-overview/decisions/0018-amounts-plaintext-at-rest.md)). An attacker with the file but not the key sees a complete dated amount distribution and a plaintext shadow of descriptions. -2. **The relay sees metadata.** Sizes, timing, and which device identifiers - exchange traffic. Traffic analysis is not defended against. +2. **A relay reads the pairing frames, and sees metadata on everything else.** + No transaction crosses a relay at all. The key epochs that do are sealed to + the receiving device and it cannot open them. The pairing frames are not + sealed: they carry both device identifiers, the devices' public keys and the + name a device goes by. On all of it, it sees sizes, timing and which device + identifiers exchange traffic; traffic analysis is not defended against + ([ADR-0033](../../../00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md)). 3. **A paired device is trusted.** Revocation rotates the key going forward; it does not un-see what was already synced ([ADR-0015](../../../00-overview/decisions/0015-multi-master-p2p-sync.md)). @@ -118,10 +123,11 @@ Data locations, export, and deletion are first-class and documented in | **G1-R17** | The release gate MUST check that no forbidden dependency has been introduced, including transitively. | | **G1-R18** | The data a user's machine sends to a third party MUST be limited to what the enabled feature's own protocol requires. | | **G1-R19** | Data locations, export, and deletion MUST be discoverable from within the application. | +| **G1-R22** | The product's own copy MUST state that no transaction crosses a relay, and MUST describe what a relay holds in both halves — the sealed key epochs and the unsealed pairing frames — rather than only the sealed one. | ## Related -- [ADR-0004](../../../00-overview/decisions/0004-local-only-hosting.md) · [ADR-0015](../../../00-overview/decisions/0015-multi-master-p2p-sync.md) · [ADR-0018](../../../00-overview/decisions/0018-amounts-plaintext-at-rest.md) · [ADR-0020](../../../00-overview/decisions/0020-open-banking-byo-key-ais-only.md) +- [ADR-0004](../../../00-overview/decisions/0004-local-only-hosting.md) · [ADR-0015](../../../00-overview/decisions/0015-multi-master-p2p-sync.md) · [ADR-0018](../../../00-overview/decisions/0018-amounts-plaintext-at-rest.md) · [ADR-0020](../../../00-overview/decisions/0020-open-banking-byo-key-ais-only.md) · [ADR-0033](../../../00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md) - [F7 Data locations, export and deletion](../f-platform/f7-data-locations.md) - [40-quality/security.md](../../../40-quality/security.md) - [90-appendix/data-retention.md](../../../90-appendix/data-retention.md) diff --git a/10-functional/features/g-ux/g5-plain-language.md b/10-functional/features/g-ux/g5-plain-language.md index 23dbb34..bcad89d 100644 --- a/10-functional/features/g-ux/g5-plain-language.md +++ b/10-functional/features/g-ux/g5-plain-language.md @@ -39,7 +39,9 @@ Where the honest answer is uncomfortable, the copy says the uncomfortable thing: - At-rest encryption does **not** encrypt everything; amounts and the search index are readable ([ADR-0018](../../../00-overview/decisions/0018-amounts-plaintext-at-rest.md)). -- A relay sees **metadata** even though it cannot read content. +- A relay **reads the pairing frames** it forwards — device identifiers, public + keys, a device name — and sees sizes and timing on everything. It cannot open + the sealed key handovers, and **no transaction crosses one at all**. - A paired device is **trusted**, and removing it later does not un-share what it already saw. - Uninstalling does **not** delete your data. diff --git a/10-functional/journeys/j2-daily-use.md b/10-functional/journeys/j2-daily-use.md index f4ec0e5..cb3f392 100644 --- a/10-functional/journeys/j2-daily-use.md +++ b/10-functional/journeys/j2-daily-use.md @@ -108,8 +108,8 @@ shows the same data as a curve with confidence bands and the dips marked. ### On the phone, later The same surfaces at phone width, from the installed application, holding their -own encrypted copy and syncing peer-to-peer over the local network — or through -a relay that cannot read anything if the desktop is asleep. +own encrypted copy and syncing peer-to-peer over the local network. If the +desktop is asleep, the phone's changes wait on the phone until it is awake. *Exercises: [G4](../features/g-ux/g4-pwa.md), [E5](../features/e-sync/e5-mobile-peer.md), [E3](../features/e-sync/e3-transport.md), [E6](../features/e-sync/e6-sync-status.md).* diff --git a/10-functional/journeys/j5-adding-a-device.md b/10-functional/journeys/j5-adding-a-device.md index 37c2a95..e29204d 100644 --- a/10-functional/journeys/j5-adding-a-device.md +++ b/10-functional/journeys/j5-adding-a-device.md @@ -44,7 +44,8 @@ The new device scans the code — camera-first on a phone — or the user types word code. Where the two devices cannot see each other directly, the handshake propagates -over the relay, which carries the frames without being able to read them. +over a relay. It carries the frames and can read them — they are not sealed — +which is why the safety number in the next step is what the ceremony rests on. *Exercises: [E2](../features/e-sync/e2-device-pairing.md), [E3](../features/e-sync/e3-transport.md), [E5](../features/e-sync/e5-mobile-peer.md).* @@ -86,9 +87,10 @@ and the operation log was re-projected. ### 7. Ongoing -Both devices sync directly over the local network when both are awake, and -through the relay when one is not. The phone dials out and never listens; that -is a platform constraint, not a demotion — in the merge it is an equal peer. +Both devices sync directly over the local network when both are awake. When one +is not, its changes wait on it until they are: no operation travels any other +road. The phone dials out and never listens; that is a platform constraint, not +a demotion — in the merge it is an equal peer. Concurrent edits to different fields of one row both survive. Concurrent edits to the same field resolve by clock. Deletes win over concurrent edits. @@ -125,7 +127,8 @@ protects going forward. The copy says so. | Private key material leaves a device | The whole model collapses. | | Initial sync can be dismissed | The user acts on a half-synced ledger believing it complete. | | Sync silently does nothing | Weeks of divergence discovered by accident. Worse than a visible failure. | -| The relay can read a blob | The zero-knowledge claim is false and the product's promise with it. | +| A relay is described as holding only ciphertext | Half of what it holds is not, and a reader nominating one decides on a false premise ([ADR-0033](../../00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md)). | +| An operation is delivered over a relay | The ledger would travel a road nobody verified, and every page saying it does not would become false. | | Concurrent edits to different fields lose one | Users learn not to trust two devices. | | Revocation is described as retroactive | The user makes a security decision on a false premise. | | A phone joining mints its own key epoch | Divergent keyrings; content one device cannot read. | diff --git a/20-architecture/data-flow.md b/20-architecture/data-flow.md index dc0312a..4ae4ecd 100644 --- a/20-architecture/data-flow.md +++ b/20-architecture/data-flow.md @@ -143,7 +143,7 @@ against. capture listener ──▶ op-log entry, signed, clock-ordered │ │ │ ▼ - │ transport ── LAN-direct, or relay + │ transport ── the LAN-direct session only │ │ │ ▼ ▼ peer device @@ -160,6 +160,10 @@ against. └─ applied ──▶ peer's DB + search index ``` +An operation crosses only the direct session between two paired devices. A +relay carries pairing frames and sealed key epochs, never an op-log entry +([ADR-0033](../00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md)). + The database is a materialised view: replaying the merged log from scratch reproduces it ([ARCH-R11](README.md#the-arch-r-namespace)). diff --git a/20-architecture/system-context.md b/20-architecture/system-context.md index 90c5ff0..2c1039e 100644 --- a/20-architecture/system-context.md +++ b/20-architecture/system-context.md @@ -52,7 +52,7 @@ requirements ([ARCH-R15](README.md#the-arch-r-namespace)). |-------|------| | **The household** | One or two people. There is no operator/user split — the person who installs it is the person who uses it. The only distinction is owner versus partner, and it exists only for account administration ([F3](../10-functional/features/f-platform/f3-auth-and-app-lock.md)). | | **A paired device** | Another copy, equal in the merge, trusted because the user paired it ([E2](../10-functional/features/e-sync/e2-device-pairing.md)). | -| **A relay** | Optional, user-configured, ciphertext-only. It is not part of the system in any meaningful sense — it moves opaque bytes ([E3](../10-functional/features/e-sync/e3-transport.md)). | +| **A relay** | Optional, user-configured, and used for pairing frames and sealed key epochs only — no operation crosses it. It can read the pairing frames it forwards; it cannot open the key epochs ([E3](../10-functional/features/e-sync/e3-transport.md), [ADR-0033](../00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md)). | There is no maintainer actor at runtime. Nothing the maintainer operates is in any data path. diff --git a/30-repos/website.md b/30-repos/website.md index 6131118..ee101dd 100644 --- a/30-repos/website.md +++ b/30-repos/website.md @@ -22,7 +22,7 @@ only surface where overstating things is tempting. |----------|---------| | Source-available, **not** open source | The licence is not OSI-approved, and the distinction has real consequences ([90-appendix/license-rationale.md](../90-appendix/license-rationale.md)) | | Local-first, with the outbound surface named | The claim is checkable and should be presented as checkable ([G1](../10-functional/features/g-ux/g1-privacy.md)) | -| That sync is peer-to-peer and end-to-end encrypted, and what the relay can see | Overstating it would be the worst kind of dishonesty for this product | +| That sync is peer-to-peer and end-to-end encrypted, that no transaction crosses a relay, and what a relay can read of what does | Overstating it would be the worst kind of dishonesty for this product | | Which installers carry a signing identity and which do not | Only Linux ships unsigned now ([20-architecture/platform-matrix.md](../20-architecture/platform-matrix.md)); a site still saying all of them are unsigned tells a macOS reader something false about the thing they are about to download | | Which platforms are supported, including the Intel exception | [20-architecture/platform-matrix.md](../20-architecture/platform-matrix.md) | diff --git a/40-quality/security.md b/40-quality/security.md index 13c491f..a96b59f 100644 --- a/40-quality/security.md +++ b/40-quality/security.md @@ -17,7 +17,7 @@ that follow. | **A lost or stolen device** | The app-lock, plus revocation that rotates the group key ([E2](../10-functional/features/e-sync/e2-device-pairing.md)) | | **An observer on the network** | Mutually authenticated, forward-secret sessions ([E3](../10-functional/features/e-sync/e3-transport.md)) | | **A machine-in-the-middle during pairing** | The mandatory safety-number confirmation on both screens | -| **A hostile relay operator** | The relay holds ciphertext and performs no cryptography, asserted by test | +| **A hostile relay operator** | No operation crosses a relay, so running one grants no access to the ledger. The key epochs it does carry are sealed to the receiving device, and it cannot put itself between two devices — the safety number both readers compare covers the sealing keys. It can read the pairing frames it forwards, which is stated rather than implied ([ADR-0033](../00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md)) | | **A hostile input file** | Typed parse failures, disabled external-entity resolution, size and line caps, archive-bomb and traversal guards, and pattern-length caps on user-supplied match expressions ([C9-R23](../10-functional/features/c-insight/c9-community-corpus.md#acceptance-criteria)) | | **A hostile aggregator response** | Host allow-list before credentials, HTTPS only, no redirects, private-address rejection | | **A tampered update** | Signature verification before anything is read from a manifest, hash verification before any install | @@ -38,7 +38,7 @@ that follow. | **A compromised operating system** | A user-space application cannot defend against it. | | **A maliciously-paired device** | A paired device legitimately holds the group key. The safety-number confirmation is the defence, and it is the user's to perform. | | **A household member escalating to operator access** | Every user added to an instance is a co-equal, fully-trusted operator: any of them may enable developer mode (the SQL console and all-user visibility) and back up or restore the whole database. Partner accounts are a convenience, not a privilege boundary. Ordinary per-user data scoping ([ARCH-R6](../20-architecture/README.md#the-arch-r-namespace)) still applies to routine reads and writes. | -| **Traffic analysis against a relay** | Sizes, timing, and recipient identifiers are observable. Documented, not defended. | +| **Traffic analysis against a relay** | Sizes, timing and both device identifiers are observable, as is the content of a pairing frame. Documented, not defended. | | **An attacker with the database file, the time, and the motivation** | The plaintext set is too informative. See below. | ## The honest statement about at-rest encryption @@ -125,6 +125,6 @@ Recorded rather than described as solved: ## Related -- [ADR-0015](../00-overview/decisions/0015-multi-master-p2p-sync.md) · [ADR-0016](../00-overview/decisions/0016-noise-transport-zero-knowledge-relay.md) · [ADR-0018](../00-overview/decisions/0018-amounts-plaintext-at-rest.md) · [ADR-0020](../00-overview/decisions/0020-open-banking-byo-key-ais-only.md) +- [ADR-0015](../00-overview/decisions/0015-multi-master-p2p-sync.md) · [ADR-0016](../00-overview/decisions/0016-noise-transport-zero-knowledge-relay.md) · [ADR-0018](../00-overview/decisions/0018-amounts-plaintext-at-rest.md) · [ADR-0020](../00-overview/decisions/0020-open-banking-byo-key-ais-only.md) · [ADR-0033](../00-overview/decisions/0033-the-relay-carries-pairing-not-the-ledger.md) - [G1 Privacy stance](../10-functional/features/g-ux/g1-privacy.md) - [ci-cd.md](ci-cd.md) · [code-standards.md](code-standards.md) diff --git a/60-brand/surface-mapping.md b/60-brand/surface-mapping.md index 1d652b8..ced2686 100644 --- a/60-brand/surface-mapping.md +++ b/60-brand/surface-mapping.md @@ -42,7 +42,8 @@ Its obligations are requirements rather than guidance - Source-available, **not** open source. - Local-first, with the outbound surface **named** rather than merely claimed. -- Sync described accurately, including what a relay can observe. +- Sync described accurately, including that no transaction crosses a relay and + what one can read of what does. - Which installers are signed and which are not, accurately in both directions. - No analytics, no tracking, no third-party embeds. diff --git a/90-appendix/data-retention.md b/90-appendix/data-retention.md index 1a98b34..8341fe0 100644 --- a/90-appendix/data-retention.md +++ b/90-appendix/data-retention.md @@ -111,7 +111,7 @@ outbound calls the user enabled, enumerated in | Open banking | The user's own aggregator account, machine-to-aggregator. | | Exchange rates | A rate source. No user data is sent — only a request for rates. | | Update check | The release host. No user-identifying data beyond what any request carries. | -| Sync | The user's own devices, end-to-end encrypted. A relay, if configured, holds ciphertext it cannot read. | +| Sync | The user's own devices, end-to-end encrypted. A relay, if configured, carries pairing frames and sealed key epochs — never a transaction. It can read the frames; it cannot open the epochs. | With all of them off, **the application makes no outbound call**. diff --git a/90-appendix/faq.md b/90-appendix/faq.md index eb23b3f..6f8943e 100644 --- a/90-appendix/faq.md +++ b/90-appendix/faq.md @@ -84,15 +84,19 @@ be mapped is reported rather than dropped silently. ### How does sync work without a server? Your devices talk directly to each other. Each holds a full encrypted copy and -they reconcile through a signed log of changes. When both are on the same -network they sync directly; when one is asleep, changes wait in a relay that -only ever holds encrypted data it cannot read. +they reconcile through a signed log of changes. That happens only when both are +on the same network; when one is asleep, the changes wait on the device that +made them until the two meet again. A relay is optional and exists only to +finish pairing and exchange keys — a transaction never passes through one. ### Can you read my data when it syncs? No. Sync is end-to-end encrypted between devices you have paired and verified -yourself. The relay holds ciphertext with no key, and there is no Beatrax -account or server that ever sees your ledger. +yourself, and there is no Beatrax account or server that ever sees your ledger. +Your transactions never pass through a relay either. What does pass through one +is the key handover at pairing, sealed to the receiving device, and the pairing +messages themselves, which are not encrypted: two device identifiers, the +devices' public keys, and the name a device goes by. ### Can two people share a ledger?