refactor(store): split gatewayStore into specialized domain stores
Major restructuring: - Split monolithic gatewayStore into 5 focused stores: - connectionStore: WebSocket connection and gateway lifecycle - configStore: quickConfig, workspaceInfo, MCP services - agentStore: clones, usage stats, agent management - handStore: hands, approvals, triggers, hand runs - workflowStore: workflows, workflow runs, execution - Update all components to use new stores with selector pattern - Remove
This commit is contained in:
175
desktop/src/lib/gateway-auth.ts
Normal file
175
desktop/src/lib/gateway-auth.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
// === 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) {
|
||||
console.warn('[GatewayClient] 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();
|
||||
console.log('[GatewayClient] Device keys cleared');
|
||||
} catch (e) {
|
||||
console.warn('[GatewayClient] 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user