Files
EveryPublish/client/core/internal/vault/vault.go
T

140 lines
3.3 KiB
Go
Raw 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 vault
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"os"
"path/filepath"
)
// Vault 凭据保险库:AES-256-GCM 加密的 JSON 文件(主密钥单独 0600 文件)。
// 二期:Windows 上主密钥改用 DPAPI 保护,文件格式不变。
type Vault struct {
dir string
key []byte
}
type vaultFile struct {
Version int `json:"version"`
Entries map[string]string `json:"entries"`
}
// Open 打开/创建保险库(主密钥 data/master.key,32 字节)
func Open(dir string) (*Vault, error) {
if err := os.MkdirAll(dir, 0o700); err != nil {
return nil, err
}
keyPath := filepath.Join(dir, "master.key")
var key []byte
if raw, err := os.ReadFile(keyPath); err == nil {
key = make([]byte, base64.RawStdEncoding.DecodedLen(len(raw)))
if _, err = base64.RawStdEncoding.Decode(key, raw); err != nil || len(key) != 32 {
return nil, errors.New("master key corrupted")
}
} else {
key = make([]byte, 32)
if _, err = rand.Read(key); err != nil {
return nil, err
}
if err = os.WriteFile(keyPath, []byte(base64.RawStdEncoding.EncodeToString(key)), 0o600); err != nil {
return nil, err
}
}
return &Vault{dir: dir, key: key}, nil
}
func (v *Vault) path() string { return filepath.Join(v.dir, "vault.enc") }
// load 读取并解密全部条目
func (v *Vault) load() (map[string]string, error) {
raw, err := os.ReadFile(v.path())
if err != nil {
if os.IsNotExist(err) {
return map[string]string{}, nil
}
return nil, err
}
var f vaultFile
if err = json.Unmarshal(raw, &f); err != nil {
return nil, err
}
if f.Entries == nil {
f.Entries = map[string]string{}
}
return f.Entries, nil
}
// Save 加密保存单条凭据(cookie/token 等,值为 JSON 字符串)
func (v *Vault) Save(accountID string, plaintext []byte) error {
entries, err := v.load()
if err != nil {
return err
}
block, err := aes.NewCipher(v.key)
if err != nil {
return err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = rand.Read(nonce); err != nil {
return err
}
sealed := gcm.Seal(nonce, nonce, plaintext, nil)
entries[accountID] = base64.StdEncoding.EncodeToString(sealed)
return v.persist(entries)
}
// Load 解密读取单条凭据
func (v *Vault) Load(accountID string) ([]byte, error) {
entries, err := v.load()
if err != nil {
return nil, err
}
sealedB64, ok := entries[accountID]
if !ok {
return nil, errors.New("entry not found")
}
sealed, err := base64.StdEncoding.DecodeString(sealedB64)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(v.key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if len(sealed) < gcm.NonceSize() {
return nil, errors.New("entry corrupted")
}
nonce, ciphertext := sealed[:gcm.NonceSize()], sealed[gcm.NonceSize():]
return gcm.Open(nil, nonce, ciphertext, nil)
}
// Delete 删除单条凭据
func (v *Vault) Delete(accountID string) error {
entries, err := v.load()
if err != nil {
return err
}
delete(entries, accountID)
return v.persist(entries)
}
func (v *Vault) persist(entries map[string]string) error {
raw, err := json.MarshalIndent(vaultFile{Version: 1, Entries: entries}, "", " ")
if err != nil {
return err
}
return os.WriteFile(v.path(), raw, 0o600)
}