Skip to main content

Crate gunnar_crypt

Crate gunnar_crypt 

Source
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 and git status stays 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 (feature user-key) — the USER’s encryption key: one key pair per user (age X25519, or the hybrid age1pq1… 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), an age1pq1… key file, or a printed recovery code (age’s scrypt).

  • wrap (feature user-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. Feature wrap adds the SSH unlock method and the quantum-safe user key.

  • pq (feature wrap) — that hybrid recipient and its identity, until an age release carries its own.

  • diff (feature diff) — 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): an age1pq1… public key and its AGE-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…, or age1pq1… 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 .gitattributes lines gunnar crypt init writes.

Functions§

diff_blobs
The diff between two stored blobs of one path (None = the path does not exist on that side), opened with ring. What gunnar crypt diff prints 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 before gunnar crypt init is plaintext).
random_bytes
N bytes from the OS random source (keys, recovery codes).