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 桩实现。
204 lines
4.7 KiB
Go
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
|
|
}
|