// Package credentialvault 加密保存管理员可重新查看的当前业务 Token。 // 主密钥来自独立文件,不进入数据库、日志或 API 响应。 package credentialvault import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "encoding/json" "errors" "io" "os" "regexp" core "git.yuntuo.hk/k2safe/orbit/model" ) // Keyring 为不可变密钥环;active 用于新写入,历史密钥用于解密既有记录。 type Keyring struct { active string keys map[string]cipher.AEAD } func (*Keyring) String() string { return "credentialvault.Keyring{REDACTED}" } func (k *Keyring) GoString() string { return k.String() } // LoadFile 读取最多 64 KiB 的 JSON 密钥文件。空路径表示未启用密文保管。 func LoadFile(path string) (*Keyring, error) { if path == "" { return nil, nil } f, err := os.Open(path) if err != nil { return nil, errors.New("cannot open credential key file") } defer f.Close() return Read(io.LimitReader(f, 65537)) } // Read 校验 active 和 keys;每个 Base64 密钥必须恰好为 32 字节。 func Read(r io.Reader) (*Keyring, error) { var config struct { Active string `json:"active"` Keys map[string]string `json:"keys"` } raw, err := io.ReadAll(io.LimitReader(r, 65537)) if err != nil || len(raw) > 65536 { return nil, errors.New("invalid credential key file") } if err = json.Unmarshal(raw, &config); err != nil || len(config.Keys) == 0 || len(config.Keys) > 16 { return nil, errors.New("invalid credential key file") } out := &Keyring{active: config.Active, keys: make(map[string]cipher.AEAD)} for id, encoded := range config.Keys { key, err := base64.StdEncoding.Strict().DecodeString(encoded) if err != nil || len(key) != 32 || !regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`).MatchString(id) { return nil, errors.New("invalid credential encryption key") } block, err := aes.NewCipher(key) if err != nil { return nil, errors.New("invalid credential encryption key") } aead, err := cipher.NewGCM(block) if err != nil { return nil, errors.New("cannot initialize credential encryption") } out.keys[id] = aead } if out.keys[out.active] == nil { return nil, errors.New("active credential encryption key is missing") } return out, nil } func aad(namespace, credential core.ID) []byte { return []byte("orbit-credential-v1:" + string(namespace) + ":" + string(credential)) } // Seal 返回密钥标识和 nonce+ciphertext。未启用时两者均为空,兼容只存摘要的管理 CLI。 func (k *Keyring) Seal(namespace, credential core.ID, token string) (*string, []byte, error) { if k == nil { return nil, nil, nil } if namespace.Validate() != nil || credential.Validate() != nil || len(token) == 0 || len(token) > 512 { return nil, nil, errors.New("invalid credential encryption input") } aead := k.keys[k.active] nonce := make([]byte, aead.NonceSize()) if _, err := rand.Read(nonce); err != nil { return nil, nil, errors.New("cannot generate credential encryption nonce") } id := k.active return &id, aead.Seal(nonce, nonce, []byte(token), aad(namespace, credential)), nil } // Open 只解密指定凭据;调用方必须先认证管理员、校验版本,再核对当前摘要并记录审计。 func (k *Keyring) Open(namespace, credential core.ID, keyID string, ciphertext []byte) (string, error) { if k == nil { return "", errors.New("credential encryption is not configured") } aead := k.keys[keyID] if aead == nil || len(ciphertext) < aead.NonceSize()+aead.Overhead() || len(ciphertext) > 1024 { return "", errors.New("credential ciphertext cannot be decrypted") } raw, err := aead.Open(nil, ciphertext[:aead.NonceSize()], ciphertext[aead.NonceSize():], aad(namespace, credential)) if err != nil { return "", errors.New("credential ciphertext cannot be decrypted") } return string(raw), nil }