112 lines
3.8 KiB
Go
112 lines
3.8 KiB
Go
// 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
|
||
}
|