c671f2115e
将原 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 桩实现。
134 lines
3.9 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|