Parse age_recipients at config load and never echo the entry (closes #153)
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- 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. Model: opus-4-8
This commit was merged in pull request #187.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -2,6 +2,7 @@ package crypto_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age"
|
||||
@@ -176,3 +177,22 @@ func TestEncryptorUpdateRecipients(t *testing.T) {
|
||||
t.Error("should not decrypt with identity1")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewEncryptorSecretKeyNotEchoed verifies that a secret key mistakenly
|
||||
// passed as a recipient does not appear in the returned error. A recipient
|
||||
// string can be sensitive, so the error must name only the position.
|
||||
func TestNewEncryptorSecretKeyNotEchoed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
secretKey := "AGE-SECRET-KEY-19CR5YSFW59HM4TLD6GX" +
|
||||
"VEDMZFTVVF7PPHKUT68TXSFPK7APHXA2QS2NJA5"
|
||||
|
||||
_, err := crypto.NewEncryptor([]string{secretKey})
|
||||
if err == nil {
|
||||
t.Fatal("NewEncryptor returned nil, want error")
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), secretKey) {
|
||||
t.Fatalf("error echoed the recipient value: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user