Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-crypt/Cargo.toml

[package]
name = "gunnar-crypt"
description = "Encrypted repositories: the content envelope, the per-repo key ring, its members, the server's plaintext check, each person's one encryption key and its unlock methods, and the ring's wrap to members' encryption keys"
readme = "README.md"
# 1.5.1 (2026-10-26, lane GUN-INSTALL / G-BOTH, the R5C publish): master differs from crates.io, so
# it gets its own version; a patch: cargo-semver-checks finds no API change against crates.io 1.5.0.
version = "1.6.1"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
# On the `cargo install gunnar` chain (owner order 2026-10-22).
publish = true
authors.workspace = true
repository.workspace = true

[lib]
name = "gunnar_crypt"
path = "src/lib.rs"

# ONE crate, native and wasm32: the CLI and the browser console run the same
# format, user key, unwrap, decrypt, diff and encrypt code. Who takes what:
#
# * gunnar-server (`default-features = false`): only `policy` and the envelope
#   FORMAT checks. No key ever reaches the server, so it links no cipher that
#   could use one, and no age.
# * the browser console (`features = ["cipher", "diff", "user-key", "pq"]`):
#   opens the person's encryption key with their security key (WebAuthn PRF),
#   then the ring, opens and seals envelopes and diffs in the browser. age's
#   X25519, scrypt and the hybrid post-quantum stanza (no SSH); `wrap` itself
#   compiles and is tested on wasm32 as well.
# * the gunnar CLI (`default`): everything.
[features]
default = ["wrap", "diff"]
cipher = ["dep:aes-siv", "dep:zeroize", "dep:hmac", "dep:hkdf"]
keygen = ["cipher", "dep:getrandom"]
# The user's encryption key (`userkey`): one key pair per user (quantum-safe
# with `pq`, the default for a new key; else X25519), its
# private half stored only as age files wrapped to each unlock method (a
# security key's PRF secret through HKDF, a printed recovery code through
# age's scrypt), and the ring wrapped to members' user public keys. What the
# browser console links with `pq`: no SSH.
user-key = ["keygen", "dep:age", "dep:hkdf", "dep:bech32", "dep:chacha20poly1305"]
# The quantum-safe user key and ring (`pq`): age's hybrid ML-KEM-768 + X25519
# recipient. What the browser console links beside `user-key` since
# CRYPT-PQ-DEFAULT (2026-10-30): a new key and a new repository are quantum-safe
# by default, so the console makes, opens and wraps them too. Native and wasm32.
pq = ["user-key", "pq-keys", "dep:age-core", "dep:hpke", "dep:x-wing"]
# Everything the CLI needs on top: the SSH unlock method (age's SSH
# recipients).
wrap = ["pq", "age/ssh"]
# Decode an `age1pq1…` key (bech32, its checksum and its 1216 bytes): the key
# store's check (gunnar-core) and the wrap. Without it a member line is
# classified by its shape only (prefix, length, alphabet), which is all the
# server's ring check needs — and the browser console and gunnar-ui-server
# link no new crate.
pq-keys = ["dep:bech32"]
diff = ["dep:imara-diff"]

[[example]]
# The browser matrix's v1 security-key wrap (CRYPT-PQ-SK): gunnar-ui uitest --sk-fixture.
name = "sk_v1_fixture"
required-features = ["user-key"]

[dependencies]
aes-siv = { version = "0.8", optional = true, default-features = false, features = ["alloc", "zeroize"] }
# The envelope v2 file nonce: HMAC-SHA256 of the whole plaintext under a key
# HKDF-SHA256 derives from the epoch key. The 0.13 line over sha2 0.11, the
# RustCrypto generation russh brings (2026-10-22: one generation where we choose).
hmac = { version = "0.13", optional = true }
hkdf = { version = "0.13", optional = true }
# Not optional: the server's ring check (policy) reads an age header (base64)
# and names an SSH member key by its age tag (SHA-256). Both were already in
# every graph that takes `cipher`.
base64 = "0.22"
sha2 = "0.11"
# `pq-keys`: the same bech32 age uses.
bech32 = { version = "0.9", optional = true }
zeroize = { version = "1", optional = true }
getrandom = { version = "0.2", optional = true }
# 0.11.5 (2026-07-14), not 0.12: age 0.12 pulls ml-kem 0.2, whose `kem
# 0.3.0-pre` cannot share a lock with russh's ml-kem 0.3 (`kem 0.3.0`).
age = { version = "0.11.5", optional = true, default-features = false, features = ["armor"] }
# The quantum-safe wrap (`pq`): age's hybrid ML-KEM-768 + X25519 recipient
# (`mlkem768x25519`, `age1pq1…`, c2sp.org/age), which no age release carries
# yet (0.12.1 has only the encryption-only tag types, and pulls ml-kem 0.2,
# which cannot share a lock with russh's ml-kem 0.3). The stanza is glue on
# age 0.11.5's public Recipient/Identity traits; the cryptography is these two
# released RustCrypto crates: HPKE (RFC 9180) with the X-Wing KEM
# (MLKEM768-X25519), HKDF-SHA256 and ChaCha20Poly1305 — exactly age's
# parameters — and x-wing 0.1.1's decapsulation that REJECTS a non-contributory
# X25519 share, as the spec's test vectors require. Both use ml-kem 0.3, the
# one russh already brings.
age-core = { version = "0.11", optional = true }
hpke = { version = "0.14.1", optional = true, default-features = false, features = ["alloc", "chacha", "mlkem", "x25519"] }
x-wing = { version = "0.1.1", optional = true, default-features = false, features = ["zeroize"] }
imara-diff = { version = "0.2", optional = true }
# The security key's wrap v2 (CRYPT-PQ-SK): XChaCha20-Poly1305 under a key HKDF
# derives from the PRF output. 0.11, the RustCrypto generation hpke already
# brings (no new crate in the lock).
chacha20poly1305 = { version = "0.11", optional = true, default-features = false, features = ["alloc"] }

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", optional = true, features = ["js"] }

[dev-dependencies]
base64 = "0.22"
age = { version = "0.11.5", default-features = false, features = ["ssh", "armor"] }

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tempfile = "3"
# The C2SP CCTV age vectors ship two of the hybrid files zlib-compressed.
flate2 = { workspace = true }
sha2 = "0.11"

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3"