将 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 个文件
179 lines
4.3 KiB
TypeScript
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,
|
|
};
|
|
}
|