Files

112 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}