Skip to main content

Module secret

Module secret 

Source
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

  1. *.age files 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.

  2. 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 the age-plugin-fido2-hmac etc.) so it can travel with the dotfiles repo as ciphertext. Used only by yui secret wrap and yui 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§

SecretReport
Per-apply summary of what the secrets walker did. Mirrors RenderReport’s shape so the apply orchestrator can union managed-path lists across both pipelines.

Functions§

decrypt_all
Walk every *.age under source, decrypt to a sibling without the suffix, and report the plaintext paths so the caller can add them to the managed .gitignore section. Mirrors the render::render_all shape: ignore-files honoured via paths::source_walker, .yuiignore filters apply, .yui/ and .git/ skipped.
decrypt_with_passkeys
Decrypt ciphertext using any of the supplied (potentially plugin-backed) identities. Used by yui secret unlock.
decrypt_x25519
Decrypt ciphertext (the bytes of a *.age file) using a single X25519 identity. Used by the apply pipeline.
encrypt_to_passkeys
Encrypt plaintext to one or more potentially-plugin recipients. Used by yui secret wrap to encrypt the X25519 identity to passkey devices (Pixel + Bitwarden + …).
encrypt_x25519
Encrypt plaintext to one or more X25519 recipients. Used for *.age files 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 by yui secret unlock where 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 way apply needs it. Refuses anything other than a plain AGE-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_recipients which batches plugin recipients.
parse_passkey_recipients
Parse a list of recipient strings, grouping plugin recipients by plugin name into a single RecipientPluginV1 per group. Without grouping, each plugin recipient would spawn the age-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] recipients list which encrypts the user’s *.age files — those must stay plugin-free so apply doesn’t prompt.
strip_age_suffix
Strip the .age suffix from a path, if present. Returns None when 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 bytes is a parseable X25519 identity file — at least one non-comment line is AGE-SECRET-KEY-1…. Used by both yui secret store (refuse to upload a corrupted local file) and yui 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 bytes to path with owner-only permissions on Unix (0600). On Windows we fall back to a plain write because file permissions don’t translate cleanly — the user’s AGE-SECRET-KEY is still in their ~/.config/yui/ directory which isn’t shared by default. Used by both secret_init and secret_unlock so neither flow leaves the X25519 secret world-readable. PR #60 review by coderabbitai.

Type Aliases§

BoxedIdentity
Boxed dyn-trait identity. age’s Decryptor::decrypt takes a trait-object iterator, so we hand it boxed identities; X25519 and plugin variants share the same type at the boundary.
BoxedRecipient
Boxed dyn-trait recipient. Same reasoning as BoxedIdentityEncryptor::with_recipients works on trait objects.