/** * 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 { 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 { 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 { // 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 { 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 { 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, }; }