Files
yml2213 ef36453694 feat(dcauth): 移除机器码,改用纯设备码认证
- DCAuth 协议升级到 v2,签名原文只含设备码;客户端 2.0.0,旧版本返回 426
- 删除客户端机器码采集、后端 machine_code_* 字段与日志、后台机器码展示
- 102 迁移在生产执行前直接改为无机器码结构;SQL 守卫仅放行该文件
- Mock 短信不启用每日限额;修复慢电脑首次启动的协议转发竞态
- 新增 macOS DCAuth.app 打包脚本,注册 dcauth:// 协议,浏览器可直接唤起
2026-10-21 20:46:25 +08:00

647 lines
16 KiB
Go

//go:build windows
package main
import (
"fmt"
"runtime"
"syscall"
"unicode/utf16"
"unsafe"
)
const (
mbOK = 0x00000000
mbOKCancel = 0x00000001
mbIconError = 0x00000010
mbIconQuestion = 0x00000020
mbIconInformation = 0x00000040
mbSetForeground = 0x00010000
mbTopMost = 0x00040000
idOK = 1
)
const (
cfUnicodeText = 13
gmemMoveable = 0x0002
)
const (
wmNull = 0x0000
wmDestroy = 0x0002
wmClose = 0x0010
wmCopyData = 0x004A
wmCommand = 0x0111
wmLButtonUp = 0x0202
wmLButtonDblClk = 0x0203
wmRButtonUp = 0x0205
wmContextMenu = 0x007B
wmApp = 0x8000
)
const (
trayCallbackMessage = wmApp + 1
showWindowMessage = wmApp + 2
copyDataMagic = 0x44434131
)
const (
trayMenuDeviceInfo = 1001
trayMenuExit = 1002
)
const (
nimAdd = 0x00000000
nimModify = 0x00000001
nimDelete = 0x00000002
)
const (
nifMessage = 0x00000001
nifIcon = 0x00000002
nifTip = 0x00000004
nifInfo = 0x00000010
)
const (
niifInfo = 0x00000001
)
const (
mfString = 0x00000000
mfGrayed = 0x00000001
mfSeparator = 0x00000800
tpmRight = 0x00000002
tpmReturn = 0x00000100
)
const (
errorAlreadyExists = syscall.Errno(183)
errorClassAlreadyReg = syscall.Errno(1410)
)
const (
windowClassName = "DCAuthHiddenWindow"
windowTitle = "DCAuth"
mutexName = `Local\DCAuth.OrderSite`
authorizationMutexName = `Local\DCAuth.OrderSite.Authorization`
)
type point struct {
X int32
Y int32
}
type message struct {
HWnd uintptr
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt point
Private uint32
}
type wndClassEx struct {
CbSize uint32
Style uint32
LpfnWndProc uintptr
CbClsExtra int32
CbWndExtra int32
HInstance uintptr
HIcon uintptr
HCursor uintptr
HbrBackground uintptr
LpszMenuName *uint16
LpszClassName *uint16
HIconSm uintptr
}
type copyDataStruct struct {
DwData uintptr
CbData uint32
LpData uintptr
}
type guid struct {
Data1 uint32
Data2 uint16
Data3 uint16
Data4 [8]byte
}
type notifyIconData struct {
CbSize uint32
HWnd uintptr
UID uint32
UFlags uint32
UCallbackMessage uint32
HIcon uintptr
SzTip [128]uint16
DwState uint32
DwStateMask uint32
SzInfo [256]uint16
UVersion uint32
SzInfoTitle [64]uint16
DwInfoFlags uint32
GuidItem guid
HBalloonIcon uintptr
}
var (
user32DLL = syscall.NewLazyDLL("user32.dll")
shell32DLL = syscall.NewLazyDLL("shell32.dll")
messageBoxW = user32DLL.NewProc("MessageBoxW")
openClip = user32DLL.NewProc("OpenClipboard")
closeClip = user32DLL.NewProc("CloseClipboard")
emptyClip = user32DLL.NewProc("EmptyClipboard")
setClipData = user32DLL.NewProc("SetClipboardData")
registerClassExW = user32DLL.NewProc("RegisterClassExW")
createWindowExW = user32DLL.NewProc("CreateWindowExW")
defWindowProcW = user32DLL.NewProc("DefWindowProcW")
getMessageW = user32DLL.NewProc("GetMessageW")
translateMessage = user32DLL.NewProc("TranslateMessage")
dispatchMessageW = user32DLL.NewProc("DispatchMessageW")
postQuitMessage = user32DLL.NewProc("PostQuitMessage")
destroyWindow = user32DLL.NewProc("DestroyWindow")
findWindowW = user32DLL.NewProc("FindWindowW")
sendMessageW = user32DLL.NewProc("SendMessageW")
postMessageW = user32DLL.NewProc("PostMessageW")
createPopupMenu = user32DLL.NewProc("CreatePopupMenu")
appendMenuW = user32DLL.NewProc("AppendMenuW")
trackPopupMenu = user32DLL.NewProc("TrackPopupMenu")
destroyMenu = user32DLL.NewProc("DestroyMenu")
setForegroundWindow = user32DLL.NewProc("SetForegroundWindow")
getCursorPos = user32DLL.NewProc("GetCursorPos")
loadIconW = user32DLL.NewProc("LoadIconW")
shellNotifyIconW = shell32DLL.NewProc("Shell_NotifyIconW")
globalAlloc = kernel32DLL.NewProc("GlobalAlloc")
globalLock = kernel32DLL.NewProc("GlobalLock")
globalUnlock = kernel32DLL.NewProc("GlobalUnlock")
createMutexW = kernel32DLL.NewProc("CreateMutexW")
getModuleW = kernel32DLL.NewProc("GetModuleHandleW")
sleep = kernel32DLL.NewProc("Sleep")
)
var (
mainWindow uintptr
instanceMutex uintptr
trayData notifyIconData
trayAdded bool
runtimeIdentity identity
identityReady bool
authorizationActive bool
windowProcedure = syscall.NewCallback(dcauthWindowProc)
)
func runClient(protocolArg string) {
runtime.LockOSThread()
firstInstance, err := acquireSingleInstance()
if err != nil {
showError("DCAuth 启动失败", err.Error())
return
}
if !firstInstance {
if authorizationInProgress() {
return
}
if forwardToRunningInstance(protocolArg) {
return
}
firstInstance, err = acquireSingleInstance()
if err != nil || !firstInstance {
showError("DCAuth 启动失败", "DCAuth 已在运行但暂时没有响应。请在任务管理器中结束 DCAuth.exe 后重试。")
return
}
}
registerProtocol()
if err := createHiddenWindow(); err != nil {
showError("DCAuth 启动失败", err.Error())
return
}
if err := addTrayIcon(); err != nil {
showError("DCAuth 启动失败", err.Error())
return
}
runtimeIdentity, err = loadOrCreateIdentity()
if err != nil {
removeTrayIcon()
showError("DCAuth 初始化失败", err.Error())
destroyWindow.Call(mainWindow)
mainWindow = 0
return
}
identityReady = true
if protocolArg != "" {
handleAuthorization(protocolArg)
} else {
showTrayBalloon("DCAuth v"+clientVersion+" 已在后台运行", "双击托盘图标可查看版本和设备码。")
}
runMessageLoop()
removeTrayIcon()
}
func acquireSingleInstance() (bool, error) {
namePtr, _ := syscall.UTF16PtrFromString(mutexName)
handle, _, callErr := createMutexW.Call(0, 0, uintptr(unsafe.Pointer(namePtr)))
runtime.KeepAlive(namePtr)
if handle == 0 {
return false, fmt.Errorf("无法创建单实例锁:%v", callErr)
}
if errno, ok := callErr.(syscall.Errno); ok && errno == errorAlreadyExists {
_ = syscall.CloseHandle(syscall.Handle(handle))
return false, nil
}
instanceMutex = handle
return true, nil
}
func authorizationInProgress() bool {
handle, acquired, err := acquireNamedMutex(authorizationMutexName)
if err != nil || !acquired {
if handle != 0 {
_ = syscall.CloseHandle(syscall.Handle(handle))
}
return err == nil && !acquired
}
_ = syscall.CloseHandle(syscall.Handle(handle))
return false
}
func acquireNamedMutex(name string) (uintptr, bool, error) {
namePtr, _ := syscall.UTF16PtrFromString(name)
handle, _, callErr := createMutexW.Call(0, 0, uintptr(unsafe.Pointer(namePtr)))
runtime.KeepAlive(namePtr)
if handle == 0 {
return 0, false, fmt.Errorf("无法创建互斥锁:%v", callErr)
}
if errno, ok := callErr.(syscall.Errno); ok && errno == errorAlreadyExists {
return handle, false, nil
}
return handle, true, nil
}
func forwardToRunningInstance(protocolArg string) bool {
for attempt := 0; attempt < 300; attempt++ {
if hwnd := findRunningInstanceWindow(); hwnd != 0 {
return sendToRunningInstance(hwnd, protocolArg)
}
sleep.Call(100)
}
return false
}
func findRunningInstanceWindow() uintptr {
classPtr, _ := syscall.UTF16PtrFromString(windowClassName)
titlePtr, _ := syscall.UTF16PtrFromString(windowTitle)
hwnd, _, _ := findWindowW.Call(
uintptr(unsafe.Pointer(classPtr)),
uintptr(unsafe.Pointer(titlePtr)),
)
runtime.KeepAlive(classPtr)
runtime.KeepAlive(titlePtr)
return hwnd
}
func sendToRunningInstance(hwnd uintptr, protocolArg string) bool {
if protocolArg == "" {
result, _, _ := postMessageW.Call(hwnd, showWindowMessage, 0, 0)
return result != 0
}
encoded, _ := syscall.UTF16FromString(protocolArg)
data := copyDataStruct{
DwData: copyDataMagic,
CbData: uint32(len(encoded) * 2),
LpData: uintptr(unsafe.Pointer(&encoded[0])),
}
result, _, _ := sendMessageW.Call(
hwnd,
wmCopyData,
0,
uintptr(unsafe.Pointer(&data)),
)
runtime.KeepAlive(encoded)
return result != 0
}
func createHiddenWindow() error {
instance, _, _ := getModuleW.Call(0)
if instance == 0 {
return fmt.Errorf("无法读取程序实例句柄")
}
icon, _, _ := loadIconW.Call(0, 32512)
classNamePtr, _ := syscall.UTF16PtrFromString(windowClassName)
class := wndClassEx{
CbSize: uint32(unsafe.Sizeof(wndClassEx{})),
LpfnWndProc: windowProcedure,
HInstance: instance,
HIcon: icon,
LpszClassName: classNamePtr,
HIconSm: icon,
}
result, _, callErr := registerClassExW.Call(uintptr(unsafe.Pointer(&class)))
runtime.KeepAlive(classNamePtr)
if result == 0 {
if errno, ok := callErr.(syscall.Errno); !ok || errno != errorClassAlreadyReg {
return fmt.Errorf("无法注册窗口类:%v", callErr)
}
}
titlePtr, _ := syscall.UTF16PtrFromString(windowTitle)
const wsExToolWindow = 0x00000080
hwnd, _, callErr := createWindowExW.Call(
wsExToolWindow,
uintptr(unsafe.Pointer(classNamePtr)),
uintptr(unsafe.Pointer(titlePtr)),
0,
0, 0, 0, 0,
0,
0,
instance,
0,
)
if hwnd == 0 {
return fmt.Errorf("无法创建 DCAuth 后台窗口:%v", callErr)
}
runtime.KeepAlive(classNamePtr)
runtime.KeepAlive(titlePtr)
mainWindow = hwnd
return nil
}
func runMessageLoop() {
var msg message
for {
result, _, _ := getMessageW.Call(
uintptr(unsafe.Pointer(&msg)),
0,
0,
0,
)
if int32(result) <= 0 {
return
}
translateMessage.Call(uintptr(unsafe.Pointer(&msg)))
dispatchMessageW.Call(uintptr(unsafe.Pointer(&msg)))
}
}
func dcauthWindowProc(hwnd, msg, wParam, lParam uintptr) uintptr {
switch uint32(msg) {
case wmCopyData:
if !identityReady {
return 0
}
data := (*copyDataStruct)(unsafe.Pointer(lParam))
if data != nil && data.DwData == copyDataMagic && data.CbData > 0 && data.LpData != 0 {
count := int(data.CbData / 2)
raw := syscall.UTF16ToString(unsafe.Slice((*uint16)(unsafe.Pointer(data.LpData)), count))
handleAuthorization(raw)
return 1
}
return 0
case showWindowMessage:
if identityReady {
showDeviceInfo()
}
return 0
case trayCallbackMessage:
switch uint32(lParam) {
case wmLButtonDblClk:
if identityReady {
showDeviceInfo()
} else {
showTrayBalloon("DCAuth 正在初始化", "设备信息准备完成后即可查看。")
}
case wmRButtonUp, wmContextMenu:
showTrayMenu(hwnd)
}
return 0
case wmClose:
destroyWindow.Call(hwnd)
return 0
case wmDestroy:
removeTrayIcon()
postQuitMessage.Call(0)
return 0
}
result, _, _ := defWindowProcW.Call(hwnd, msg, wParam, lParam)
return result
}
func addTrayIcon() error {
icon, _, _ := loadIconW.Call(0, 32512)
trayData = notifyIconData{
HWnd: mainWindow,
UID: 1,
UFlags: nifMessage | nifIcon | nifTip,
UCallbackMessage: trayCallbackMessage,
HIcon: icon,
}
trayData.CbSize = uint32(unsafe.Sizeof(trayData))
copyUTF16(trayData.SzTip[:], "DCAuth v"+clientVersion+" - 本机设备认证(后台运行)")
result, _, _ := shellNotifyIconW.Call(
nimAdd,
uintptr(unsafe.Pointer(&trayData)),
)
if result == 0 {
return fmt.Errorf("无法创建系统托盘图标")
}
trayAdded = true
return nil
}
func removeTrayIcon() {
if !trayAdded {
return
}
shellNotifyIconW.Call(nimDelete, uintptr(unsafe.Pointer(&trayData)))
trayAdded = false
}
func showTrayBalloon(title, text string) {
if !trayAdded {
return
}
info := trayData
info.UFlags |= nifInfo
info.UVersion = 10000
info.DwInfoFlags = niifInfo
copyUTF16(info.SzInfoTitle[:], title)
copyUTF16(info.SzInfo[:], text)
shellNotifyIconW.Call(nimModify, uintptr(unsafe.Pointer(&info)))
}
func showTrayMenu(hwnd uintptr) {
menu, _, _ := createPopupMenu.Call()
if menu == 0 {
return
}
defer destroyMenu.Call(menu)
statusText := mustUTF16("DCAuth v" + clientVersion + " · 后台运行中")
deviceText := mustUTF16("查看设备码")
exitText := mustUTF16("退出 DCAuth")
appendMenuW.Call(menu, mfString|mfGrayed, 0, uintptr(unsafe.Pointer(statusText)))
appendMenuW.Call(menu, mfString, trayMenuDeviceInfo, uintptr(unsafe.Pointer(deviceText)))
appendMenuW.Call(menu, mfSeparator, 0, 0)
appendMenuW.Call(menu, mfString, trayMenuExit, uintptr(unsafe.Pointer(exitText)))
runtime.KeepAlive(statusText)
runtime.KeepAlive(deviceText)
runtime.KeepAlive(exitText)
var cursor point
if ok, _, _ := getCursorPos.Call(uintptr(unsafe.Pointer(&cursor))); ok == 0 {
return
}
setForegroundWindow.Call(hwnd)
command, _, _ := trackPopupMenu.Call(
menu,
tpmRight|tpmReturn,
uintptr(cursor.X),
uintptr(cursor.Y),
0,
hwnd,
0,
)
postMessageW.Call(hwnd, wmNull, 0, 0)
switch command {
case trayMenuDeviceInfo:
if identityReady {
showDeviceInfo()
} else {
showTrayBalloon("DCAuth 正在初始化", "设备信息准备完成后即可查看。")
}
case trayMenuExit:
destroyWindow.Call(hwnd)
}
}
func showDeviceInfo() {
text := fmt.Sprintf(
"版本:v%s\n\n设备码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码已复制到剪贴板。",
clientVersion,
runtimeIdentity.deviceID,
)
copyToClipboard(runtimeIdentity.deviceID)
showInfo("DCAuth v"+clientVersion+" 设备信息", text)
}
func handleAuthorization(raw string) {
if authorizationActive {
showTrayBalloon("DCAuth v"+clientVersion, "已有一次登录确认正在进行,请先完成当前窗口。")
return
}
authorizationActive = true
defer func() { authorizationActive = false }()
authHandle, acquired, err := acquireNamedMutex(authorizationMutexName)
if err != nil {
showError("DCAuth 验证失败", err.Error())
return
}
if !acquired {
_ = syscall.CloseHandle(syscall.Handle(authHandle))
showTrayBalloon("DCAuth v"+clientVersion, "已有一次登录确认正在进行,请先完成当前窗口。")
return
}
defer syscall.CloseHandle(syscall.Handle(authHandle))
target, err := parseAuthorizeURL(raw)
if err != nil {
showError("DCAuth 协议无效", err.Error())
return
}
app := appRegistry[target.App]
if !confirm(
"DCAuth v"+clientVersion+" · "+app.DisplayName+" 本机登录确认",
fmt.Sprintf(
"是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n\n点击“确定”允许本次登录,点击“取消”拒绝。",
app.DisplayName,
clientVersion,
runtimeIdentity.deviceID,
),
) {
return
}
if err := submitAuthorization(runtimeIdentity, target); err != nil {
showError("本机验证失败", err.Error())
return
}
showTrayBalloon("DCAuth v"+clientVersion+" 验证成功", "已确认本机设备,请返回浏览器继续登录。")
}
func showInfo(title, text string) {
showMessage(title, text, mbOK|mbIconInformation)
}
func showError(title, text string) {
showMessage(title, text, mbOK|mbIconError)
}
func confirm(title, text string) bool {
return showMessage(title, text, mbOKCancel|mbIconQuestion) == idOK
}
func showMessage(title, text string, flags uint) int {
titlePtr, _ := syscall.UTF16PtrFromString(title)
textPtr, _ := syscall.UTF16PtrFromString(text)
result, _, _ := messageBoxW.Call(
mainWindow,
uintptr(unsafe.Pointer(textPtr)),
uintptr(unsafe.Pointer(titlePtr)),
uintptr(flags|mbSetForeground|mbTopMost),
)
runtime.KeepAlive(titlePtr)
runtime.KeepAlive(textPtr)
return int(result)
}
func copyUTF16(destination []uint16, text string) {
encoded := utf16.Encode([]rune(text))
if len(encoded) >= len(destination) {
encoded = encoded[:len(destination)-1]
}
copy(destination, encoded)
destination[len(encoded)] = 0
}
func mustUTF16(text string) *uint16 {
value, _ := syscall.UTF16PtrFromString(text)
return value
}
func copyToClipboard(text string) {
opened, _, _ := openClip.Call(mainWindow)
if opened == 0 {
return
}
defer closeClip.Call()
emptyClip.Call(mainWindow)
encoded := utf16.Encode([]rune(text))
encoded = append(encoded, 0)
size := uintptr(len(encoded) * 2)
handle, _, _ := globalAlloc.Call(gmemMoveable, size)
if handle == 0 {
return
}
pointer, _, _ := globalLock.Call(handle)
if pointer == 0 {
return
}
target := unsafe.Slice((*uint16)(unsafe.Pointer(pointer)), len(encoded))
copy(target, encoded)
globalUnlock.Call(handle)
setClipData.Call(cfUnicodeText, handle)
}