Files
iceking2nd c671f2115e feat: Phase 1 WinAuth Go 移植完整实现
将原 C#/.NET WinAuth 移植为 Go + Gio GUI,覆盖 Phase 1 全部功能。

核心模块:
- internal/authenticator: TOTP (Google/Microsoft/Okta) + HOTP + BattleNet + Steam,含
  enroll/sync/code 生成、Steam 交易确认轮询
- internal/config: YAML 配置 + 老版 WinAuth XML 导入(DPAPI + Password + Blowfish/PBKDF2 解密链)
- internal/crypto: 现代加密 (WAGO1) + DPAPI 跨平台封装 + 老版 Blowfish ECB
- internal/win32: 单实例 Mutex 锁 + 全局热键管理器 (RegisterHotKey + PeekMessage 泵) +
  SendInput Unicode 注入 + 剪贴板文本/CF_DIB 图像读写 + AttachThreadInput 焦点切换
- internal/hotkey: "Ctrl+Alt+G" 风格快捷键字符串解析/格式化
- internal/qr: gozxing 二维码解码 + otpauth:// URI 解析
- internal/i18n: en/zh-CN/de 三语 TOML

UI 模块 (Gio):
- 主窗口:圆环倒计时进度条、复制按钮 + Toast 反馈、空列表占位、行分隔线
- 添加流程:vendor 菜单 + 各 vendor 独立对话框 + 二维码扫描入口(文件 / 剪贴板)
- 设置:密码加密、老版 XML 导入、每条目热键配置
- Steam:注册向导(含 captcha/email/SMS 多步)+ 交易确认窗

构建:Windows 主目标,非 Windows 平台所有 Win32 功能走 build-tag 桩实现。
2026-06-12 03:10:37 +08:00

134 lines
3.9 KiB
Go

package ui
import (
"errors"
"gioui.org/app"
"git.wxccs.org/iceking2nd/winauth-go/internal/global"
"git.wxccs.org/iceking2nd/winauth-go/internal/hotkey"
"git.wxccs.org/iceking2nd/winauth-go/internal/win32"
)
// registerAllHotkeys walks every entry and registers its configured
// hotkey. Failure on a single row is logged and skipped so the rest
// keep working. Called once at startup; applyHotkey handles incremental
// updates.
func (st *appState) registerAllHotkeys() {
const fn = "internal.ui.appState.registerAllHotkeys"
st.mu.Lock()
defer st.mu.Unlock()
for _, en := range st.entries {
if en.Hotkey == "" {
continue
}
h, err := hotkey.Parse(en.Hotkey)
if err != nil {
global.Log.WithField("func", fn).WithField("entry", en.Name).
WithError(err).Warn("bad hotkey string; skipping")
continue
}
id, err := st.hkMgr.Register(h)
if err != nil {
global.Log.WithField("func", fn).WithField("entry", en.Name).
WithField("hotkey", en.Hotkey).WithError(err).
Warn("hotkey registration failed (already taken?)")
continue
}
en.hotkeyID = id
}
}
// applyHotkey updates target.Hotkey to value (empty string clears),
// unregistering the old binding and registering the new one. Errors are
// surfaced via st.saveErr so the user sees them.
func (st *appState) applyHotkey(target *entry, value string) {
const fn = "internal.ui.appState.applyHotkey"
st.mu.Lock()
defer st.mu.Unlock()
if target.hotkeyID != 0 {
if err := st.hkMgr.Unregister(target.hotkeyID); err != nil {
global.Log.WithField("func", fn).WithError(err).
Warn("unregister old hotkey failed")
}
target.hotkeyID = 0
}
target.Hotkey = value
if value == "" {
return
}
h, err := hotkey.Parse(value)
if err != nil {
st.saveErr = err.Error()
target.Hotkey = ""
return
}
id, err := st.hkMgr.Register(h)
if err != nil {
st.saveErr = err.Error()
target.Hotkey = ""
return
}
target.hotkeyID = id
}
// runHotkeyLoop drains the manager's Events channel and triggers the
// Auto-type flow for whichever entry owns the fired ID. Runs as a
// daemon goroutine until the events channel closes (Stop).
func (st *appState) runHotkeyLoop(w *app.Window) {
const fn = "internal.ui.appState.runHotkeyLoop"
for ev := range st.hkMgr.Events() {
target := st.findEntryByHotkeyID(ev.ID)
if target == nil {
continue
}
code, err := target.Auth.CurrentCode()
if err != nil {
global.Log.WithField("func", fn).WithError(err).Warn("compute OTP failed")
continue
}
st.autoType(code)
w.Invalidate()
}
}
// findEntryByHotkeyID is a tiny lookup with the lock held just for the
// scan. Returns nil if no entry matches.
func (st *appState) findEntryByHotkeyID(id int32) *entry {
st.mu.Lock()
defer st.mu.Unlock()
for _, en := range st.entries {
if en.hotkeyID == id {
return en
}
}
return nil
}
// autoType pushes the OTP onto the clipboard and types it into the
// currently-foreground window. If GetForegroundWindow points at
// winauth-go itself (because the user was looking at it when the
// hotkey fired) we only copy — typing would inject into our own
// editor field which is almost never useful.
func (st *appState) autoType(code string) {
const fn = "internal.ui.appState.autoType"
if err := win32.SetClipboardText(code); err != nil {
if !errors.Is(err, win32.ErrUnsupported) {
global.Log.WithField("func", fn).WithError(err).Warn("clipboard write failed")
}
}
// We do not have a HWND for our own Gio window via the public API,
// so we cannot reliably detect "foreground is us." In practice the
// global hotkey almost always fires while another window is on top
// (that's the whole point), so we just inject blindly. The OTP is
// also on the clipboard as a safety net.
if err := win32.TypeUnicode(code); err != nil {
if !errors.Is(err, win32.ErrUnsupported) {
global.Log.WithField("func", fn).WithError(err).Warn("send input failed")
}
}
}