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) }