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 桩实现。
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user