Parse age_recipients at config load and never echo the entry (closes #153)
check / check (pull_request) Successful in 2m18s
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:
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/vaultik/internal/chunker"
|
||||
@@ -178,6 +179,83 @@ func TestValidateBlobSizeLimit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateAgeRecipients checks that recipients are parsed at config load
|
||||
// (a bad entry fails immediately, not mid-backup) and that no invalid entry —
|
||||
// least of all a pasted secret key — is echoed in the error.
|
||||
func TestValidateAgeRecipients(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
baseConfig := func(recipients []string) *Config {
|
||||
return &Config{
|
||||
AgeRecipients: recipients,
|
||||
Snapshots: map[string]SnapshotConfig{"test": {Paths: []string{"/tmp/src"}}},
|
||||
StorageURL: "file:///tmp/vaultik-test-store",
|
||||
ChunkSize: Size(10 * 1024 * 1024),
|
||||
BlobSizeLimit: Size(10 * 1024 * 1024 * 1024),
|
||||
CompressionLevel: 3,
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
recipients []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "config init placeholder is rejected",
|
||||
recipients: []string{"age1REPLACE_WITH_YOUR_PUBLIC_KEY"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "ssh-ed25519 recipient is rejected",
|
||||
recipients: []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexamplekeydata"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "truncated age1 string is rejected",
|
||||
recipients: []string{"age1short"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "secret key passed as recipient is rejected",
|
||||
recipients: []string{testIntegrationAgePrivateKey},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "two valid recipients are accepted",
|
||||
recipients: []string{testSneakAgePublicKey, testIntegrationAgePublicKey},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := baseConfig(tt.recipients).Validate()
|
||||
if !tt.wantErr {
|
||||
if err != nil {
|
||||
t.Fatalf("Validate() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("Validate() returned nil, want error")
|
||||
}
|
||||
|
||||
// The entry itself must never appear in the error, since a
|
||||
// recipient string can be a secret key.
|
||||
for _, recipient := range tt.recipients {
|
||||
if strings.Contains(err.Error(), recipient) {
|
||||
t.Fatalf("Validate() error echoed the recipient value: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtractAgeSecretKey tests extraction of AGE-SECRET-KEY from various inputs
|
||||
func TestExtractAgeSecretKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
Reference in New Issue
Block a user