0918fd446a
- 修改密码对话框引入旧密码校验:通过 store.VerifyPassword 走 constant-time 比较,对话框自身不持有 live passphrase 副本。 - 配置加密且 auto_lock_minutes>0 时启用自动锁定;明文配置不受影响。 锁定时刻意保留 store.passphrase,避免解锁瞬间出现保存竞态——进程内存 无论如何都是威胁边界。 - HOTP 点击后 toast 仅显示推进后的计数器值,绝不携带验证码字符串。 - SaveYAML 每次写入前将旧文件轮转为 .bak(0o600),防止加密/序列化失败 导致用户无可恢复副本。 - Preferences 对话框新增"自动锁定(分钟)"输入项。
133 lines
3.6 KiB
Go
133 lines
3.6 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"git.wxccs.org/iceking2nd/winauth-go/internal/crypto"
|
|
"git.wxccs.org/iceking2nd/winauth-go/internal/global"
|
|
)
|
|
|
|
// Sentinel errors returned by LoadYAML for the encrypted-config password
|
|
// path. Callers use errors.Is to distinguish them from generic I/O / parse
|
|
// failures.
|
|
var (
|
|
ErrPasswordRequired = errors.New("config: passphrase required")
|
|
ErrPasswordWrong = errors.New("config: wrong passphrase")
|
|
)
|
|
|
|
// SaveYAML writes the receiver as YAML to path. If passphrase is non-empty
|
|
// and cfg.Encrypted is true, the entries slice is serialized to JSON,
|
|
// encrypted, and stored as EncryptedBlob — entries are NOT written in
|
|
// plaintext in that case.
|
|
func SaveYAML(cfg *Config, path string, passphrase []byte) error {
|
|
const fn = "internal.config.SaveYAML"
|
|
logger := global.Log.WithField("func", fn).WithField("path", path)
|
|
|
|
out := *cfg
|
|
if cfg.Encrypted && len(passphrase) > 0 {
|
|
raw, err := json.Marshal(cfg.Entries)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
blob, err := crypto.EncryptModern(raw, passphrase)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out.EncryptedBlob = blob
|
|
out.Entries = nil
|
|
}
|
|
|
|
data, err := yaml.Marshal(&out)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := EnsureDir(path); err != nil {
|
|
return err
|
|
}
|
|
// Best-effort .bak rotation: if a previous file exists, copy it to
|
|
// path+".bak" before we overwrite, so a botched encrypt / serialize
|
|
// does not leave the user with no recoverable copy. Failure to back
|
|
// up is logged but does not block the save — the data is more
|
|
// important than the safety net.
|
|
if err := backupIfExists(path); err != nil {
|
|
logger.WithError(err).Warn("backup before save failed")
|
|
}
|
|
tmp := path + ".tmp"
|
|
if err := os.WriteFile(tmp, data, 0o600); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(tmp, path); err != nil {
|
|
_ = os.Remove(tmp)
|
|
return err
|
|
}
|
|
logger.Debug("config saved")
|
|
return nil
|
|
}
|
|
|
|
// backupIfExists copies path to path+".bak" with 0o600 permissions when
|
|
// path exists, atomically overwriting any previous .bak. Returns nil if
|
|
// path does not exist (first save).
|
|
func backupIfExists(path string) error {
|
|
src, err := os.Open(path)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
bakTmp := path + ".bak.tmp"
|
|
dst, err := os.OpenFile(bakTmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(dst, src); err != nil {
|
|
dst.Close()
|
|
_ = os.Remove(bakTmp)
|
|
return err
|
|
}
|
|
if err := dst.Close(); err != nil {
|
|
_ = os.Remove(bakTmp)
|
|
return err
|
|
}
|
|
return os.Rename(bakTmp, path+".bak")
|
|
}
|
|
|
|
// LoadYAML reads the YAML config at path. If the file is encrypted,
|
|
// passphrase is required and the EncryptedBlob is decrypted into Entries.
|
|
func LoadYAML(path string, passphrase []byte) (*Config, error) {
|
|
const fn = "internal.config.LoadYAML"
|
|
logger := global.Log.WithField("func", fn).WithField("path", path)
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return nil, fmt.Errorf("config: parse YAML: %w", err)
|
|
}
|
|
|
|
if cfg.Encrypted && cfg.EncryptedBlob != "" {
|
|
if len(passphrase) == 0 {
|
|
return &cfg, ErrPasswordRequired
|
|
}
|
|
raw, err := crypto.DecryptModern(cfg.EncryptedBlob, passphrase)
|
|
if err != nil {
|
|
return &cfg, ErrPasswordWrong
|
|
}
|
|
if err := json.Unmarshal(raw, &cfg.Entries); err != nil {
|
|
return nil, fmt.Errorf("config: decode entries: %w", err)
|
|
}
|
|
}
|
|
|
|
logger.WithField("entries", len(cfg.Entries)).Debug("config loaded")
|
|
return &cfg, nil
|
|
}
|