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

215 lines
6.5 KiB
Go

//go:build windows
package win32
import (
"fmt"
"syscall"
"time"
"unicode/utf16"
"unsafe"
"golang.org/x/sys/windows"
)
// SetClipboardText copies s onto the Windows clipboard as CF_UNICODETEXT.
// OpenClipboard may transiently fail if another process holds the
// clipboard; we retry a few times before giving up.
func SetClipboardText(s string) error {
const cfUnicodeText = 13
utf := utf16.Encode([]rune(s + "\x00"))
size := len(utf) * 2
hMem, _, e := procGlobalAlloc.Call(0x0042 /*GMEM_MOVEABLE|GMEM_ZEROINIT*/, uintptr(size))
if hMem == 0 {
return fmt.Errorf("win32: GlobalAlloc: %w", e)
}
dst, _, _ := procGlobalLock.Call(hMem)
if dst == 0 {
procGlobalFree.Call(hMem)
return fmt.Errorf("win32: GlobalLock failed")
}
dstSlice := unsafe.Slice((*uint16)(unsafe.Pointer(dst)), len(utf))
copy(dstSlice, utf)
procGlobalUnlock.Call(hMem)
if err := openClipboardRetry(0); err != nil {
procGlobalFree.Call(hMem)
return err
}
procEmptyClipboard.Call()
r, _, ce := procSetClipboardData.Call(cfUnicodeText, hMem)
if r == 0 {
procCloseClipboard.Call()
procGlobalFree.Call(hMem)
return fmt.Errorf("win32: SetClipboardData: %w", ce)
}
// Ownership of hMem transfers to the system on success — do not free.
procCloseClipboard.Call()
return nil
}
func openClipboardRetry(hwnd uintptr) error {
var last error
for i := 0; i < 8; i++ {
r, _, e := procOpenClipboard.Call(hwnd)
if r != 0 {
return nil
}
last = e
time.Sleep(20 * time.Millisecond)
}
return fmt.Errorf("win32: OpenClipboard: %w", last)
}
// GetForegroundWindow returns the HWND that currently has keyboard
// focus. Use this to remember the target window before the user clicks
// into winauth-go (which itself becomes foreground and would otherwise
// receive injected keystrokes).
func GetForegroundWindow() uintptr {
r, _, _ := procGetForegroundWindow.Call()
return r
}
// FocusWindow restores hwnd to the foreground. Hits the well-known
// SetForegroundWindow restriction (only the foreground process may
// hand focus to another); we work around it by attaching to the target
// thread's input queue briefly, the trick documented in MSDN's
// "AttachThreadInput" page.
func FocusWindow(hwnd uintptr) error {
if hwnd == 0 {
return fmt.Errorf("win32: FocusWindow: nil hwnd")
}
curTID, _, _ := procGetCurrentThreadId.Call()
targetTID, _, _ := procGetWindowThreadProcessId.Call(hwnd, 0)
if targetTID == 0 {
return fmt.Errorf("win32: GetWindowThreadProcessId failed")
}
if curTID != targetTID {
procAttachThreadInput.Call(curTID, targetTID, 1)
defer procAttachThreadInput.Call(curTID, targetTID, 0)
}
procSetForegroundWindowProc.Call(hwnd)
procShowWindowProc.Call(hwnd, 9 /*SW_RESTORE*/)
procBringWindowToTop.Call(hwnd)
return nil
}
// TypeUnicode injects s as Unicode characters using SendInput KEYEVENTF_UNICODE.
// Special characters in s pass through transparently; no translation
// of newlines / tabs happens. If you need an Enter at the end, pass
// "\n" and let the caller append it explicitly.
func TypeUnicode(s string) error {
if s == "" {
return nil
}
utf := utf16.Encode([]rune(s))
// Each rune becomes 2 inputs (keydown + keyup).
inputs := make([]inputUnion, 0, len(utf)*2)
for _, u := range utf {
inputs = append(inputs,
makeUnicodeInput(u, false),
makeUnicodeInput(u, true),
)
}
r, _, e := procSendInput.Call(
uintptr(len(inputs)),
uintptr(unsafe.Pointer(&inputs[0])),
unsafe.Sizeof(inputs[0]),
)
if int(r) != len(inputs) {
if errno, ok := e.(syscall.Errno); ok && errno != 0 {
return fmt.Errorf("win32: SendInput: sent %d/%d: %w", r, len(inputs), errno)
}
return fmt.Errorf("win32: SendInput: sent %d/%d", r, len(inputs))
}
return nil
}
// PressKey injects a single virtual-key down+up pair (e.g. VK_RETURN).
func PressKey(vk uint16) error {
inputs := [2]inputUnion{
makeVKInput(vk, false),
makeVKInput(vk, true),
}
r, _, e := procSendInput.Call(
uintptr(len(inputs)),
uintptr(unsafe.Pointer(&inputs[0])),
unsafe.Sizeof(inputs[0]),
)
if int(r) != len(inputs) {
if errno, ok := e.(syscall.Errno); ok && errno != 0 {
return fmt.Errorf("win32: SendInput (vk): %w", errno)
}
return fmt.Errorf("win32: SendInput (vk): short send")
}
return nil
}
// VK_RETURN is exposed for callers that want to press Enter after Auto-type.
const VK_RETURN uint16 = 0x0D
// -----------------------------------------------------------------------------
// raw syscalls + structs
var (
procOpenClipboard = user32.NewProc("OpenClipboard")
procCloseClipboard = user32.NewProc("CloseClipboard")
procEmptyClipboard = user32.NewProc("EmptyClipboard")
procSetClipboardData = user32.NewProc("SetClipboardData")
procGetClipboardData = user32.NewProc("GetClipboardData")
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
procSetForegroundWindowProc = user32.NewProc("SetForegroundWindow")
procShowWindowProc = user32.NewProc("ShowWindow")
procBringWindowToTop = user32.NewProc("BringWindowToTop")
procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
procAttachThreadInput = user32.NewProc("AttachThreadInput")
procSendInput = user32.NewProc("SendInput")
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procGlobalAlloc = kernel32.NewProc("GlobalAlloc")
procGlobalFree = kernel32.NewProc("GlobalFree")
procGlobalLock = kernel32.NewProc("GlobalLock")
procGlobalUnlock = kernel32.NewProc("GlobalUnlock")
procGlobalSize = kernel32.NewProc("GlobalSize")
procGetCurrentThreadId = kernel32.NewProc("GetCurrentThreadId")
)
// inputUnion is the Win32 INPUT structure, sized for KEYBDINPUT (the
// largest variant on x64 is MOUSEINPUT but KEYBDINPUT padded to 40
// works because INPUT_KEYBOARD never reads the extra trailing bytes).
type inputUnion struct {
typ uint32
_pad uint32 // alignment on 64-bit
wVk uint16
wScan uint16
dwFlags uint32
time uint32
dwExtra uintptr
// 8 bytes of MOUSEINPUT-sized padding so the layout is large enough
// for the union on amd64.
_padTail [8]byte
}
const (
inputKeyboard uint32 = 1
keyeventfKeyUp uint32 = 0x0002
keyeventfUnicode uint32 = 0x0004
)
func makeUnicodeInput(r uint16, up bool) inputUnion {
flags := keyeventfUnicode
if up {
flags |= keyeventfKeyUp
}
return inputUnion{typ: inputKeyboard, wScan: r, dwFlags: flags}
}
func makeVKInput(vk uint16, up bool) inputUnion {
var flags uint32
if up {
flags = keyeventfKeyUp
}
return inputUnion{typ: inputKeyboard, wVk: vk, dwFlags: flags}
}