Expand description
age-based file encryption for the secrets pipeline.
*.age files in source are decrypted to a sibling without the
.age suffix on every apply, and the sibling lands in the
managed # >>> yui rendered <<< section of .gitignore so the
plaintext never gets committed. From the apply walker’s
perspective the sibling is just another regular file — link it
to target like any other dotfile.
§Why a separate module from render.rs
*.tera and *.age both produce a sibling-without-suffix and
both wire that sibling through the .gitignore managed section,
but they’re different operations: rendering needs Tera contexts
and yui-when headers; decryption needs an age identity file and
recipient validation. Keeping secret::* self-contained also
means the crypto stays out of render.rs, which a casual
reader expects to be pure-text manipulation.
§Two distinct encryption paths
-
*.agefiles in apply — encrypted to[secrets] recipients, decrypted with the plain X25519 secret at[secrets] identity(e.g.~/.config/yui/age.txt). Runs every apply, must be friction-free, must NOT trigger device prompts. Identities here are X25519 only by convention. -
passkey wrap of the X25519 secret itself — the user’s
~/.config/yui/age.txt(plain X25519) gets encrypted to one or more passkey recipients (Pixel / Bitwarden / YubiKey, via theage-plugin-fido2-hmacetc.) so it can travel with the dotfiles repo as ciphertext. Used only byyui secret wrapandyui secret unlock— never by apply. Plugin identities appear ONLY here, so the apply path stays plugin-free.
Recipient strings split the same way: age1… for X25519 and
age1<plugin>1… for plugin recipients. Multiple recipient types
can mix in a single ciphertext — useful for wrap, where the
user might want both Pixel and Bitwarden as recovery devices.
Structs§
- Secret
Report - Per-
applysummary of what the secrets walker did. MirrorsRenderReport’s shape so the apply orchestrator can union managed-path lists across both pipelines.
Functions§
- decrypt_
all - Walk every
*.ageundersource, decrypt to a sibling without the suffix, and report the plaintext paths so the caller can add them to the managed.gitignoresection. Mirrors therender::render_allshape: ignore-files honoured viapaths::source_walker,.yuiignorefilters apply,.yui/and.git/skipped. - decrypt_
with_ passkeys - Decrypt
ciphertextusing any of the supplied (potentially plugin-backed) identities. Used byyui secret unlock. - decrypt_
x25519 - Decrypt
ciphertext(the bytes of a*.agefile) using a single X25519 identity. Used by the apply pipeline. - encrypt_
to_ passkeys - Encrypt
plaintextto one or more potentially-plugin recipients. Used byyui secret wrapto encrypt the X25519 identity to passkey devices (Pixel + Bitwarden + …). - encrypt_
x25519 - Encrypt
plaintextto one or more X25519 recipients. Used for*.agefiles in the apply pipeline. - generate_
x25519_ keypair - Generate a fresh X25519 keypair. Returns the serialised secret
(write this to the identity file) and the corresponding public
recipient string (add this to
[secrets] recipients). - load_
passkey_ identities - Load every identity from
path, allowing plugin entries (AGE-PLUGIN-…). Used byyui secret unlockwhere the file holds passkey identities (Pixel, Bitwarden, …) that age must drive interactively at decrypt time. - load_
x25519_ identity - Load an age X25519 identity from
path, the wayapplyneeds it. Refuses anything other than a plainAGE-SECRET-KEY-1…secret — apply must NEVER drop into a plugin flow because that would prompt for a touch / PIN / biometric on every run. (The user’s mental model is “Pixel only at unlock time, not every apply”, so apply stays X25519-only on principle.) - parse_
passkey_ recipient - Parse a single recipient string — X25519 or plugin. Used in
tests and for debugging; production wrap goes through
parse_passkey_recipientswhich batches plugin recipients. - parse_
passkey_ recipients - Parse a list of recipient strings, grouping plugin recipients
by plugin name into a single
RecipientPluginV1per group. Without grouping, each plugin recipient would spawn theage-plugin-*binary independently — wasteful and (for some plugins) prompts the user multiple times. (PR #60 review by gemini-code-assist.) - parse_
x25519_ recipient - Parse an X25519 recipient string (
age1…). Used for the[secrets] recipientslist which encrypts the user’s*.agefiles — those must stay plugin-free so apply doesn’t prompt. - strip_
age_ suffix - Strip the
.agesuffix from a path, if present. ReturnsNonewhen the path doesn’t end in.age(so callers can short-circuit non-secret files in a uniform walk). - validate_
x25519_ identity_ bytes - Validate that
bytesis a parseable X25519 identity file — at least one non-comment line isAGE-SECRET-KEY-1…. Used by bothyui secret store(refuse to upload a corrupted local file) andyui secret unlock(refuse to persist a vault item that doesn’t actually hold an age identity). The messages name “the payload” rather than a specific source so both call sites read naturally. - write_
private_ file - Write
bytestopathwith owner-only permissions on Unix (0600). On Windows we fall back to a plain write because file permissions don’t translate cleanly — the user’sAGE-SECRET-KEYis still in their~/.config/yui/directory which isn’t shared by default. Used by bothsecret_initandsecret_unlockso neither flow leaves the X25519 secret world-readable. PR #60 review by coderabbitai.
Type Aliases§
- Boxed
Identity - Boxed dyn-trait identity. age’s
Decryptor::decrypttakes a trait-object iterator, so we hand it boxed identities; X25519 and plugin variants share the same type at the boundary. - Boxed
Recipient - Boxed dyn-trait recipient. Same reasoning as
BoxedIdentity—Encryptor::with_recipientsworks on trait objects.