import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; import type { MessageRevision } from '../../../../shared-kernel'; import { ServerDirectoryFacade } from '../../../server-directory'; const STORAGE_KEY = 'metoyou.messageSigningKeyPair'; interface StoredSigningKeyPair { publicKeyJwk: JsonWebKey; privateKeyJwk: JsonWebKey; } @Injectable({ providedIn: 'root' }) export class MessageSigningService { private readonly http = inject(HttpClient); private readonly serverDirectory = inject(ServerDirectoryFacade); private keyPairPromise: Promise | null = null; async ensureSigningKeyPair(): Promise { if (!this.keyPairPromise) { this.keyPairPromise = this.loadOrCreateKeyPair(); } return await this.keyPairPromise; } async getPublicKeyJwk(): Promise { const keyPair = await this.ensureSigningKeyPair(); return await crypto.subtle.exportKey('jwk', keyPair.publicKey); } async registerSigningPublicKeyIfNeeded(): Promise { const activeServer = this.serverDirectory.activeServer(); if (!activeServer) { return; } const publicKeyJwk = await this.getPublicKeyJwk(); const apiBase = `${activeServer.url}/api`; await firstValueFrom(this.http.put(`${apiBase}/users/me/signing-key`, { publicKeyJwk })); } async signRevision(revision: MessageRevision): Promise { const keyPair = await this.ensureSigningKeyPair(); const payload = this.canonicalRevisionPayload(revision); const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload)); const signature = await crypto.subtle.sign('Ed25519', keyPair.privateKey, digest); return this.bufferToBase64Url(signature); } async fetchSigningPublicKey(userId: string): Promise { const activeServer = this.serverDirectory.activeServer(); if (!activeServer) { return null; } try { const response = await firstValueFrom( this.http.get<{ publicKeyJwk: JsonWebKey }>(`${activeServer.url}/api/users/${encodeURIComponent(userId)}/signing-public-key`) ); return response.publicKeyJwk; } catch { return null; } } async verifyRevisionSignature( revision: MessageRevision, publicKeyJwk: JsonWebKey ): Promise { if (!revision.signature) { return false; } try { const publicKey = await crypto.subtle.importKey( 'jwk', publicKeyJwk, { name: 'Ed25519' }, false, ['verify'] ); const payload = this.canonicalRevisionPayload(revision); const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload)); const signature = this.base64UrlToBuffer(revision.signature); return await crypto.subtle.verify('Ed25519', publicKey, signature, digest); } catch { return false; } } private async loadOrCreateKeyPair(): Promise { const stored = this.readStoredKeyPair(); if (stored) { const publicKey = await crypto.subtle.importKey( 'jwk', stored.publicKeyJwk, { name: 'Ed25519' }, true, ['verify'] ); const privateKey = await crypto.subtle.importKey( 'jwk', stored.privateKeyJwk, { name: 'Ed25519' }, false, ['sign'] ); return { publicKey, privateKey }; } const generated = await crypto.subtle.generateKey( { name: 'Ed25519' }, true, ['sign', 'verify'] ); const publicKeyJwk = await crypto.subtle.exportKey('jwk', generated.publicKey); const privateKeyJwk = await crypto.subtle.exportKey('jwk', generated.privateKey); this.writeStoredKeyPair({ publicKeyJwk, privateKeyJwk }); return generated; } private canonicalRevisionPayload(revision: MessageRevision): string { const { signature: _signature, ...unsigned } = revision; return JSON.stringify(unsigned, Object.keys(unsigned).sort()); } private readStoredKeyPair(): StoredSigningKeyPair | null { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) { return null; } return JSON.parse(raw) as StoredSigningKeyPair; } catch { return null; } } private writeStoredKeyPair(pair: StoredSigningKeyPair): void { localStorage.setItem(STORAGE_KEY, JSON.stringify(pair)); } private bufferToBase64Url(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer); return btoa(String.fromCharCode(...bytes)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, ''); } private base64UrlToBuffer(value: string): ArrayBuffer { const normalized = value.replace(/-/g, '+').replace(/_/g, '/'); const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4); const binary = atob(padded); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return bytes.buffer; } }