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

204 lines
4.7 KiB
Go

//go:build windows
package win32
import (
"fmt"
"runtime"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
// HotkeyManager owns a dedicated OS thread that runs a GetMessageW
// pump. RegisterHotKey can only be called from the thread that will
// receive the messages, so all register/unregister/dispatch operations
// are serialized onto that goroutine via the cmd channel.
type HotkeyManager struct {
cmd chan hkCmd
events chan HotkeyEvent
mu sync.Mutex
nextID int32
stopped bool
}
type hkCmd struct {
kind hkCmdKind
id int32
hotkey Hotkey
reply chan error
}
type hkCmdKind int
const (
hkCmdRegister hkCmdKind = iota
hkCmdUnregister
hkCmdStop
)
// NewHotkeyManager starts the dedicated thread and returns a manager
// ready to accept Register calls. Events() yields presses; the channel
// is closed on Stop().
func NewHotkeyManager() *HotkeyManager {
m := &HotkeyManager{
cmd: make(chan hkCmd),
events: make(chan HotkeyEvent, 16),
nextID: 1,
}
started := make(chan struct{})
go m.run(started)
<-started
return m
}
// Events returns the read-only event channel.
func (m *HotkeyManager) Events() <-chan HotkeyEvent { return m.events }
// Register adds a global hotkey and returns its assigned id. Re-registering
// a combination that is already taken by another application returns an
// error; the caller should surface it to the user.
func (m *HotkeyManager) Register(h Hotkey) (int32, error) {
m.mu.Lock()
if m.stopped {
m.mu.Unlock()
return 0, fmt.Errorf("win32: hotkey manager stopped")
}
id := m.nextID
m.nextID++
m.mu.Unlock()
reply := make(chan error, 1)
m.cmd <- hkCmd{kind: hkCmdRegister, id: id, hotkey: h, reply: reply}
if err := <-reply; err != nil {
return 0, err
}
return id, nil
}
// Unregister removes a previously registered hotkey.
func (m *HotkeyManager) Unregister(id int32) error {
reply := make(chan error, 1)
m.cmd <- hkCmd{kind: hkCmdUnregister, id: id, reply: reply}
return <-reply
}
// Stop tears down the message pump and closes Events(). Subsequent
// Register calls fail.
func (m *HotkeyManager) Stop() {
m.mu.Lock()
if m.stopped {
m.mu.Unlock()
return
}
m.stopped = true
m.mu.Unlock()
reply := make(chan error, 1)
m.cmd <- hkCmd{kind: hkCmdStop, reply: reply}
<-reply
}
// run is the dedicated-thread loop. It owns the message queue that
// RegisterHotKey targets.
func (m *HotkeyManager) run(started chan struct{}) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Force the message queue to exist before anybody tries to post to
// us. PeekMessage with PM_NOREMOVE is the canonical incantation.
var msg msgStruct
procPeekMessageW.Call(
uintptr(unsafe.Pointer(&msg)),
0, 0, 0, 0, // PM_NOREMOVE
)
close(started)
for {
// Non-blocking message pump: drain hotkey messages first, then
// service one cmd, then sleep briefly. A blocking GetMessage
// would freeze the cmd intake.
for {
r, _, _ := procPeekMessageW.Call(
uintptr(unsafe.Pointer(&msg)),
0, 0, 0, 1, // PM_REMOVE
)
if r == 0 {
break
}
if msg.message == wmHotKey {
select {
case m.events <- HotkeyEvent{ID: int32(msg.wParam)}:
default:
// Listener slow — drop to avoid stalling the pump.
}
}
}
select {
case c := <-m.cmd:
switch c.kind {
case hkCmdRegister:
err := registerHotKey(0, c.id, c.hotkey.Mods, c.hotkey.VK)
c.reply <- err
case hkCmdUnregister:
err := unregisterHotKey(0, c.id)
c.reply <- err
case hkCmdStop:
close(m.events)
c.reply <- nil
return
}
default:
// brief sleep so we don't busy-loop. 30ms is well under any
// human-perceptible hotkey latency.
windows.SleepEx(30, false)
}
}
}
// -----------------------------------------------------------------------------
// raw syscalls
var (
procRegisterHotKey = user32.NewProc("RegisterHotKey")
procUnregisterHotKey = user32.NewProc("UnregisterHotKey")
procPeekMessageW = user32.NewProc("PeekMessageW")
)
const wmHotKey uint32 = 0x0312
type msgStruct struct {
hwnd uintptr
message uint32
wParam uintptr
lParam uintptr
time uint32
pt struct{ x, y int32 }
}
func registerHotKey(hwnd uintptr, id int32, mods, vk uint32) error {
r, _, e := procRegisterHotKey.Call(
hwnd, uintptr(id), uintptr(mods), uintptr(vk),
)
if r == 0 {
if errno, ok := e.(syscall.Errno); ok && errno != 0 {
return fmt.Errorf("win32: RegisterHotKey: %w", errno)
}
return fmt.Errorf("win32: RegisterHotKey: unknown failure")
}
return nil
}
func unregisterHotKey(hwnd uintptr, id int32) error {
r, _, e := procUnregisterHotKey.Call(hwnd, uintptr(id))
if r == 0 {
if errno, ok := e.(syscall.Errno); ok && errno != 0 {
return fmt.Errorf("win32: UnregisterHotKey: %w", errno)
}
}
return nil
}