Parse age_recipients at config load and never echo the entry (closes #153)
check / check (pull_request) Successful in 2m18s

Config.Validate now parses every age_recipients entry with
age.ParseX25519Recipient, so a bad recipient fails at config load instead
of deep in a backup after the snapshot row and tree walk. On failure the
error names the position (age_recipients[N]) and never the value: a
recipient string can itself be a secret key an operator pasted by mistake,
and age's own error quotes its input. An entry starting with
AGE-SECRET-KEY- (compared case-insensitively) gets a specific message.

The remaining parse sites (blobgen.NewWriter, crypto NewEncryptor and
UpdateRecipients), reachable by callers that skip config.Load, likewise
drop the value and age's wrapped error, naming only the position.

test/config.yaml's placeholder second recipient is replaced with a valid
X25519 key so it still loads.

Model: opus-4-8
This commit is contained in:
2026-09-22 10:41:59 +00:00
parent b4654f8e52
commit e0e43548b7
7 changed files with 176 additions and 8 deletions
+13 -4
View File
@@ -17,6 +17,11 @@ import (
// without any recipient public keys.
var ErrNoRecipients = errors.New("at least one recipient is required")
// errInvalidRecipient is returned when a recipient string does not parse as
// an X25519 age1... public key. It omits the value, which can be sensitive.
var errInvalidRecipient = errors.New(
"not a valid X25519 age1... recipient")
// Encryptor provides thread-safe encryption using the age encryption library.
// It supports encrypting data for multiple recipients simultaneously, allowing
// any of the corresponding private keys to decrypt the data. This is useful
@@ -36,10 +41,12 @@ func NewEncryptor(publicKeys []string) (*Encryptor, error) {
}
recipients := make([]age.Recipient, 0, len(publicKeys))
for _, key := range publicKeys {
for i, key := range publicKeys {
// The key string can be sensitive (e.g. a secret key pasted by
// mistake), so the error names its position, never its value.
recipient, err := age.ParseX25519Recipient(key)
if err != nil {
return nil, fmt.Errorf("parsing age recipient %s: %w", key, err)
return nil, fmt.Errorf("%w: recipient %d", errInvalidRecipient, i)
}
recipients = append(recipients, recipient)
@@ -142,10 +149,12 @@ func (e *Encryptor) UpdateRecipients(publicKeys []string) error {
}
recipients := make([]age.Recipient, 0, len(publicKeys))
for _, key := range publicKeys {
for i, key := range publicKeys {
// The key string can be sensitive (e.g. a secret key pasted by
// mistake), so the error names its position, never its value.
recipient, err := age.ParseX25519Recipient(key)
if err != nil {
return fmt.Errorf("parsing age recipient %s: %w", key, err)
return fmt.Errorf("%w: recipient %d", errInvalidRecipient, i)
}
recipients = append(recipients, recipient)