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:
@@ -1,6 +1,9 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
@@ -9,8 +12,8 @@ import (
|
||||
"git.wxccs.org/iceking2nd/winauth-go/internal/i18n"
|
||||
)
|
||||
|
||||
// preferencesDialog edits the persistent UI prefs: UI language and
|
||||
// theme. Auto-lock and minimize-to-tray live in their own bucket and
|
||||
// preferencesDialog edits the persistent UI prefs: UI language, theme,
|
||||
// and auto-lock timeout. Minimize-to-tray lives in its own bucket and
|
||||
// will appear here later.
|
||||
type preferencesDialog struct {
|
||||
lang string
|
||||
@@ -27,6 +30,11 @@ type preferencesDialog struct {
|
||||
langZhCNBt widget.Clickable
|
||||
langDeBt widget.Clickable
|
||||
|
||||
// Auto-lock timeout (whole minutes). 0 disables. Only takes effect
|
||||
// when the config is encrypted — the prefs dialog does not gate the
|
||||
// input on that, the store does.
|
||||
autoLockEd widget.Editor
|
||||
|
||||
okBt widget.Clickable
|
||||
cancelBt widget.Clickable
|
||||
}
|
||||
@@ -34,9 +42,10 @@ type preferencesDialog struct {
|
||||
// preferencesResult is what Layout's callback receives. cancel skips
|
||||
// any persistence.
|
||||
type preferencesResult struct {
|
||||
cancel bool
|
||||
language string
|
||||
theme themeMode
|
||||
cancel bool
|
||||
language string
|
||||
theme themeMode
|
||||
autoLockMinutes int
|
||||
}
|
||||
|
||||
// supportedLanguages is the closed set the prefs dialog exposes. Keep
|
||||
@@ -50,14 +59,17 @@ var supportedLanguages = []struct {
|
||||
{"de", "Deutsch"},
|
||||
}
|
||||
|
||||
func newPreferencesDialog(currentLang string, currentTheme themeMode) *preferencesDialog {
|
||||
func newPreferencesDialog(currentLang string, currentTheme themeMode, currentAutoLock int) *preferencesDialog {
|
||||
if currentLang == "" {
|
||||
currentLang = "en"
|
||||
}
|
||||
if currentTheme == "" {
|
||||
currentTheme = themeSystem
|
||||
}
|
||||
return &preferencesDialog{lang: currentLang, theme: currentTheme}
|
||||
d := &preferencesDialog{lang: currentLang, theme: currentTheme}
|
||||
d.autoLockEd.SingleLine = true
|
||||
d.autoLockEd.SetText(strconv.Itoa(currentAutoLock))
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *preferencesDialog) Layout(
|
||||
@@ -69,7 +81,14 @@ func (d *preferencesDialog) Layout(
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
if d.okBt.Clicked(gtx) {
|
||||
onDone(preferencesResult{language: d.lang, theme: d.theme})
|
||||
// Best-effort parse — non-numeric or negative input maps to 0
|
||||
// (disabled) rather than surfacing an error, since the field
|
||||
// is paired with a "0 disables" hint.
|
||||
mins, err := strconv.Atoi(strings.TrimSpace(d.autoLockEd.Text()))
|
||||
if err != nil || mins < 0 {
|
||||
mins = 0
|
||||
}
|
||||
onDone(preferencesResult{language: d.lang, theme: d.theme, autoLockMinutes: mins})
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
||||
|
||||
@@ -127,6 +146,10 @@ func (d *preferencesDialog) Layout(
|
||||
chip(&d.langDeBt, supportedLanguages[2].label, d.lang == "de"),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(labeledEditor(th, i18n.T("label_auto_lock_minutes"), &d.autoLockEd, "")),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(material.Caption(th, i18n.T("hint_auto_lock_disabled")).Layout),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user