Lots more changes

This commit is contained in:
2026-06-01 01:29:47 -05:00
parent 8bec7f0dda
commit 050ec138f3
20 changed files with 1773 additions and 689 deletions
+163 -38
View File
@@ -1,6 +1,8 @@
package config
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"net/netip"
@@ -14,23 +16,34 @@ import (
)
const (
defaultListenAddr = ":8080"
defaultReadHeaderTimeout = "5s"
defaultReadTimeout = "30s"
defaultWriteTimeout = "30s"
defaultIdleTimeout = "120s"
defaultMaxHeaderBytes = 1 << 20
defaultMaxUploadSize = "100MiB"
defaultTTL = "1h"
defaultRawCacheMaxSize = "200MiB"
defaultRawCacheMaxEntries = 32
defaultDataDir = "./data"
defaultCleanupInterval = "1m"
defaultRateLimitEnabled = true
defaultRequestsPerMinute = 30
defaultRateLimitBurst = 10
maxDefaultTTLLimit = 24 * time.Hour
minCleanupInterval = 10 * time.Second
defaultListenAddr = ":8080"
defaultReadHeaderTimeout = "5s"
defaultReadTimeout = "30s"
defaultWriteTimeout = "30s"
defaultIdleTimeout = "120s"
defaultMaxHeaderBytes = 1 << 20
defaultAccessLogEnabled = true
defaultAccessLogMaxSize = "100MiB"
defaultAccessLogMaxBackups = 5
defaultMaxUploadSize = "100MiB"
defaultTTL = "15m"
defaultRawCacheMaxSize = "200MiB"
defaultRawCacheMaxEntries = 32
defaultDataDir = "./data"
defaultCleanupInterval = "1m"
defaultStorageEncryptionKey = "vVPlqMfKa8OtD3x0RKp4rVCJ4QoScf5mWdgfg4f+5GU="
defaultEncryptionKeyFile = "config.key"
defaultUIRateLimitEnabled = false
defaultAPIReadRateLimitEnabled = false
defaultAPIWriteRateLimitEnabled = true
defaultUIRequestsPerMinute = 360
defaultUIRateLimitBurst = 120
defaultAPIReadRequestsPerMinute = 300
defaultAPIReadRateLimitBurst = 100
defaultAPIWriteRequestsPerMinute = 30
defaultAPIWriteRateLimitBurst = 10
maxDefaultTTLLimit = 24 * time.Hour
minCleanupInterval = 10 * time.Second
)
type Config struct {
@@ -41,16 +54,25 @@ type Config struct {
}
type ServerConfig struct {
ListenAddr string `yaml:"listen_addr" comment:"Address to bind the HTTP server to."`
ReadHeaderTimeout string `yaml:"read_header_timeout" comment:"Maximum duration for reading request headers (for slowloris protection). Must be > 0."`
ReadTimeout string `yaml:"read_timeout" comment:"Maximum duration for reading the full request, including body. Must be > 0."`
WriteTimeout string `yaml:"write_timeout" comment:"Maximum duration before timing out writes of a response. Must be > 0."`
IdleTimeout string `yaml:"idle_timeout" comment:"Maximum amount of time to wait for the next request when keep-alives are enabled. Must be > 0."`
MaxHeaderBytes int `yaml:"max_header_bytes" comment:"Maximum size of request headers in bytes. Must be > 0."`
ReadHeaderTimeoutDur time.Duration `yaml:"-"`
ReadTimeoutDur time.Duration `yaml:"-"`
WriteTimeoutDur time.Duration `yaml:"-"`
IdleTimeoutDur time.Duration `yaml:"-"`
ListenAddr string `yaml:"listen_addr" comment:"Address to bind the HTTP server to."`
ReadHeaderTimeout string `yaml:"read_header_timeout" comment:"Maximum duration for reading request headers (for slowloris protection). Must be > 0."`
ReadTimeout string `yaml:"read_timeout" comment:"Maximum duration for reading the full request, including body. Must be > 0."`
WriteTimeout string `yaml:"write_timeout" comment:"Maximum duration before timing out writes of a response. Must be > 0."`
IdleTimeout string `yaml:"idle_timeout" comment:"Maximum amount of time to wait for the next request when keep-alives are enabled. Must be > 0."`
MaxHeaderBytes int `yaml:"max_header_bytes" comment:"Maximum size of request headers in bytes. Must be > 0."`
AccessLog AccessLogConfig `yaml:"access_log" comment:"Access log settings for all HTTP requests."`
ReadHeaderTimeoutDur time.Duration `yaml:"-"`
ReadTimeoutDur time.Duration `yaml:"-"`
WriteTimeoutDur time.Duration `yaml:"-"`
IdleTimeoutDur time.Duration `yaml:"-"`
}
type AccessLogConfig struct {
Enabled bool `yaml:"enabled" comment:"Enable HTTP access logs."`
FilePath string `yaml:"file_path" comment:"Optional file path for access logs. When set, logs are tee'd to stdout and this file."`
MaxSize string `yaml:"max_size" comment:"Maximum size for access log file before rotation. Supports B, KB, MB, GB and KiB, MiB, GiB."`
MaxBackups int `yaml:"max_backups" comment:"Number of rotated access log files to keep."`
MaxSizeBytes int64 `yaml:"-"`
}
type LimitsConfig struct {
@@ -66,14 +88,18 @@ type LimitsConfig struct {
type StorageConfig struct {
DataDir string `yaml:"data_dir" comment:"Directory where scratch files and metadata are stored."`
CleanupInterval string `yaml:"cleanup_interval" comment:"How often expired scratches are deleted (minimum 10s)."`
EncryptionKeyFile string `yaml:"encryption_key_file" comment:"Path to a file containing a base64-encoded 32-byte AES key for at-rest encryption. Relative paths are resolved from the config file directory."`
CleanupIntervalParsed time.Duration `yaml:"-"`
EncryptionKeyBytes []byte `yaml:"-"`
}
type SecurityConfig struct {
AllowedIPs []string `yaml:"allowed_ips" comment:"Allowlist for POST /api/scratch as IPs or CIDRs (examples: 127.0.0.1, 192.168.1.0/24). Keep 127.0.0.1 for local access; add trusted ranges as needed. Empty means unrestricted."`
TrustProxyHeaders bool `yaml:"trust_proxy_headers" comment:"Honor X-Forwarded-For / X-Real-IP only when request source IP matches security.trusted_proxy_ips."`
TrustedProxyIPs []string `yaml:"trusted_proxy_ips" comment:"IPs/CIDRs of reverse proxies allowed to supply forwarded client IP headers. Required when trust_proxy_headers is true."`
RateLimit RateLimitConfig `yaml:"rate_limit" comment:"Per-client token bucket on write and scratch read routes."`
RateLimitUI RateLimitConfig `yaml:"rate_limit_ui" comment:"Per-client token bucket on UI routes (/, /u, /s/{id})."`
RateLimitAPIRead RateLimitConfig `yaml:"rate_limit_api_read" comment:"Per-client token bucket on API read routes (GET /api/config, GET /api/scratch/{id}, GET /api/raw/{id})."`
RateLimitAPIWrite RateLimitConfig `yaml:"rate_limit_api_write" comment:"Per-client token bucket on API write route (POST /api/scratch)."`
AllowedPrefixes []netip.Prefix `yaml:"-"`
TrustedProxyPrefixes []netip.Prefix `yaml:"-"`
}
@@ -95,6 +121,9 @@ func Load(path string) (Config, error) {
return Config{}, fmt.Errorf("parse config yaml: %w", err)
}
}
if err := cfg.loadEncryptionKey(path); err != nil {
return Config{}, err
}
if err := cfg.Validate(); err != nil {
return Config{}, err
@@ -115,6 +144,11 @@ func defaults() Config {
WriteTimeout: defaultWriteTimeout,
IdleTimeout: defaultIdleTimeout,
MaxHeaderBytes: defaultMaxHeaderBytes,
AccessLog: AccessLogConfig{
Enabled: defaultAccessLogEnabled,
MaxSize: defaultAccessLogMaxSize,
MaxBackups: defaultAccessLogMaxBackups,
},
},
Limits: LimitsConfig{
MaxUploadSize: defaultMaxUploadSize,
@@ -123,17 +157,29 @@ func defaults() Config {
RawCacheMaxEntries: defaultRawCacheMaxEntries,
},
Storage: StorageConfig{
DataDir: defaultDataDir,
CleanupInterval: defaultCleanupInterval,
DataDir: defaultDataDir,
CleanupInterval: defaultCleanupInterval,
EncryptionKeyFile: defaultEncryptionKeyFile,
EncryptionKeyBytes: mustDecodeDefaultEncryptionKey(),
},
Security: SecurityConfig{
AllowedIPs: []string{
"127.0.0.1",
},
RateLimit: RateLimitConfig{
Enabled: defaultRateLimitEnabled,
RequestsPerMinute: defaultRequestsPerMinute,
Burst: defaultRateLimitBurst,
RateLimitUI: RateLimitConfig{
Enabled: defaultUIRateLimitEnabled,
RequestsPerMinute: defaultUIRequestsPerMinute,
Burst: defaultUIRateLimitBurst,
},
RateLimitAPIRead: RateLimitConfig{
Enabled: defaultAPIReadRateLimitEnabled,
RequestsPerMinute: defaultAPIReadRequestsPerMinute,
Burst: defaultAPIReadRateLimitBurst,
},
RateLimitAPIWrite: RateLimitConfig{
Enabled: defaultAPIWriteRateLimitEnabled,
RequestsPerMinute: defaultAPIWriteRequestsPerMinute,
Burst: defaultAPIWriteRateLimitBurst,
},
},
}
@@ -182,6 +228,14 @@ func (c *Config) Validate() error {
if c.Server.MaxHeaderBytes <= 0 {
return errors.New("server.max_header_bytes must be > 0")
}
accessLogMaxSizeBytes, err := parseHumanSize(c.Server.AccessLog.MaxSize)
if err != nil {
return fmt.Errorf("server.access_log.max_size invalid: %w", err)
}
c.Server.AccessLog.MaxSizeBytes = accessLogMaxSizeBytes
if c.Server.AccessLog.MaxBackups <= 0 {
return errors.New("server.access_log.max_backups must be > 0")
}
sizeBytes, err := parseHumanSize(c.Limits.MaxUploadSize)
if err != nil {
@@ -222,6 +276,12 @@ func (c *Config) Validate() error {
if err := ensureWritableDir(c.Storage.DataDir); err != nil {
return fmt.Errorf("storage.data_dir not writable: %w", err)
}
if strings.TrimSpace(c.Storage.EncryptionKeyFile) == "" {
return errors.New("storage.encryption_key_file is required")
}
if len(c.Storage.EncryptionKeyBytes) != 32 {
return fmt.Errorf("storage.encryption_key_file must decode to 32 bytes, got %d", len(c.Storage.EncryptionKeyBytes))
}
prefixes, err := parseAllowedPrefixes(c.Security.AllowedIPs)
if err != nil {
@@ -237,11 +297,23 @@ func (c *Config) Validate() error {
return errors.New("security.trusted_proxy_ips is required when security.trust_proxy_headers is true")
}
if c.Security.RateLimit.RequestsPerMinute <= 0 {
return errors.New("security.rate_limit.requests_per_minute must be > 0")
if c.Security.RateLimitUI.RequestsPerMinute <= 0 {
return errors.New("security.rate_limit_ui.requests_per_minute must be > 0")
}
if c.Security.RateLimit.Burst <= 0 {
return errors.New("security.rate_limit.burst must be > 0")
if c.Security.RateLimitUI.Burst <= 0 {
return errors.New("security.rate_limit_ui.burst must be > 0")
}
if c.Security.RateLimitAPIRead.RequestsPerMinute <= 0 {
return errors.New("security.rate_limit_api_read.requests_per_minute must be > 0")
}
if c.Security.RateLimitAPIRead.Burst <= 0 {
return errors.New("security.rate_limit_api_read.burst must be > 0")
}
if c.Security.RateLimitAPIWrite.RequestsPerMinute <= 0 {
return errors.New("security.rate_limit_api_write.requests_per_minute must be > 0")
}
if c.Security.RateLimitAPIWrite.Burst <= 0 {
return errors.New("security.rate_limit_api_write.burst must be > 0")
}
return nil
@@ -338,3 +410,56 @@ func parseHumanSize(raw string) (int64, error) {
}
return size, nil
}
func GenerateEncryptionKey() (string, error) {
raw := make([]byte, 32)
if _, err := rand.Read(raw); err != nil {
return "", fmt.Errorf("generate encryption key bytes: %w", err)
}
return base64.StdEncoding.EncodeToString(raw), nil
}
func (c *Config) loadEncryptionKey(configPath string) error {
if configPath == "" {
return nil
}
keyPath := strings.TrimSpace(c.Storage.EncryptionKeyFile)
if keyPath == "" {
return errors.New("storage.encryption_key_file is required")
}
if !filepath.IsAbs(keyPath) {
keyPath = filepath.Join(filepath.Dir(configPath), keyPath)
}
keyRaw, err := os.ReadFile(keyPath)
if err != nil {
return fmt.Errorf("read storage.encryption_key_file %q: %w", keyPath, err)
}
encKey, err := decodeEncryptionKey(strings.TrimSpace(string(keyRaw)))
if err != nil {
return fmt.Errorf("storage.encryption_key_file invalid: %w", err)
}
c.Storage.EncryptionKeyBytes = encKey
return nil
}
func decodeEncryptionKey(raw string) ([]byte, error) {
if raw == "" {
return nil, errors.New("key is empty")
}
encKey, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, fmt.Errorf("invalid base64: %w", err)
}
if len(encKey) != 32 {
return nil, fmt.Errorf("must decode to 32 bytes, got %d", len(encKey))
}
return encKey, nil
}
func mustDecodeDefaultEncryptionKey() []byte {
encKey, err := decodeEncryptionKey(defaultStorageEncryptionKey)
if err != nil {
panic(fmt.Sprintf("invalid built-in encryption key: %v", err))
}
return encKey
}
+165 -11
View File
@@ -1,6 +1,7 @@
package config
import (
"encoding/base64"
"net/netip"
"os"
"path/filepath"
@@ -37,8 +38,17 @@ func TestValidateAppliesDefaultsAndParsedFields(t *testing.T) {
if cfg.Server.MaxHeaderBytes != 1<<20 {
t.Fatalf("MaxHeaderBytes = %d, want %d", cfg.Server.MaxHeaderBytes, 1<<20)
}
if cfg.Limits.DefaultTTLDuration != time.Hour {
t.Fatalf("DefaultTTLDuration = %s, want 1h", cfg.Limits.DefaultTTLDuration)
if !cfg.Server.AccessLog.Enabled {
t.Fatalf("AccessLog.Enabled = %v, want true", cfg.Server.AccessLog.Enabled)
}
if cfg.Server.AccessLog.MaxSizeBytes != 100*1024*1024 {
t.Fatalf("AccessLog.MaxSizeBytes = %d, want %d", cfg.Server.AccessLog.MaxSizeBytes, int64(100*1024*1024))
}
if cfg.Server.AccessLog.MaxBackups != 5 {
t.Fatalf("AccessLog.MaxBackups = %d, want 5", cfg.Server.AccessLog.MaxBackups)
}
if cfg.Limits.DefaultTTLDuration != 15*time.Minute {
t.Fatalf("DefaultTTLDuration = %s, want 15m", cfg.Limits.DefaultTTLDuration)
}
if cfg.Limits.RawCacheMaxBytes != 200*1024*1024 {
t.Fatalf("RawCacheMaxBytes = %d, want %d", cfg.Limits.RawCacheMaxBytes, int64(200*1024*1024))
@@ -49,6 +59,9 @@ func TestValidateAppliesDefaultsAndParsedFields(t *testing.T) {
if cfg.Storage.CleanupIntervalParsed != time.Minute {
t.Fatalf("CleanupIntervalParsed = %s, want 1m", cfg.Storage.CleanupIntervalParsed)
}
if len(cfg.Storage.EncryptionKeyBytes) != 32 {
t.Fatalf("EncryptionKeyBytes length = %d, want 32", len(cfg.Storage.EncryptionKeyBytes))
}
if len(cfg.Security.AllowedPrefixes) != 1 {
t.Fatalf("AllowedPrefixes len = %d, want 1", len(cfg.Security.AllowedPrefixes))
}
@@ -63,6 +76,7 @@ func TestLoadParsesConfiguredFields(t *testing.T) {
tmp := t.TempDir()
dataDir := filepath.Join(tmp, "store")
configPath := filepath.Join(tmp, "config.yaml")
keyPath := filepath.Join(tmp, "scratchbox.key")
content := `
server:
listen_addr: "127.0.0.1:9090"
@@ -71,6 +85,11 @@ server:
write_timeout: "35s"
idle_timeout: "90s"
max_header_bytes: 2097152
access_log:
enabled: true
file_path: "` + filepath.Join(tmp, "access.log") + `"
max_size: "2MiB"
max_backups: 7
limits:
max_upload_size: "2MiB"
default_ttl: "2h"
@@ -79,6 +98,7 @@ limits:
storage:
data_dir: "` + dataDir + `"
cleanup_interval: "45s"
encryption_key_file: "` + filepath.Base(keyPath) + `"
security:
allowed_ips:
- "127.0.0.1"
@@ -86,7 +106,15 @@ security:
trust_proxy_headers: true
trusted_proxy_ips:
- "10.0.0.0/8"
rate_limit:
rate_limit_ui:
enabled: true
requests_per_minute: 12
burst: 4
rate_limit_api_read:
enabled: true
requests_per_minute: 12
burst: 4
rate_limit_api_write:
enabled: true
requests_per_minute: 12
burst: 4
@@ -94,6 +122,9 @@ security:
if err := os.WriteFile(configPath, []byte(content), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
if err := os.WriteFile(keyPath, []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n"), 0o600); err != nil {
t.Fatalf("WriteFile(key) error = %v", err)
}
cfg, err := Load(configPath)
if err != nil {
@@ -118,6 +149,15 @@ security:
if cfg.Server.MaxHeaderBytes != 2*1024*1024 {
t.Fatalf("MaxHeaderBytes = %d, want %d", cfg.Server.MaxHeaderBytes, 2*1024*1024)
}
if !cfg.Server.AccessLog.Enabled {
t.Fatalf("AccessLog.Enabled = false, want true")
}
if cfg.Server.AccessLog.MaxSizeBytes != 2*1024*1024 {
t.Fatalf("AccessLog.MaxSizeBytes = %d, want %d", cfg.Server.AccessLog.MaxSizeBytes, int64(2*1024*1024))
}
if cfg.Server.AccessLog.MaxBackups != 7 {
t.Fatalf("AccessLog.MaxBackups = %d, want 7", cfg.Server.AccessLog.MaxBackups)
}
if cfg.Limits.MaxUploadSizeBytes != 2*1024*1024 {
t.Fatalf("MaxUploadSizeBytes = %d, want %d", cfg.Limits.MaxUploadSizeBytes, int64(2*1024*1024))
}
@@ -133,11 +173,23 @@ security:
if cfg.Storage.CleanupIntervalParsed != 45*time.Second {
t.Fatalf("CleanupIntervalParsed = %s, want 45s", cfg.Storage.CleanupIntervalParsed)
}
if len(cfg.Storage.EncryptionKeyBytes) != 32 {
t.Fatalf("EncryptionKeyBytes length = %d, want 32", len(cfg.Storage.EncryptionKeyBytes))
}
if cfg.Storage.EncryptionKeyBytes[0] != 0 {
t.Fatalf("EncryptionKeyBytes[0] = %d, want 0", cfg.Storage.EncryptionKeyBytes[0])
}
if !cfg.Security.TrustProxyHeaders {
t.Fatalf("TrustProxyHeaders = false, want true")
}
if cfg.Security.RateLimit.RequestsPerMinute != 12 || cfg.Security.RateLimit.Burst != 4 {
t.Fatalf("unexpected rate_limit values: %+v", cfg.Security.RateLimit)
if cfg.Security.RateLimitUI.RequestsPerMinute != 12 || cfg.Security.RateLimitUI.Burst != 4 {
t.Fatalf("unexpected rate_limit_ui values: %+v", cfg.Security.RateLimitUI)
}
if cfg.Security.RateLimitAPIRead.RequestsPerMinute != 12 || cfg.Security.RateLimitAPIRead.Burst != 4 {
t.Fatalf("unexpected rate_limit_api_read values: %+v", cfg.Security.RateLimitAPIRead)
}
if cfg.Security.RateLimitAPIWrite.RequestsPerMinute != 12 || cfg.Security.RateLimitAPIWrite.Burst != 4 {
t.Fatalf("unexpected rate_limit_api_write values: %+v", cfg.Security.RateLimitAPIWrite)
}
if len(cfg.Security.AllowedPrefixes) != 2 {
t.Fatalf("AllowedPrefixes len = %d, want 2", len(cfg.Security.AllowedPrefixes))
@@ -204,6 +256,20 @@ func TestValidateRejectsInvalidValues(t *testing.T) {
},
wantErr: "server.max_header_bytes",
},
{
name: "invalid access log max size",
mutate: func(cfg *Config) {
cfg.Server.AccessLog.MaxSize = "oops"
},
wantErr: "server.access_log.max_size",
},
{
name: "invalid access log max backups",
mutate: func(cfg *Config) {
cfg.Server.AccessLog.MaxBackups = 0
},
wantErr: "server.access_log.max_backups",
},
{
name: "invalid ttl parse",
mutate: func(cfg *Config) {
@@ -255,11 +321,25 @@ func TestValidateRejectsInvalidValues(t *testing.T) {
wantErr: "security.trusted_proxy_ips is required",
},
{
name: "invalid rate limit burst",
name: "invalid ui rate limit burst",
mutate: func(cfg *Config) {
cfg.Security.RateLimit.Burst = 0
cfg.Security.RateLimitUI.Burst = 0
},
wantErr: "security.rate_limit.burst",
wantErr: "security.rate_limit_ui.burst",
},
{
name: "invalid api read rate limit burst",
mutate: func(cfg *Config) {
cfg.Security.RateLimitAPIRead.Burst = 0
},
wantErr: "security.rate_limit_api_read.burst",
},
{
name: "invalid api write rate limit burst",
mutate: func(cfg *Config) {
cfg.Security.RateLimitAPIWrite.Burst = 0
},
wantErr: "security.rate_limit_api_write.burst",
},
{
name: "data dir required",
@@ -283,11 +363,39 @@ func TestValidateRejectsInvalidValues(t *testing.T) {
wantErr: "storage.cleanup_interval",
},
{
name: "invalid rate limit rpm",
name: "encryption key file required",
mutate: func(cfg *Config) {
cfg.Security.RateLimit.RequestsPerMinute = 0
cfg.Storage.EncryptionKeyFile = " "
},
wantErr: "security.rate_limit.requests_per_minute",
wantErr: "storage.encryption_key_file is required",
},
{
name: "encryption key bytes invalid length",
mutate: func(cfg *Config) {
cfg.Storage.EncryptionKeyBytes = []byte("short")
},
wantErr: "storage.encryption_key_file must decode to 32 bytes",
},
{
name: "invalid ui rate limit rpm",
mutate: func(cfg *Config) {
cfg.Security.RateLimitUI.RequestsPerMinute = 0
},
wantErr: "security.rate_limit_ui.requests_per_minute",
},
{
name: "invalid api read rate limit rpm",
mutate: func(cfg *Config) {
cfg.Security.RateLimitAPIRead.RequestsPerMinute = 0
},
wantErr: "security.rate_limit_api_read.requests_per_minute",
},
{
name: "invalid api write rate limit rpm",
mutate: func(cfg *Config) {
cfg.Security.RateLimitAPIWrite.RequestsPerMinute = 0
},
wantErr: "security.rate_limit_api_write.requests_per_minute",
},
}
@@ -328,6 +436,29 @@ func TestLoadErrors(t *testing.T) {
if err == nil || !strings.Contains(err.Error(), "parse config yaml") {
t.Fatalf("Load() error = %v, want yaml parse error", err)
}
validConfigPath := filepath.Join(tmp, "valid.yaml")
validConfig := `
storage:
data_dir: "` + filepath.Join(tmp, "store") + `"
cleanup_interval: "10s"
`
if err := os.WriteFile(validConfigPath, []byte(validConfig), 0o644); err != nil {
t.Fatalf("WriteFile(valid config) error = %v", err)
}
_, err = Load(validConfigPath)
if err == nil || !strings.Contains(err.Error(), "read storage.encryption_key_file") {
t.Fatalf("Load() error = %v, want missing key file error", err)
}
keyPath := filepath.Join(tmp, "config.key")
if err := os.WriteFile(keyPath, []byte("not-base64"), 0o600); err != nil {
t.Fatalf("WriteFile(invalid key) error = %v", err)
}
_, err = Load(validConfigPath)
if err == nil || !strings.Contains(err.Error(), "storage.encryption_key_file invalid: invalid base64") {
t.Fatalf("Load() error = %v, want invalid key base64 error", err)
}
}
func TestParseAllowedPrefixesTrimsAndParses(t *testing.T) {
@@ -385,3 +516,26 @@ func TestParseHumanSizeVariants(t *testing.T) {
})
}
}
func TestGenerateEncryptionKey(t *testing.T) {
t.Parallel()
k1, err := GenerateEncryptionKey()
if err != nil {
t.Fatalf("GenerateEncryptionKey() error = %v", err)
}
k2, err := GenerateEncryptionKey()
if err != nil {
t.Fatalf("GenerateEncryptionKey() second error = %v", err)
}
if k1 == k2 {
t.Fatalf("GenerateEncryptionKey() produced duplicate keys")
}
b1, err := base64.StdEncoding.DecodeString(k1)
if err != nil {
t.Fatalf("DecodeString(k1) error = %v", err)
}
if len(b1) != 32 {
t.Fatalf("decoded key length = %d, want 32", len(b1))
}
}