Files
zclaw_openfang/desktop/src/lib/gateway-auth.ts
iven ecd7f2e928 fix(desktop): console.log 清理 — 替换为结构化 logger
将 desktop/src 中 23 处 console.log 替换为 createLogger() 结构化日志:
- 生产构建自动静默 debug/info 级别
- 保留 console.error 用于关键错误可见性
- 新增 dompurify 依赖修复 XSS 防护引入缺失

涉及文件: App.tsx, offlineStore.ts, autonomy-manager.ts,
gateway-auth.ts, llm-service.ts, request-helper.ts,
security-index.ts, skill-discovery.ts, use-onboarding.ts 等 16 个文件
2026-03-30 16:22:16 +08:00

179 lines
4.3 KiB
TypeScript

/**
* gateway-auth.ts - Device Authentication Module
*
* Extracted from gateway-client.ts for modularity.
* Handles Ed25519 device key generation, loading, signing,
* and device identity management using OS keyring or localStorage.
*/
import nacl from 'tweetnacl';
import {
storeDeviceKeys,
getDeviceKeys,
deleteDeviceKeys,
} from './secure-storage';
import { createLogger } from './logger';
const log = createLogger('GatewayAuth');
// === Types ===
export interface DeviceKeys {
deviceId: string;
publicKey: Uint8Array;
secretKey: Uint8Array;
publicKeyBase64: string;
}
export interface LocalDeviceIdentity {
deviceId: string;
publicKeyBase64: string;
}
// === Base64 Encoding ===
export function b64Encode(bytes: Uint8Array): string {
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// === Key Derivation ===
async function deriveDeviceId(publicKey: Uint8Array): Promise<string> {
const stableBytes = Uint8Array.from(publicKey);
const digest = await crypto.subtle.digest('SHA-256', stableBytes.buffer);
return Array.from(new Uint8Array(digest))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
}
// === Key Generation ===
async function generateDeviceKeys(): Promise<DeviceKeys> {
const keyPair = nacl.sign.keyPair();
const deviceId = await deriveDeviceId(keyPair.publicKey);
return {
deviceId,
publicKey: keyPair.publicKey,
secretKey: keyPair.secretKey,
publicKeyBase64: b64Encode(keyPair.publicKey),
};
}
// === Key Loading ===
/**
* Load device keys from secure storage.
* Uses OS keyring when available, falls back to localStorage.
*/
export async function loadDeviceKeys(): Promise<DeviceKeys> {
// Try to load from secure storage (keyring or localStorage fallback)
const storedKeys = await getDeviceKeys();
if (storedKeys) {
try {
const deviceId = await deriveDeviceId(storedKeys.publicKey);
return {
deviceId,
publicKey: storedKeys.publicKey,
secretKey: storedKeys.secretKey,
publicKeyBase64: b64Encode(storedKeys.publicKey),
};
} catch (e) {
log.warn('Failed to load stored keys:', e);
// Invalid stored keys, clear and regenerate
await deleteDeviceKeys();
}
}
// Generate new keys
const keys = await generateDeviceKeys();
// Store in secure storage (keyring when available, localStorage fallback)
await storeDeviceKeys(keys.publicKey, keys.secretKey);
return keys;
}
// === Public Identity ===
export async function getLocalDeviceIdentity(): Promise<LocalDeviceIdentity> {
const keys = await loadDeviceKeys();
return {
deviceId: keys.deviceId,
publicKeyBase64: keys.publicKeyBase64,
};
}
/**
* Clear cached device keys to force regeneration on next connect.
* Useful when device signature validation fails repeatedly.
*/
export async function clearDeviceKeys(): Promise<void> {
try {
await deleteDeviceKeys();
log.debug('Device keys cleared');
} catch (e) {
log.warn('Failed to clear device keys:', e);
}
}
// === Device Auth Signing ===
export function buildDeviceAuthPayload(params: {
clientId: string;
clientMode: string;
deviceId: string;
nonce: string;
role: string;
scopes: string[];
signedAt: number;
token?: string;
}): string {
return [
'v2',
params.deviceId,
params.clientId,
params.clientMode,
params.role,
params.scopes.join(','),
String(params.signedAt),
params.token || '',
params.nonce,
].join('|');
}
export function signDeviceAuth(params: {
clientId: string;
clientMode: string;
deviceId: string;
nonce: string;
role: string;
scopes: string[];
secretKey: Uint8Array;
token?: string;
}): { signature: string; signedAt: number } {
const signedAt = Date.now();
const message = buildDeviceAuthPayload({
clientId: params.clientId,
clientMode: params.clientMode,
deviceId: params.deviceId,
nonce: params.nonce,
role: params.role,
scopes: params.scopes,
signedAt,
token: params.token,
});
const messageBytes = new TextEncoder().encode(message);
const signature = nacl.sign.detached(messageBytes, params.secretKey);
return {
signature: b64Encode(signature),
signedAt,
};
}