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,214 @@
|
||||
//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}
|
||||
}
|
||||
Reference in New Issue
Block a user