feat(dcauth): 移除机器码,改用纯设备码认证

- DCAuth 协议升级到 v2,签名原文只含设备码;客户端 2.0.0,旧版本返回 426
- 删除客户端机器码采集、后端 machine_code_* 字段与日志、后台机器码展示
- 102 迁移在生产执行前直接改为无机器码结构;SQL 守卫仅放行该文件
- Mock 短信不启用每日限额;修复慢电脑首次启动的协议转发竞态
- 新增 macOS DCAuth.app 打包脚本,注册 dcauth:// 协议,浏览器可直接唤起
This commit is contained in:
yml2213
2026-10-21 20:46:25 +08:00
parent cb916d7750
commit ef36453694
28 changed files with 355 additions and 326 deletions
+34 -7
View File
@@ -5,7 +5,7 @@
## 运行方式
- **Windows**:GUI 常驻程序。启动后进入系统托盘,通过协议唤起时只出现一个登录确认窗口;成功后在托盘气泡中提示,不再连续弹多个消息框。
- **macOS**:后台常驻进程,使用系统图形对话框确认登录。重复执行命令会转发给已有进程,不会叠加实例。
- **macOS**:`DCAuth.app` 注册 `dcauth://`,浏览器可直接唤起;内部是后台常驻进程 + 系统图形确认框,重复唤起会转发给已有进程,不会叠加实例。
## 构建
@@ -18,9 +18,10 @@ x86_64-w64-mingw32-windres --codepage=65001 versioninfo.rc \
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
go build -trimpath -ldflags "-H windowsgui -s -w" \
-o dist/DCAuth.exe .
cp dist/DCAuth.exe dist/DCAuth-2.0.0.exe
```
版本号统一在 `main.go` 的 `clientVersion` 和 `versioninfo.rc` 中维护。`DCAuth.exe` 右键“属性 → 详细信息”会显示文件版本和产品版本,托盘菜单、设备信息及确认窗口也会显示当前版本。
版本号统一在 `main.go` 的 `clientVersion` 和 `versioninfo.rc` 中维护。`DCAuth.exe` 右键“属性 → 详细信息”会显示文件版本 `2.0.0.0` 和产品版本 `2.0.0`,托盘菜单、设备信息及确认窗口也会显示当前版本。
Windows ARM64:
@@ -56,12 +57,38 @@ codesign --force --sign - dist/DCAuth-macos
托盘操作:
- 双击图标:查看设备码和机器码,并复制到剪贴板。
- 双击图标:查看设备码并复制到剪贴板。
- 右键菜单:查看设备码、退出 DCAuth。
- 浏览器登录:只弹出一个确认窗口;点击“确定”后显示成功气泡,点击“取消”则拒绝。
- 再次唤起协议:第二个进程会把请求转交给常驻进程,然后立即退出。
## macOS 开发调试
## macOS 安装(一次性)
```bash
cd apps/dcauth
./build-macos.sh # 默认装到 ~/Applications/DCAuth.app
```
脚本会做四件事:
1. 用 `go build` 产出当前架构的二进制(自动读取 `main.go` 的 `clientVersion`)。
2. 用 `osacompile` 生成 AppleScript 壳,把 `dcauth://` 事件转成 argv 交给二进制。
3. 向 LaunchServices 注册 `dcauth://` 协议,并做 ad-hoc 签名。
4. 打印安装路径与退出方式。
装好之后浏览器登录页点一次即可直接唤起确认框,和 Windows 一样,**不再需要命令行**。
- 查看设备码:`open "$HOME/Applications/DCAuth.app"`
- 停止后台进程:`"$HOME/Applications/DCAuth.app/Contents/Resources/dcauth" --exit`
- 换成 Apple 芯片 / Intel 通用包:脚本在目标机器上各跑一次即可(二进制与架构绑定)。
> 为什么要套一层 AppleScript:macOS 只把自定义协议 URL 作为 Apple Event 投递,
> 纯 Go 的命令行程序收不到(实测进程参数为空)。AppleScript applet 的
> `on open location` 是目前不引入 Cocoa/cgo 的最稳做法。
## macOS 命令行方式(备用)
没有安装 `.app` 时,也可以直接执行二进制:
登录页在开发模式会显示一条 `dcauth://...` 链接。复制后在终端执行:
@@ -78,7 +105,7 @@ cd apps/dcauth
./dist/DCAuth-macos --exit
```
macOS 当前仍是开发版:私钥明文保存在用户配置目录的 `DCAuth/device.key`,且浏览器不会自动拉起程序,需要从登录页复制命令执行。
macOS 当前仍是开发版:私钥明文保存在 `~/Library/Application Support/DCAuth/device.key`(没有 DPAPI 等系统级保护),不要把这一版当成生产级凭证。安装 `DCAuth.app` 后浏览器可以自动唤起,但密钥保护强度仍低于 Windows。
如果只是测后台业务,不想每次都走短信和设备验证,可以在“后台用户”里把测试账号设为 **技术测试**。该账号只使用账号密码登录;正式账号仍必须走短信 + DCAuth。
@@ -118,7 +145,7 @@ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "-H windowsg
## 安全说明
- 私钥不离开本机,Windows 使用 DPAPI 保护。
- 服务端只保存公钥和机器码哈希/脱敏值。
- 机器码采集主板 UUID、MachineGuid、主板/BIOS/系统盘序列号、CPU ID,不上传原始序列号。
- 服务端只保存设备公钥和设备码,不采集或保存硬件序列号。
- 设备码只由公钥派生,不包含硬件指纹;更换硬件不会改变设备码。
- 正式分发前建议使用代码签名证书,避免 SmartScreen 和杀毒软件误报。
- 如果要做到 token 也不能复制,需要后续让 exe 对每次 API 请求签名,或把 exe 作为唯一客户端。
-40
View File
@@ -2,8 +2,6 @@ package main
import (
"crypto/sha256"
"fmt"
"sort"
"strings"
)
@@ -15,25 +13,6 @@ func buildDeviceID(publicKey []byte) string {
return formatCode("D1", sum[:])
}
func buildMachineCode() (string, error) {
components, err := collectHardwareComponents()
if err != nil {
return "", err
}
keys := make([]string, 0, len(components))
for key := range components {
keys = append(keys, key)
}
sort.Strings(keys)
parts := []string{"dcauth.machine.v1"}
for _, key := range keys {
parts = append(parts, key+"="+components[key])
}
sum := sha256.Sum256([]byte(strings.Join(parts, "\n")))
return formatCode("M1", sum[:]), nil
}
func formatCode(prefix string, data []byte) string {
encoded := base32Crockford(data)
if len(encoded) > 20 {
@@ -68,22 +47,3 @@ func base32Crockford(data []byte) string {
}
return builder.String()
}
func normalizeComponent(value string) string {
normalized := strings.ToUpper(strings.TrimSpace(value))
normalized = strings.Join(strings.Fields(normalized), "")
switch normalized {
case "", "0", "NONE", "N/A", "NA", "UNKNOWN", "DEFAULTSTRING",
"TOBEFILLEDBYO.E.M.", "TOBEFILLEDBYOEM", "SYSTEMSERIALNUMBER",
"00000000-0000-0000-0000-000000000000":
return ""
}
return normalized
}
func ensureNotEmpty(components map[string]string) error {
if len(components) == 0 {
return fmt.Errorf("无法读取本机硬件信息,请联系管理员")
}
return nil
}
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# 构建 macOS 版 DCAuth.app,并向 LaunchServices 注册 dcauth:// 协议。
#
# 为什么需要 .app:macOS 只把自定义协议的 URL 以 Apple Event 形式投递,
# 非 Cocoa 的纯命令行程序拿不到(实测 argv 里没有 URL)。这里用 AppleScript
# applet 的 on open location 接住事件,再把 URL 作为 argv 交给 Go 二进制,
# 从而让浏览器点一下就能唤起本机确认窗口,不需要手动执行命令。
set -euo pipefail
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "[build-macos] 只支持在 macOS 上运行" >&2
exit 1
fi
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_PATH="${1:-$HOME/Applications/DCAuth.app}"
LSREGISTER="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
VERSION="$(sed -n 's/^const clientVersion = "\(.*\)"$/\1/p' "$ROOT/main.go")"
if [[ -z "$VERSION" ]]; then
echo "[build-macos] 无法从 main.go 读取 clientVersion" >&2
exit 1
fi
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
echo "[build-macos] 编译 DCAuth v$VERSION"
(
cd "$ROOT"
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o "$WORK_DIR/dcauth" .
)
echo "[build-macos] 生成 AppleScript 壳"
cat > "$WORK_DIR/main.applescript" <<'APPLESCRIPT'
on run
set bundlePath to POSIX path of (path to me)
do shell script quoted form of (bundlePath & "Contents/Resources/dcauth")
end run
on open location theURL
set bundlePath to POSIX path of (path to me)
do shell script quoted form of (bundlePath & "Contents/Resources/dcauth") & " " & quoted form of theURL
end open location
APPLESCRIPT
rm -rf "$APP_PATH"
mkdir -p "$(dirname "$APP_PATH")"
osacompile -o "$APP_PATH" "$WORK_DIR/main.applescript"
mkdir -p "$APP_PATH/Contents/Resources"
install -m 0755 "$WORK_DIR/dcauth" "$APP_PATH/Contents/Resources/dcauth"
PLIST="$APP_PATH/Contents/Info.plist"
plist_set_string() {
local entry="$1"
local value="$2"
# osacompile 生成的 plist 只有少数键,Set 失败时要带类型 Add。
/usr/libexec/PlistBuddy -c "Set $entry $value" "$PLIST" 2>/dev/null ||
/usr/libexec/PlistBuddy -c "Add $entry string $value" "$PLIST"
}
plist_delete() {
/usr/libexec/PlistBuddy -c "Delete $1" "$PLIST" 2>/dev/null || true
}
plist_set_string ':CFBundleIdentifier' 'com.khhao.dcauth'
plist_set_string ':CFBundleName' 'DCAuth'
plist_set_string ':CFBundleDisplayName' 'DCAuth'
plist_set_string ':CFBundleShortVersionString' "$VERSION"
plist_set_string ':CFBundleVersion' "$VERSION"
plist_delete ':CFBundleURLTypes'
/usr/libexec/PlistBuddy -c 'Add :CFBundleURLTypes array' "$PLIST"
/usr/libexec/PlistBuddy -c 'Add :CFBundleURLTypes:0 dict' "$PLIST"
/usr/libexec/PlistBuddy -c 'Add :CFBundleURLTypes:0:CFBundleURLName string com.khhao.dcauth' "$PLIST"
/usr/libexec/PlistBuddy -c 'Add :CFBundleURLTypes:0:CFBundleURLSchemes array' "$PLIST"
/usr/libexec/PlistBuddy -c 'Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string dcauth' "$PLIST"
# 后台常驻,不显示 Dock 图标;双击 .app 等同于"启动/查看设备码"。
plist_delete ':LSUIElement'
/usr/libexec/PlistBuddy -c 'Add :LSUIElement bool true' "$PLIST"
echo "[build-macos] 签名并注册协议"
codesign --force --deep --sign - "$APP_PATH" >/dev/null 2>&1
if [[ -x "$LSREGISTER" ]]; then
"$LSREGISTER" -f "$APP_PATH"
fi
echo
echo "[build-macos] 完成:$APP_PATH"
echo "[build-macos] 浏览器再次点击登录即可直接唤起 DCAuth(无需命令行)。"
echo "[build-macos] 查看设备码:open \"$APP_PATH\""
echo "[build-macos] 停止后台进程:\"$APP_PATH/Contents/Resources/dcauth\" --exit"
-19
View File
@@ -1,19 +0,0 @@
//go:build !windows
package main
import (
"os"
"runtime"
)
func collectHardwareComponents() (map[string]string, error) {
hostname, _ := os.Hostname()
components := map[string]string{
"platform": runtime.GOOS + "-" + runtime.GOARCH,
}
if normalized := normalizeComponent(hostname); normalized != "" {
components["hostname"] = normalized
}
return components, ensureNotEmpty(components)
}
-75
View File
@@ -1,75 +0,0 @@
//go:build windows
package main
import (
"encoding/json"
"os/exec"
"strings"
)
func collectHardwareComponents() (map[string]string, error) {
components := map[string]string{}
script := `
$ErrorActionPreference = 'SilentlyContinue'
$cs = Get-CimInstance Win32_ComputerSystemProduct
$bb = Get-CimInstance Win32_BaseBoard
$bios = Get-CimInstance Win32_BIOS
$cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
$disk = Get-CimInstance Win32_DiskDrive | Where-Object { $_.Index -eq 0 } | Select-Object -First 1
[pscustomobject]@{
smbiosUuid = [string]$cs.UUID
baseboardSerial = [string]$bb.SerialNumber
biosSerial = [string]$bios.SerialNumber
cpuId = [string]$cpu.ProcessorId
systemDiskSerial = [string]$disk.SerialNumber
} | ConvertTo-Json -Compress
`
command := exec.Command(
"powershell.exe",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy", "Bypass",
"-Command", script,
)
hideCommandWindow(command)
output, err := command.Output()
if err == nil {
var payload map[string]string
if json.Unmarshal([]byte(strings.TrimSpace(string(output))), &payload) == nil {
for key, value := range payload {
if normalized := normalizeComponent(value); normalized != "" {
components[key] = normalized
}
}
}
}
if machineGUID := readMachineGUID(); machineGUID != "" {
components["machineGuid"] = machineGUID
}
if err := ensureNotEmpty(components); err != nil {
return nil, err
}
return components, nil
}
func readMachineGUID() string {
command := exec.Command(
"reg", "query", `HKLM\SOFTWARE\Microsoft\Cryptography`, "/v", "MachineGuid",
)
hideCommandWindow(command)
output, err := command.Output()
if err != nil {
return ""
}
line := strings.TrimSpace(string(output))
for _, candidate := range strings.Split(line, "\n") {
fields := strings.Fields(strings.TrimSpace(candidate))
if len(fields) >= 3 && strings.EqualFold(fields[0], "MachineGuid") {
return normalizeComponent(strings.Join(fields[2:], " "))
}
}
return ""
}
+19 -24
View File
@@ -15,7 +15,7 @@ import (
"time"
)
const clientVersion = "1.1.0"
const clientVersion = "2.0.0"
type appConfig struct {
DisplayName string
@@ -58,10 +58,9 @@ var appRegistry = map[string]appConfig{
}
type identity struct {
privateKey ed25519.PrivateKey
publicKey ed25519.PublicKey
deviceID string
machineCode string
privateKey ed25519.PrivateKey
publicKey ed25519.PublicKey
deviceID string
}
type authorizeRequest struct {
@@ -69,7 +68,6 @@ type authorizeRequest struct {
ChallengeID string `json:"challengeId"`
Challenge string `json:"challenge"`
DeviceID string `json:"deviceId"`
MachineCode string `json:"machineCode"`
PublicKey string `json:"publicKey"`
Signature string `json:"signature"`
Timestamp int64 `json:"timestamp"`
@@ -170,15 +168,7 @@ func containsOrigin(allowed []string, origin string) bool {
func submitAuthorization(id identity, target authorizeTarget) error {
timestamp := time.Now().Unix()
canonical := strings.Join([]string{
"dcauth.v1",
target.App,
target.ChallengeID,
target.Challenge,
id.deviceID,
id.machineCode,
fmt.Sprintf("%d", timestamp),
}, "\n")
canonical := buildAuthorizationCanonical(id, target, timestamp)
signature := ed25519.Sign(id.privateKey, []byte(canonical))
payload := authorizeRequest{
@@ -186,7 +176,6 @@ func submitAuthorization(id identity, target authorizeTarget) error {
ChallengeID: target.ChallengeID,
Challenge: target.Challenge,
DeviceID: id.deviceID,
MachineCode: id.machineCode,
PublicKey: base64.RawURLEncoding.EncodeToString(id.publicKey),
Signature: base64.RawURLEncoding.EncodeToString(signature),
Timestamp: timestamp,
@@ -217,6 +206,17 @@ func submitAuthorization(id identity, target authorizeTarget) error {
return nil
}
func buildAuthorizationCanonical(id identity, target authorizeTarget, timestamp int64) string {
return strings.Join([]string{
"dcauth.v2",
target.App,
target.ChallengeID,
target.Challenge,
id.deviceID,
fmt.Sprintf("%d", timestamp),
}, "\n")
}
func summarizeResponse(body []byte) string {
var payload map[string]any
if json.Unmarshal(body, &payload) == nil {
@@ -287,14 +287,9 @@ func loadOrCreateIdentity() (identity, error) {
}
publicKey := privateKey.Public().(ed25519.PublicKey)
machineCode, err := buildMachineCode()
if err != nil {
return identity{}, err
}
return identity{
privateKey: privateKey,
publicKey: publicKey,
deviceID: buildDeviceID(publicKey),
machineCode: machineCode,
privateKey: privateKey,
publicKey: publicKey,
deviceID: buildDeviceID(publicKey),
}, nil
}
+21 -1
View File
@@ -1,6 +1,9 @@
package main
import "testing"
import (
"strings"
"testing"
)
func TestParseAuthorizeURLAllowedOrigins(t *testing.T) {
tests := []struct {
@@ -33,3 +36,20 @@ func TestParseAuthorizeURLRejectsUnknownOrigin(t *testing.T) {
t.Fatal("parseAuthorizeURL() accepted an unknown origin")
}
}
func TestBuildAuthorizationCanonicalUsesDeviceCodeOnly(t *testing.T) {
id := identity{deviceID: "D1-ABCD-EFGH-JKLM-NPQR"}
target := authorizeTarget{
App: "order_site",
ChallengeID: "challenge-id",
Challenge: "challenge-value",
}
got := buildAuthorizationCanonical(id, target, 1700000000)
want := "dcauth.v2\norder_site\nchallenge-id\nchallenge-value\nD1-ABCD-EFGH-JKLM-NPQR\n1700000000"
if got != want {
t.Fatalf("canonical = %q, want %q", got, want)
}
if strings.Contains(got, "M1-") || strings.Contains(strings.ToLower(got), "machine") {
t.Fatalf("canonical still contains machine code data: %q", got)
}
}
+3 -5
View File
@@ -141,11 +141,10 @@ func handleDarwinAuthorization(identity identity, raw string) {
if !confirmDarwin(
"DCAuth v"+clientVersion+" · "+app.DisplayName+" 本机登录确认",
fmt.Sprintf(
"是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n机器码:%s\n\n点击“允许登录”完成验证,点击“取消”拒绝。",
"是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n\n点击“允许登录”完成验证,点击“取消”拒绝。",
app.DisplayName,
clientVersion,
identity.deviceID,
identity.machineCode,
),
) {
return
@@ -158,14 +157,13 @@ func handleDarwinAuthorization(identity identity, raw string) {
}
func showDarwinDeviceInfo(identity identity) {
copyToClipboard(identity.deviceID + "\n" + identity.machineCode)
copyToClipboard(identity.deviceID)
showInfo(
"DCAuth v"+clientVersion+" 设备信息",
fmt.Sprintf(
"版本:v%s\n\n设备码:%s\n机器码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码和机器码已复制到剪贴板。",
"版本:v%s\n\n设备码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码已复制到剪贴板。",
clientVersion,
identity.deviceID,
identity.machineCode,
),
)
}
+4 -5
View File
@@ -16,13 +16,12 @@ func runClient(protocolArg string) {
if protocolArg == "" {
text := fmt.Sprintf(
"DCAuth v%s\n设备码:%s\n机器码:%s\n\n设备码用于识别本机,机器码用于管理员核对。\n请从后台登录页点击“验证本机”完成登录。",
"DCAuth v%s\n设备码:%s\n\n设备码用于识别本机设备。\n请从后台登录页点击“验证本机”完成登录。",
clientVersion,
identity.deviceID,
identity.machineCode,
)
copyToClipboard(identity.deviceID + "\n" + identity.machineCode)
showInfo("DCAuth v"+clientVersion+" 设备信息", text+"\n\n设备码和机器码已复制到剪贴板。")
copyToClipboard(identity.deviceID)
showInfo("DCAuth v"+clientVersion+" 设备信息", text+"\n\n设备码已复制到剪贴板。")
return
}
@@ -34,7 +33,7 @@ func runClient(protocolArg string) {
app := appRegistry[target.App]
if !confirm(
app.DisplayName+" · 本机登录确认",
fmt.Sprintf("是否确认使用本机设备登录「%s」?\n\n设备码:%s\n机器码:%s", app.DisplayName, identity.deviceID, identity.machineCode),
fmt.Sprintf("是否确认使用本机设备登录「%s」?\n\n设备码:%s", app.DisplayName, identity.deviceID),
) {
return
}
+46 -25
View File
@@ -191,6 +191,7 @@ var (
trayData notifyIconData
trayAdded bool
runtimeIdentity identity
identityReady bool
authorizationActive bool
windowProcedure = syscall.NewCallback(dcauthWindowProc)
)
@@ -209,16 +210,14 @@ func runClient(protocolArg string) {
if forwardToRunningInstance(protocolArg) {
return
}
showError("DCAuth 启动失败", "已有 DCAuth 实例正在启动,但暂时无法连接,请稍后重试。")
return
firstInstance, err = acquireSingleInstance()
if err != nil || !firstInstance {
showError("DCAuth 启动失败", "DCAuth 已在运行但暂时没有响应。请在任务管理器中结束 DCAuth.exe 后重试。")
return
}
}
registerProtocol()
runtimeIdentity, err = loadOrCreateIdentity()
if err != nil {
showError("DCAuth 初始化失败", err.Error())
return
}
if err := createHiddenWindow(); err != nil {
showError("DCAuth 启动失败", err.Error())
return
@@ -228,10 +227,20 @@ func runClient(protocolArg string) {
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+" 已在后台运行", "双击托盘图标可查看版本、设备码和机器码。")
showTrayBalloon("DCAuth v"+clientVersion+" 已在后台运行", "双击托盘图标可查看版本和设备码。")
}
runMessageLoop()
@@ -279,16 +288,16 @@ func acquireNamedMutex(name string) (uintptr, bool, error) {
}
func forwardToRunningInstance(protocolArg string) bool {
for attempt := 0; attempt < 100; attempt++ {
if postToRunningInstance(protocolArg) {
return true
for attempt := 0; attempt < 300; attempt++ {
if hwnd := findRunningInstanceWindow(); hwnd != 0 {
return sendToRunningInstance(hwnd, protocolArg)
}
sleep.Call(100)
}
return false
}
func postToRunningInstance(protocolArg string) bool {
func findRunningInstanceWindow() uintptr {
classPtr, _ := syscall.UTF16PtrFromString(windowClassName)
titlePtr, _ := syscall.UTF16PtrFromString(windowTitle)
hwnd, _, _ := findWindowW.Call(
@@ -297,12 +306,13 @@ func postToRunningInstance(protocolArg string) bool {
)
runtime.KeepAlive(classPtr)
runtime.KeepAlive(titlePtr)
if hwnd == 0 {
return false
}
return hwnd
}
func sendToRunningInstance(hwnd uintptr, protocolArg string) bool {
if protocolArg == "" {
postMessageW.Call(hwnd, showWindowMessage, 0, 0)
return true
result, _, _ := postMessageW.Call(hwnd, showWindowMessage, 0, 0)
return result != 0
}
encoded, _ := syscall.UTF16FromString(protocolArg)
@@ -387,6 +397,9 @@ func runMessageLoop() {
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)
@@ -396,12 +409,18 @@ func dcauthWindowProc(hwnd, msg, wParam, lParam uintptr) uintptr {
}
return 0
case showWindowMessage:
showDeviceInfo()
if identityReady {
showDeviceInfo()
}
return 0
case trayCallbackMessage:
switch uint32(lParam) {
case wmLButtonDblClk:
showDeviceInfo()
if identityReady {
showDeviceInfo()
} else {
showTrayBalloon("DCAuth 正在初始化", "设备信息准备完成后即可查看。")
}
case wmRButtonUp, wmContextMenu:
showTrayMenu(hwnd)
}
@@ -497,7 +516,11 @@ func showTrayMenu(hwnd uintptr) {
switch command {
case trayMenuDeviceInfo:
showDeviceInfo()
if identityReady {
showDeviceInfo()
} else {
showTrayBalloon("DCAuth 正在初始化", "设备信息准备完成后即可查看。")
}
case trayMenuExit:
destroyWindow.Call(hwnd)
}
@@ -505,12 +528,11 @@ func showTrayMenu(hwnd uintptr) {
func showDeviceInfo() {
text := fmt.Sprintf(
"版本:v%s\n\n设备码:%s\n机器码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码和机器码已复制到剪贴板。",
"版本:v%s\n\n设备码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码已复制到剪贴板。",
clientVersion,
runtimeIdentity.deviceID,
runtimeIdentity.machineCode,
)
copyToClipboard(runtimeIdentity.deviceID + "\n" + runtimeIdentity.machineCode)
copyToClipboard(runtimeIdentity.deviceID)
showInfo("DCAuth v"+clientVersion+" 设备信息", text)
}
@@ -543,11 +565,10 @@ func handleAuthorization(raw string) {
if !confirm(
"DCAuth v"+clientVersion+" · "+app.DisplayName+" 本机登录确认",
fmt.Sprintf(
"是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n机器码:%s\n\n点击“确定”允许本次登录,点击“取消”拒绝。",
"是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n\n点击“确定”允许本次登录,点击“取消”拒绝。",
app.DisplayName,
clientVersion,
runtimeIdentity.deviceID,
runtimeIdentity.machineCode,
),
) {
return
+4 -4
View File
@@ -1,6 +1,6 @@
1 VERSIONINFO
FILEVERSION 1,1,0,0
PRODUCTVERSION 1,1,0,0
FILEVERSION 2,0,0,0
PRODUCTVERSION 2,0,0,0
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS 0x40004L
@@ -13,12 +13,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "Order Site"
VALUE "FileDescription", "DCAuth device authentication client"
VALUE "FileVersion", "1.1.0.0"
VALUE "FileVersion", "2.0.0.0"
VALUE "InternalName", "DCAuth"
VALUE "LegalCopyright", "Copyright (C) 2026 Order Site"
VALUE "OriginalFilename", "DCAuth.exe"
VALUE "ProductName", "DCAuth"
VALUE "ProductVersion", "1.1.0"
VALUE "ProductVersion", "2.0.0"
END
END
BLOCK "VarFileInfo"
Binary file not shown.