feat(ui): 安全增强 — 修改密码 / 自动锁定 / HOTP 提示 / 配置备份
- 修改密码对话框引入旧密码校验:通过 store.VerifyPassword 走 constant-time 比较,对话框自身不持有 live passphrase 副本。 - 配置加密且 auto_lock_minutes>0 时启用自动锁定;明文配置不受影响。 锁定时刻意保留 store.passphrase,避免解锁瞬间出现保存竞态——进程内存 无论如何都是威胁边界。 - HOTP 点击后 toast 仅显示推进后的计数器值,绝不携带验证码字符串。 - SaveYAML 每次写入前将旧文件轮转为 .bak(0o600),防止加密/序列化失败 导致用户无可恢复副本。 - Preferences 对话框新增"自动锁定(分钟)"输入项。
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -49,6 +50,14 @@ func SaveYAML(cfg *Config, path string, passphrase []byte) error {
|
||||
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
|
||||
@@ -61,6 +70,35 @@ func SaveYAML(cfg *Config, path string, passphrase []byte) error {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user