fix(config): redact MasterKey type; drop os.Clearenv; add hex validation test

This commit is contained in:
2026-04-28 13:44:06 +08:00
parent 26d6c72c33
commit baf296abfb
2 changed files with 40 additions and 4 deletions
+19 -2
View File
@@ -4,16 +4,33 @@ package config
import ( import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"log/slog"
"strings" "strings"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// MasterKey wraps a 32-byte AES master key with redacted formatting to prevent
// accidental log/fmt exposure.
type MasterKey []byte
// String redacts when used with %s / fmt.Println.
func (k MasterKey) String() string { return "[REDACTED 32B]" }
// GoString redacts when used with %#v.
func (k MasterKey) GoString() string { return "[REDACTED 32B]" }
// MarshalJSON redacts when serialized to JSON.
func (k MasterKey) MarshalJSON() ([]byte, error) { return []byte(`"[REDACTED]"`), nil }
// LogValue redacts when logged via slog.
func (k MasterKey) LogValue() slog.Value { return slog.StringValue("[REDACTED 32B]") }
// Config 是应用全局配置的根结构。 // Config 是应用全局配置的根结构。
type Config struct { type Config struct {
Env string `mapstructure:"env"` Env string `mapstructure:"env"`
DataDir string `mapstructure:"data_dir"` DataDir string `mapstructure:"data_dir"`
MasterKey []byte // 解码后的 32 字节 MasterKey MasterKey `mapstructure:"-"` // 解码后的 32 字节,单独从 master_key 解析
HTTP HTTPConfig `mapstructure:"http"` HTTP HTTPConfig `mapstructure:"http"`
DB DBConfig `mapstructure:"db"` DB DBConfig `mapstructure:"db"`
Bootstrap BootstrapConfig `mapstructure:"bootstrap"` Bootstrap BootstrapConfig `mapstructure:"bootstrap"`
@@ -86,7 +103,7 @@ func Load(configFile string) (*Config, error) {
if len(key) != 32 { if len(key) != 32 {
return nil, fmt.Errorf("APP_MASTER_KEY must be 32 bytes (got %d)", len(key)) return nil, fmt.Errorf("APP_MASTER_KEY must be 32 bytes (got %d)", len(key))
} }
cfg.MasterKey = key cfg.MasterKey = MasterKey(key)
if cfg.DB.DSN == "" { if cfg.DB.DSN == "" {
return nil, fmt.Errorf("APP_DB_DSN required") return nil, fmt.Errorf("APP_DB_DSN required")
+21 -2
View File
@@ -1,7 +1,8 @@
package config package config
import ( import (
"os" "encoding/json"
"fmt"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -27,8 +28,8 @@ func TestLoad_FromEnv(t *testing.T) {
} }
func TestLoad_MissingMasterKey(t *testing.T) { func TestLoad_MissingMasterKey(t *testing.T) {
os.Clearenv()
t.Setenv("APP_DB_DSN", "x") t.Setenv("APP_DB_DSN", "x")
t.Setenv("APP_MASTER_KEY", "") // explicitly empty so t.Setenv cleans up
_, err := Load("") _, err := Load("")
require.Error(t, err) require.Error(t, err)
require.Contains(t, err.Error(), "MASTER_KEY") require.Contains(t, err.Error(), "MASTER_KEY")
@@ -40,3 +41,21 @@ func TestLoad_MasterKeyWrongLength(t *testing.T) {
_, err := Load("") _, err := Load("")
require.Error(t, err) require.Error(t, err)
} }
func TestLoad_MasterKeyNotHex(t *testing.T) {
t.Setenv("APP_DB_DSN", "x")
t.Setenv("APP_MASTER_KEY", "zz") // not valid hex
_, err := Load("")
require.Error(t, err)
require.Contains(t, err.Error(), "invalid hex")
}
func TestMasterKey_Redaction(t *testing.T) {
k := MasterKey([]byte{0x01, 0x02, 0x03})
require.Equal(t, "[REDACTED 32B]", k.String())
require.Equal(t, "[REDACTED 32B]", fmt.Sprintf("%v", k))
require.NotContains(t, fmt.Sprintf("%v", k), "01")
b, err := json.Marshal(k)
require.NoError(t, err)
require.Equal(t, `"[REDACTED]"`, string(b))
}