The technical version: what is encrypted and with what, what every observer can and cannot do, how to check each claim without trusting us — and the attack vectors we designed against, each with its honest residual. This page is the complete trust thesis; there is no separate write-up to chase down.
No invented cryptography. Standard primitives, named open-source implementations, one fixed suite, two independent verifiers, and a review process built to break it before you rely on it.
Your machine and your keystore are trusted; our control plane and the storage bucket are untrusted for confidentiality — and every property below is stated against a malicious host and a malicious bucket, not a well-behaved one. Confidentiality survives that adversary structurally: there is no key material server-side. In the vault lane, source payload and repository-history content are ciphertext-only; the substrate retains only enumerated plaintext metadata and holds zero vault keys.
KyGit makes three promises with three different strengths, and refuses to blur them — conflating a cryptographic incapacity with an operational promise is exactly the overclaim this page exists to prevent:
git pushA vault is an append-only chain of signed, admitted heads over encrypted pack sets. Everything cryptographic happens on your machine, before any byte leaves it:
K_repo, from the operating system's CSPRNG. It is never
transmitted. Your keystore (~/.config/run402/gitvault/) also holds an Ed25519
signing keypair (the writer key) and an X25519 encryption keypair (your
recipient key). We register only the public halves.
k_obj = HKDF-SHA-256(K_repo, info), where info is a length-prefixed,
domain-separated encoding of the protocol label (r402s/v0), the suite, the vault
id, the key epoch, the object kind, and the object id. Object ids are single-use key labels:
a retry re-reads and compares, it never re-encrypts under the same id — so the "same nonce,
same key, different plaintext" failure class is excluded by construction, twice over.
"R402S0" ‖ suite byte ‖ nonce ‖ ciphertext‖tag, and the
additional authenticated data binds the vault id, object kind, object id, epoch, and suite —
the frame header itself is authenticated, so a ciphertext cannot be replayed as a different
object, into a different vault, or under a different epoch without failing its tag.
K_repo is
wrapped to each authorized recipient's X25519 key with HPKE (RFC 9180, base mode:
DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20-Poly1305). The HPKE info and
AAD bind the protocol label, the suite, the recipient's key fingerprint, the sealing writer
key, the vault id, and the epoch — an envelope for one vault, epoch, or recipient is not
openable as any other. An envelope's signature is verified before any open is
attempted.
push reports that
anything landed, the client compares every finalization receipt against its local expected
manifest and reads the admitted head back from storage. A 200 alone is never
proof — a client that trusts the status code has handed the host a freshness oracle it was not
supposed to have.
One consequence worth noticing: nothing the server stores or compares is derived from your plaintext un-keyed. Where the platform needs to compare (for example, that two captures commit to the same snapshot), it compares keyed HMAC digests — it can check equality without learning what is equal.
There is exactly one suite, r402s-1, and every primitive in it is a published
standard with decades-old or heavily-reviewed lineage. We did not invent a primitive, a mode, or
a construction; where a composed construction was needed (key wrapping), we adopted RFC 9180
HPKE as a named implementation rather than assembling it from parts — a rule enforced
by CI gates in both codebases, not by good intentions.
| Purpose | Primitive | Standard | TypeScript client | Rust verifier |
|---|---|---|---|---|
| Signatures | Ed25519, strict verification | RFC 8032 | @noble/curves (zip215: false pinned) |
ed25519-dalek verify_strict |
| Key wrapping | HPKE base mode: DHKEM(X25519, HKDF-SHA256) + HKDF-SHA256 + ChaCha20-Poly1305 | RFC 9180 | @hpke/core |
rust-hpke, version-and-checksum pinned |
| Bulk encryption | XChaCha20-Poly1305 (24-byte nonce) | IETF draft, ChaCha20-Poly1305 family (RFC 8439) | @noble/ciphers (Node-native backend where available) |
RustCrypto chacha20poly1305 |
| Key derivation | HKDF-SHA-256 | RFC 5869 | @noble/hashes |
RustCrypto hkdf |
| Hash / keyed digests | SHA-256 / HMAC-SHA-256 | FIPS 180-4 / RFC 2104 | @noble/hashes |
RustCrypto |
| Canonical serialization | JSON Canonicalization Scheme, strings-only strict profile | RFC 8785 | json-canonicalize + a strict parser |
a hand-written strict I-JSON front-end + JCS |
Everything above is open source, and it is implemented twice, independently:
@run402/sdk, on the noble/WebCrypto/@hpke/core
stack. This is what encrypts, signs, pushes, and verifies day to day.rust-hpke +
ed25519-dalek), and it never imports the SDK. It re-checks a vault's head chain
from a raw bucket export against your own pin, replays the protocol's test vectors, and runs a
differential git validation that independently re-hashes every object rather than trusting
git's index. It builds reproducibly against a committed lockfile, and released binaries ship
with checksums after passing the vector replay on the build machine.The non-sharing is the point: a differential verifier that reuses the code it is checking verifies nothing. And the second implementation is not decorative — it is load-bearing in the protocol's most dangerous ceremony: Every prune requires receipts from two independently built open-source implementations — one in TypeScript, one in Rust. Nothing is deleted on one codebase's opinion.
It is also what makes the exit ramp checkable by someone who trusts neither our servers nor our client: If we disappeared tomorrow, your repository is recoverable from a bucket listing and your keys, with an open-source tool. No server required. Provable, not promised. We proved it against production — a real vault, mirrored, then recovered ref-exact with the gateway deliberately unreachable.
The honest shared assumption: both implementations implement the same specification, so a specification-level flaw would fool both. That is why the specification itself — not just the code — was the primary attack target of the review process below, and why the test vectors are generated and re-checked by a third, stdlib-Python reference implementation that shares code with neither.
The protocol ships a frozen, deterministic vector set —
vendored in the
public repository — covering roughly three hundred vectors across two dozen classes:
strict-parse accepts and rejects, per-field AAD rejection (flip any bound field and the frame
must fail), chain continuity and rollback regression, HKDF and preimage derivations,
HPKE interop golden files sealed by one implementation and opened by the other in both
directions, tamper triples that must fail byte-for-byte, non-canonical-signature rejection, and
state-machine edge tables. Deliberately, no prose hand-copies the total: a generated
COUNTS.json carries it, and the verifier asserts prose-free counts against the
vector file itself.
Before the wire format froze, we did to this design what we expect attackers to do — on the record, round after round, until the attacks stopped landing on the cryptography.
The protocol was drafted and defended by one frontier model and attacked by another, from a rival lab, with no stake in being agreeable: Anthropic's Fable 5 on the design side, OpenAI's GPT‑5.6 Pro as the standing hostile reviewer — rival labs' frontier reasoning models, pointed at each other. The reviewer's charter is blunt: the client trusts nothing the server says beyond what a signature or an AEAD tag forces it to, and the reviewer's job is to break that — chain-integrity and rollback attacks, AEAD and nonce discipline, HPKE misbinding and cross-context replay, key-derivation domain separation, signature forgery and malleability, metadata leaks beyond the disclosed set, downgrade paths, the two-implementation claim itself, and the recovery story.
Forty-nine recorded review rounds came out of that charter, and the record is not a parade of passes — it reads the way real security work reads:
The process did not stop at the freeze. Every capability added since — multi-recipient envelopes, epoch rotation, recovery-code custody — went through its own adversarial rounds under the same rules before shipping, and the findings above that named honest limits (rotation is forward-only; recipient authenticity is operator-confirmed, not host-blind) are printed on this page and in the threat model rather than smoothed over.
Model-vs-model review is a complement to open source, not a substitute for human eyes — which is exactly why the protocol, the threat model, the schemas, the vectors, and both implementations are public. Everything the reviewer attacked, you can attack.
Content is hidden; structure is not. This is not zero-knowledge hosting, and the honest way to say so is to enumerate the boundary. The full observer-by-observer matrix lives in the threat model; the shape of it:
| Observer | Sees | Cannot |
|---|---|---|
| Run402 (control plane) | That a vault exists and whose it is; principal identities and access times; object counts, ciphertext sizes and ids; generations and cadence; admission times; policy state; keyed digests. A closed, enumerated list — anything beyond it is a protocol violation, not an implementation detail. | Decrypt anything. See branch names, commit messages, file paths, or commit ids. Forge a head (no writer key). Produce an un-keyed digest of your plaintext, because none exists. |
| Bucket operator (AWS) | The same objects minus control-plane context, plus access patterns. | Decrypt; forge. It can delete or withhold bytes — the availability rows above. |
| Network | Endpoints, sizes, timing. TLS covers the rest. | Read object ids or content. |
| Your agent's deploy delegate | Heads, ciphertext, receipts for what it uploads — enough to push. | Prune, compact, repair, change policy, or override activation. Those are owner powers, and a delegate is structurally never an owner: nothing the agent holds can destroy the history it wrote. |
| An org member without an envelope | Ciphertext and control-plane state; can administer the vault. | Read a byte of content. No envelope is addressed to them. |
| A removed recipient | Nothing new — their credentials stop working. | Be made to forget. What they already fetched or opened stays decryptable to them; only an epoch rotation closes the forward direction. See the FAQ. |
A product whose confidentiality rests on the platform holding zero vault keys has, by construction, no platform-side rescue for lost keys. So the durability sentence carries its qualifier in the sentence itself:
The vault protects source history from host-side loss while a principal keystore survives.
Every vault starts with exactly one keystore that can open it, and the client says so to your
face — run402 repos view prints, verbatim: "whole-machine or whole-keystore
loss is terminal for vault history until human envelopes ship". It does not have to stay
that way:
run402 repos access shows who is covered
and who is still pending.run402 repos create
allocates the vault immediately and prints the recovery receipt once: a signed
fingerprint of the vault's genesis that later proves the vault you are served is the one you
created. It decrypts nothing. Print it, mail it to yourself, put it in the README — the more
copies the better."Why is this secure?" decomposes into specific attacks. Here is each one we designed against — what stops it, and, where a residual remains, the residual stated instead of hidden. The questions are grouped by what the attacker is trying to do.
Because there is nothing on our side that can. Encryption happens on your machine before upload;
K_repo and every derived object key exist only in your keystore and in HPKE
envelopes sealed to authorized recipients' keys; the substrate stores ciphertext frames plus the
enumerated metadata and holds zero vault keys. This is a capability gap, not a policy: there is
no configuration of our servers, no admin tool, and no insider with a database password that
changes it. The claim is deliberately scoped — we can read what you explicitly deploy
through run402's separate deploy path (below) — and that narrowness is
what makes the vault claim checkable rather than rhetorical.
Not from the protocol: no message in r402s/v0 ever carries key material to us, so
there is nothing for a compromised server to collect. A compromised server gets what
the observer table says and no more. The real vector is the client — shipping you a modified
build that exfiltrates keys — and we state it rather than pretend it away: the client is open
source, its releases are pinned and inspectable, a second independent implementation exists to
check its outputs, and a reader who audits or pins their client keeps the machine-side trust
assumption in their own hands. That residual is common to every end-to-end-encrypted product
ever shipped; the difference is whether the vendor says it out loud.
State the worst case first: Run402 is operated by a US company and the bucket is Amazon S3, a US provider — assume legal process reaches both. An order served on us can compel at most what we possess: the enumerated vault-lane metadata (who, when, how big, how often) and, for projects that deploy, the deploy lane's plaintext artifacts. It cannot compel vault keys, vault content, commit ids, branch names, or file paths — none of it exists on our side to produce. A compelled host is a strictly weaker adversary than the malicious host every claim on this page is already stated against. Two honesty notes: we make no promise to notify you of legal process (a sealed order could forbid exactly that), and we do not claim the metadata is harmless — it is real information, and it is exactly what an order would yield.
Not from here. There is no plaintext on our side to index, embed, or train on — and that holds against a future us too: a policy change, an acquisition, or a partnership cannot un-encrypt ciphertext. Meanwhile the product is built for your agents, not against them: an agent you authorize holds its own envelope and works normally, while everyone else's AI — including ours — sees ciphertext. Selective disclosure, not abstinence.
The bucket operator sees the same ciphertext objects, minus our control-plane context, plus access patterns; it cannot decrypt or forge anything, for the same reason we cannot. The network sees endpoints, sizes, and timing under TLS. Both can deny service or destroy bytes — which is an availability question, answered by the create-only storage posture and the mirror (below), never a confidentiality one.
No — this is not zero-knowledge hosting, and we say so on every page. The platform learns the enumerated metadata list: that the vault exists and whose it is, ciphertext sizes and object ids, generations and push cadence, access times, policy state, and the structural skeleton of maintenance. The list is closed — the protocol treats any plaintext-structured object beyond it as a violation, not a detail. What never appears in it: content, branch names, commit messages, file paths, commit ids, or any un-keyed digest of plaintext.
Sizes and cadence are visible, and we disclose that rather than pad it away — traffic-shaping countermeasures are not part of v0, so an observer can estimate how big your repository is and how actively you push. What an observer cannot do is the sharper attack this category usually hides: there is no cross-vault deduplication oracle and no compression oracle against the server, because nothing the server can compare is derived from plaintext un-keyed — every digest the platform can compare is keyed with HMAC. Two vaults containing the same file look unrelated to us.
No. Unauthorized, nonexistent, and wrong-project requests all return one identical refusal, so no response distinguishes "exists but not yours" from "does not exist."
KyGit is run402's git vault under its own front door, and run402 is also a deployment platform. If — and only if — you choose to deploy a project, the files you publish pass through run402's content store in plaintext, custodially, because serving a website requires reading it. That is the second sentence of the first claim: deployment artifacts remain a disclosed plaintext custody boundary. Deploying is an explicit act — it decrypts exactly what you chose to publish — and the deploy path carries its own rule — Activation requires vault admission by default; an explicit, audited override can bypass it. A deploy cannot silently outrun its own history capture. A vault-only KyGit project never deploys anything, and then the custody boundary is empty: there is no plaintext of yours anywhere on our side.
No. Every head is signed with your Ed25519 writer key, which never leaves your keystore; the server registers only the public half. Signature verification is strict RFC 8032 — non-canonical encodings and small-order points are rejected, and the vector set includes a ZIP215-only signature that must fail — so there is no malleable acceptance path to exploit. A server that cannot sign cannot mint history; the strongest thing a malicious host can do is refuse to show you the newest history you wrote, which is the next question.
Not below your pin. Your client keeps a local trust pin at the newest verified generation; a
served chain that regresses below it fails loudly (GENERATION_REGRESSION), and
every link is verified — signatures, hashes, continuity — so a fork or a splice fails
structurally. The honest limit, stated rather than hidden: freshness is pin-relative. A
malicious host can serve a stale but authentic suffix, and no client can
cryptographically prove the host is not withholding a newer one — that impossibility is general
to hosted storage, not particular to us. The mitigations are the read-back rule (your own push
is confirmed against storage before the client believes it), pins that only move forward, and
the mirror, which dual-pushes on every snapshot so the copy in your bucket tracks the newest
generation you published.
A fabricated chain can be internally consistent — the attacker signs it with their own key — but it cannot be your vault: your client pins your writer and genesis identity, and the printed recovery receipt binds the vault's genesis hash and creator fingerprints, so a substituted vault is refused on mismatch. This is exactly why the receipt is worth keeping copies of, and why a recovery without receipt or pin is labeled unauthenticated salvage rather than passed off as verified.
No — context binding is everywhere. A content frame's AAD binds vault id, object kind, object
id, epoch, and suite; move it anywhere else and the tag fails. An envelope's HPKE
info/AAD bind the protocol label, suite, recipient fingerprint, sealing key, vault
id, epoch, and (during rotation) the specific rotation attempt; it opens in exactly one context.
The vector set carries per-field rejection vectors — each bound field is flipped in turn and the
operation must fail — plus "near-neighbor" encodings of the binding data that must not collide.
It gets caught, attributably. Every object is AEAD-authenticated, its framed hash is named in a
signed head, and storage is create-only — there is no "latest" pointer to clobber, so corruption
cannot hide behind an overwrite; it can only appear as a loud, detectable gap in a chain your
client (or the independent Rust verifier) walks. run402 repos fsck verifies from
your pin to the newest listed generation and fails closed on a regression, a gap, or a
descriptor it cannot validate.
This exact attack was found by the adversarial review — round one of the epoch-rotation review showed an earlier draft would have let the platform's member-key directory smuggle a host-controlled key into a recipient set — and the design was rejected and rebuilt around it. As shipped: your client seals envelopes only to recipient keys the vault owner has confirmed, pins recipient keys on first use, and refuses a rotation whose recipient set does not exactly match the owner-confirmed state. The honest boundary, printed in the threat model verbatim: recipient authenticity today is operator-confirmed, not host-blind — your confirmation is the thing standing between a malicious directory and your recipient set, and an owner-signed recipient registry is the named hardening before any stronger claim gets made.
No. Every primitive is a published standard (Ed25519, HPKE per RFC 9180, XChaCha20-Poly1305,
HKDF-SHA-256, SHA-256/HMAC, RFC 8785 canonical JSON) in mainstream open-source implementations
— the noble/scure family and @hpke/core on the TypeScript side; RustCrypto,
rust-hpke, and ed25519-dalek on the Rust side. What is ours
is the protocol that composes them — object model, bindings, state machines — and that is
precisely the layer the adversarial review spent forty-nine rounds
attacking, the vector set freezes, and two independent implementations cross-check.
Three independent layers would each have to fail. Nonces are 24 bytes (the XChaCha extension exists precisely to make random nonces safe at scale) and drawn fresh from the OS CSPRNG. Every object is encrypted under its own HKDF-derived key, so a nonce collision across objects would be harmless anyway. And object ids are single-use key labels — a retry or resumed upload re-reads and compares rather than re-encrypting under the same id, which removes the concurrency-and-retry path by which "same key, same nonce, different plaintext" actually happens in production systems.
There is nothing to downgrade to. One suite exists (r402s-1); the suite identifier
is authenticated inside every frame and envelope; no negotiation happens anywhere in the
protocol. A weaker-suite attack requires a second suite, and introducing one is a numbered
protocol revision with its own review and vectors, not a runtime code path.
Both are addressed at the root. Everything signed is serialized as RFC 8785 canonical JSON under a strict, strings-only profile — no numbers, no duplicate keys, no lone surrogates — so there is one byte sequence per object and nothing for a signer and verifier to disagree about. Verification is strict RFC 8032 with the permissive-verification mode explicitly pinned off and a conformance vector proving the permissive-only signature is refused. And because parser differentials are exactly the bug class that sneaks past a single implementation, the Rust verifier carries its own hand-written strict parser and replays the same accept/reject vectors.
The protocol does not rely on the AEAD to prevent that. Envelopes are signature-verified before any open is attempted; the epoch key carries a signed commitment that the opened key must reproduce; and the client refuses — with named, distinct diagnostics — an envelope whose opened key fails the commitment or fails to decrypt the generation it claims to carry. A wrong-key open therefore cannot be silently accepted as content.
An honest answer instead of a buzzword: r402s-1 is a classical suite. X25519 key
agreement is not post-quantum, so an adversary who records your ciphertext today and obtains a
cryptographically relevant quantum computer later could attack the recorded key envelopes —
"harvest now, decrypt later" applies to KyGit as it applies to TLS and to every deployed E2E
system of this generation. The suite identifier exists in every frame and envelope so that a
successor suite (e.g. a hybrid post-quantum KEM, which HPKE accommodates) can be introduced as a
deliberate, reviewed, vectored protocol revision. We will make a post-quantum claim when we ship
one, and not before.
Straight: a product whose confidentiality rests on us holding zero vault keys has, by
construction, no platform-side rescue for lost keys. The durability sentence is
"The vault protects source history from host-side loss while a principal keystore
survives" — and the "while" clause is the claim, not a caveat. What survives what:
losing the repository key file with your identity intact is recovered from your own envelope;
losing only the signing key leaves history readable but read-only; losing your whole machine is
recovered if the keystore is backed up or a covered teammate holds an envelope — and terminal if
every covered keystore is gone, which the client tells you to your face rather than in a
footnote. Practical consequences: back up ~/.config/run402/gitvault/, and cover a
second principal. Either one keeps a lost machine from being terminal.
Two different removals, two different strengths, stated separately. Removing their org
membership is immediate and operational: the platform stops serving them heads, ciphertext, and
envelopes. Making the removal cryptographic is epoch
rotation, live today: run402 repos access revoke-key removes the recipient,
run402 repos access repair rotates the vault to a fresh epoch key sealed only to
the remaining confirmed recipients, and run402 repos access declare-exposure is the
same ceremony for a key you believe compromised. After an admitted rotation, the
removed key opens nothing published from the rotation forward. The limit we will not launder:
rotation is forward-only. Ciphertext someone already fetched decrypts with the
key they already held — no rotation, here or anywhere, unshares what someone has seen. Copy that
promises retroactive un-sharing is lying to you.
Write, not destroy. A deploy delegate holds exactly {read heads, read ciphertext, upload, publish} — enough to push history, never to prune, compact, repair, change policy, or override activation, because those are owner powers and a delegate is structurally never an owner. Nothing the agent holds can delete the history it wrote; a compromised agent credential is revoked with one call, and the vault's history survives everything the credential could reach. Compare that with handing an agent a forge account whose token can force-push and delete repositories.
Then the attacker is on the trusted side of the boundary, and no end-to-end design survives
that intact — an attacker with your keystore is you, exactly as an attacker with your SSH key is
you to GitHub. What the design still gives you: the blast radius is bounded to vaults whose
envelopes that keystore holds; repos access declare-exposure plus a rotation
re-keys a vault forward once you have evicted the attacker; and because the vault is a second
remote, your primary host and your teammates' pins make a quiet history rewrite by the attacker
detectable rather than silent.
Supply chain is the honest soft underbelly of every developer tool, so here is the posture
rather than a promise: the client and its crypto dependencies are open source and pinned; the
Rust verifier is built --locked against a committed lockfile with checksummed
release binaries, shares no dependencies with the npm stack, and can re-verify what the client
produced — so a backdoored client that forges or mangles vault state is detectable by an
implementation the same compromise cannot reach. A backdoored client that exfiltrates your keys
remains the client-side residual stated above; pinning versions,
auditing diffs, or building from source keeps that trust decision in your hands.
Pushing to KyGit needs KyGit up — we will not pretend otherwise, and we make no uptime claims here. What is structurally true: commits are local, so an outage delays publishing, never work; the byte path has no git application server in it (client ↔ storage via presigned URLs, with only the small admission surface on us); and a second remote in an independent failure domain means your primary host's outage and ours are uncorrelated — when one is down, the other keeps taking pushes. Reads have a stronger story: with the mirror, recovery and cloning need no server of ours at all.
Against our own bugs and our own code: the bucket policy — enforced by AWS, not by our application — accepts only create-only conditional writes, refuses bulk deletion outright, and allows no lifecycle expiry; create and delete are separate code paths with separate powers; every deletion is two-phase, attested, receipted, and requires the two independent verifier receipts; unreachable history stays recoverable for at least 90 days measured from storage-commit time; and every storage operation is independently logged. The bytes themselves rest on storage designed for eleven nines of object durability. Against a genuinely malicious us, we say the true thing: retention is an operational promise of the platform, not a cryptographic guarantee against it — no cryptography can stop a storage provider from destroying what it stores. What the protocol guarantees instead is attribution: a deletion that was not owner-authorized leaves a ledger gap your own client detects. Silent loss is loud loss — and the mirror means loss on our side is not loss.
Nothing dramatic. Read-side vault routes are never lifecycle-gated: through the billing grace ladder your vault stays readable and cloneable; what a lapsed lease gates is control-plane writes. And the mirror means your history is never hostage to a billing state in the first place.
The three scenarios share one answer, because the design never depended on our goodwill: the exit ramp. Your ciphertext replicates to a bucket you own. We can't decrypt your repository history — and we can't hold it either. If we disappeared tomorrow, your repository is recoverable from a bucket listing and your keys, with an open-source tool. No server required. Provable, not promised. We proved it against production: a real vault, mirrored, then recovered ref-exact with the gateway deliberately unreachable. And acquisition changes nothing about ciphertext: a future owner of this company inherits our incapacity along with our servers — we hold no key for them to change our minds with. The honest limits ride along: offline recovery proves validity, never freshness, and without your keys, mirrored ciphertext recovers nothing.
You shouldn't have to — that is the design goal. The wire protocol, the threat model, the schemas, the frozen vectors, the TypeScript crypto core, and the Rust verifier are all public. Run the verifier against your own vault's bucket export; replay the vectors; diff the claims on this page against the threat model. Even this page is machine-gated: a CI truth gate checks every KyGit page against the published client's own capability manifest on every deploy and every day, so the marketing cannot quietly outrun the software.
Every one of these is a sentence a louder page would have rounded up. The narrowness is the claim: what remains, you can check.