var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { Injectable } from '@angular/core'; /** * Manages the browser-local Ed25519 keypair used to sign admin auth * challenges. Real WebCrypto Ed25519 (RFC 8032 support landed in evergreen * browsers) - not a placeholder. The private key is generated * non-extractable and kept only in IndexedDB as a CryptoKey handle; it is * never serialized, never sent anywhere, and cannot be exported by design. * * Registering `publicKey` with an admin's account (associating it with a * role) is a backend-side, out-of-band operation (e.g. an Owner approving a * new admin's public key) - entirely outside this frontend's scope. */ const DB_NAME = 'admin-auth-ed25519'; const DB_VERSION = 1; const STORE_NAME = 'keypair'; const KEY_RECORD_ID = 'device-keypair'; let Ed25519KeypairService = class Ed25519KeypairService { constructor() { this.cached = null; } isSupported() { return typeof crypto !== 'undefined' && !!crypto.subtle && typeof indexedDB !== 'undefined'; } /** Returns the device's Ed25519 keypair, generating and persisting one on first use. */ async getOrCreateKeyPair() { if (!this.isSupported()) { throw new Error('Ed25519 is not supported in this browser (requires WebCrypto + IndexedDB).'); } const existing = await this.loadFromStore(); if (existing) { this.cached = existing; return { publicKeyBase64: existing.publicKeyBase64 }; } const generated = await this.generateAndPersist(); this.cached = generated; return { publicKeyBase64: generated.publicKeyBase64 }; } async sign(message) { const keyPair = this.cached ?? (await this.loadFromStore()); if (!keyPair) { throw new Error('No Ed25519 keypair available - call getOrCreateKeyPair() first.'); } const signatureBuffer = await crypto.subtle.sign('Ed25519', keyPair.privateKey, new TextEncoder().encode(message)); return this.toBase64(new Uint8Array(signatureBuffer)); } /** Discards the local keypair (e.g. "forget this device"). A new keypair on next login requires re-registration with the backend. */ async clear() { this.cached = null; const db = await this.openDatabase(); await new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); tx.objectStore(STORE_NAME).delete(KEY_RECORD_ID); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } async generateAndPersist() { const keyPair = (await crypto.subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify'])); const publicKeyRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey); const publicKeyBase64 = this.toBase64(new Uint8Array(publicKeyRaw)); const record = { id: KEY_RECORD_ID, publicKey: keyPair.publicKey, privateKey: keyPair.privateKey, publicKeyBase64 }; const db = await this.openDatabase(); await new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readwrite'); tx.objectStore(STORE_NAME).put(record); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); return record; } async loadFromStore() { const db = await this.openDatabase(); return new Promise((resolve, reject) => { const tx = db.transaction(STORE_NAME, 'readonly'); const request = tx.objectStore(STORE_NAME).get(KEY_RECORD_ID); request.onsuccess = () => resolve(request.result ?? null); request.onerror = () => reject(request.error); }); } openDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { if (!request.result.objectStoreNames.contains(STORE_NAME)) { request.result.createObjectStore(STORE_NAME, { keyPath: 'id' }); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } toBase64(bytes) { let binary = ''; for (const byte of bytes) { binary += String.fromCharCode(byte); } return btoa(binary); } }; Ed25519KeypairService = __decorate([ Injectable({ providedIn: 'root' }) ], Ed25519KeypairService); export { Ed25519KeypairService };