Expand description
Encrypted repositories: the operator of a gunnar server cannot read a repository’s content; only its members can, each with their own encryption key, opened by their SSH key or their security key.
The design, the threat model and what still leaks are in
.nornir/encrypted-repos.md. This crate is the part every door shares:
-
envelope— the format of one encrypted file (blob): a 13-byte header naming the key epoch, then AES-256-SIV over the content. Deterministic on purpose: the same content under the same key gives the same blob, so git’s clean filter is stable andgit statusstays quiet, and the blob id is a KEYED id (it depends on the secret key), so nobody without the key can confirm that a known file is in the repository. -
keyring— the repository’s data keys, one per epoch. A rotation adds an epoch; old epochs stay so members can still read old content. -
members— the plaintext list of members’ PUBLIC keys (.gunnar-crypt/members). The key ring is wrapped to every key in it. -
policy— what a server checks on a push to an encrypted repository: every blob is an envelope, except the three files the scheme itself needs. It reads formats, never keys. -
userkey(featureuser-key) — the USER’s encryption key: one key pair per user (age X25519, or the hybridage1pq1…for quantum-safe repositories), its private half stored on the server only as age files, one per unlock method: an SSH key file (CLI), a security key’s WebAuthn PRF / FIDO2 hmac-secret output through HKDF-SHA256 (browser and CLI), anage1pq1…key file, or a printed recovery code (age’s scrypt). -
wrap(featureuser-key) — the age wrap of the key ring to every member’s user public key. A repository never names an SSH key or a security key; adding or removing an unlock method touches no repository. Featurewrapadds the SSH unlock method and the quantum-safe user key. -
pq(featurewrap) — that hybrid recipient and its identity, until an age release carries its own. -
diff(featurediff) — line diffs of decrypted content. The server cannot diff ciphertext and keeps no pre-made diffs; the client does it.
One crate, two targets. The CLI and the browser console run this same
code, native and wasm32: fetch ciphertext → open the user key with an
unlock method → unwrap the key ring → open and diff locally → seal before
push. The CLI opens the user key with the SSH key file in ~/.ssh (or a
security key over FIDO2 hmac-secret); the browser opens it with a security
key (WebAuthn PRF), in the tab, with no helper. tests/vectors.rs and
tests/userkey.rs run the same vectors on both targets.
Who links what: gunnar-server takes default-features = false (policy and
format only; no cipher, no key), the browser console takes cipher,
diff and user-key, and the CLI takes everything.
Re-exports§
pub use zeroize;
Modules§
- diff
- Line diffs of DECRYPTED content, computed where the key is: in the CLI
(
gunnar crypt diff) and in the browser console’s wasm. The server never sees plaintext, so it cannot diff an encrypted repository; there are no pre-made diffs either (they would only cost space). Both sides run this same function, so a diff reads the same in the terminal and in the browser. - door
- The forge door for a person’s encryption key (
gunnar-user-key, CRYPT-YUBI 2026-10-26): what the CLI asks over the SSH door git already signs in with, and what the server answers. Pure text, both sides. - envelope
- One encrypted file (blob). Two versions; this code writes v2 and reads both.
- keyring
- The repository’s data keys: one 64-byte AES-256-SIV key per EPOCH.
- members
.gunnar-crypt/members: who the key ring is wrapped to, by each member’s USER encryption key (its public half).- paths
- Where the scheme’s own files live in a repository.
- policy
- What a server checks on a push to an ENCRYPTED repository.
- pq
- age’s hybrid post-quantum recipient,
mlkem768x25519(c2sp.org/age): anage1pq1…public key and itsAGE-SECRET-KEY-PQ-1…identity. - userkey
- The user’s encryption key: one key pair per user, the same for every repository, opened by any one of the user’s unlock methods.
- wrap
- The key ring, wrapped with age to every member’s USER encryption key
(
age1…, orage1pq1…in a quantum-safe repository), and unwrapped with that user key (crate::userkey).
Constants§
- DRIVER
- The name of the git filter, diff and merge drivers.
- GITATTRIBUTES_
LINES - The
.gitattributeslinesgunnar crypt initwrites.
Functions§
- diff_
blobs - The diff between two stored blobs of one path (
None= the path does not exist on that side), opened withring. Whatgunnar crypt diffprints and what the console shows for an encrypted repository. - open_
blob - Open a blob as text: an envelope is opened with
ring; anything else is returned as it is (history from beforegunnar crypt initis plaintext). - random_
bytes Nbytes from the OS random source (keys, recovery codes).