diff --git a/infrastructure/eid-wallet/src-tauri/gen/android/app/universal/release/app-universal-release.aab b/infrastructure/eid-wallet/src-tauri/gen/android/app/universal/release/app-universal-release.aab new file mode 100644 index 00000000..80200395 Binary files /dev/null and b/infrastructure/eid-wallet/src-tauri/gen/android/app/universal/release/app-universal-release.aab differ diff --git a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj index 87b68c06..3bba0ddc 100644 --- a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj +++ b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet.xcodeproj/project.pbxproj @@ -416,7 +416,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", ); MARKETING_VERSION = 0.2.1; - PRODUCT_BUNDLE_IDENTIFIER = "foundation.metastate.eid-wallet"; + PRODUCT_BUNDLE_IDENTIFIER = foundation.metastate.eid-wallet; PRODUCT_NAME = "eID for W3DS"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -464,7 +464,7 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", ); MARKETING_VERSION = 0.2.1; - PRODUCT_BUNDLE_IDENTIFIER = "foundation.metastate.eid-wallet"; + PRODUCT_BUNDLE_IDENTIFIER = foundation.metastate.eid-wallet; PRODUCT_NAME = "eID for W3DS"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist index 7d7a310a..abe5571f 100644 --- a/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist +++ b/infrastructure/eid-wallet/src-tauri/gen/apple/eid-wallet_iOS/Info.plist @@ -61,4 +61,4 @@ UIInterfaceOrientationLandscapeRight - + \ No newline at end of file diff --git a/infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts b/infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts new file mode 100644 index 00000000..ee763019 --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/crypto/HardwareKeyManager.ts @@ -0,0 +1,91 @@ +import type { KeyManager } from './types'; +import { KeyManagerError, KeyManagerErrorCodes } from './types'; +import { + exists as hwExists, + generate as hwGenerate, + getPublicKey as hwGetPublicKey, + signPayload as hwSignPayload, + verifySignature as hwVerifySignature, +} from '@auvo/tauri-plugin-crypto-hw-api'; + +/** + * Hardware key manager implementation using Tauri crypto hardware API + */ +export class HardwareKeyManager implements KeyManager { + getType(): 'hardware' | 'software' { + return 'hardware'; + } + + async exists(keyId: string): Promise { + try { + return await hwExists(keyId); + } catch (error) { + console.error('Hardware key exists check failed:', error); + throw new KeyManagerError( + 'Failed to check if hardware key exists', + KeyManagerErrorCodes.HARDWARE_UNAVAILABLE, + keyId + ); + } + } + + async generate(keyId: string): Promise { + try { + const result = await hwGenerate(keyId); + console.log(`Hardware key generated for ${keyId}:`, result); + return result; + } catch (error) { + console.error('Hardware key generation failed:', error); + throw new KeyManagerError( + 'Failed to generate hardware key', + KeyManagerErrorCodes.KEY_GENERATION_FAILED, + keyId + ); + } + } + + async getPublicKey(keyId: string): Promise { + try { + const publicKey = await hwGetPublicKey(keyId); + console.log(`Hardware public key retrieved for ${keyId}:`, publicKey); + return publicKey; + } catch (error) { + console.error('Hardware public key retrieval failed:', error); + throw new KeyManagerError( + 'Failed to get hardware public key', + KeyManagerErrorCodes.KEY_NOT_FOUND, + keyId + ); + } + } + + async signPayload(keyId: string, payload: string): Promise { + try { + const signature = await hwSignPayload(keyId, payload); + console.log(`Hardware signature created for ${keyId}`); + return signature; + } catch (error) { + console.error('Hardware signing failed:', error); + throw new KeyManagerError( + 'Failed to sign payload with hardware key', + KeyManagerErrorCodes.SIGNING_FAILED, + keyId + ); + } + } + + async verifySignature(keyId: string, payload: string, signature: string): Promise { + try { + const isValid = await hwVerifySignature(keyId, payload, signature); + console.log(`Hardware signature verification for ${keyId}:`, isValid); + return isValid; + } catch (error) { + console.error('Hardware signature verification failed:', error); + throw new KeyManagerError( + 'Failed to verify signature with hardware key', + KeyManagerErrorCodes.VERIFICATION_FAILED, + keyId + ); + } + } +} diff --git a/infrastructure/eid-wallet/src/lib/crypto/KeyManagerFactory.ts b/infrastructure/eid-wallet/src/lib/crypto/KeyManagerFactory.ts new file mode 100644 index 00000000..8d01587f --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/crypto/KeyManagerFactory.ts @@ -0,0 +1,99 @@ +import type { KeyManager, KeyManagerConfig } from './types'; +import { HardwareKeyManager } from './HardwareKeyManager'; +import { SoftwareKeyManager } from './SoftwareKeyManager'; +import { KeyManagerError, KeyManagerErrorCodes } from './types'; + +/** + * Factory class to create appropriate key managers based on context + */ +export class KeyManagerFactory { + private static hardwareKeyManager: HardwareKeyManager | null = null; + private static softwareKeyManager: SoftwareKeyManager | null = null; + + /** + * Get a key manager instance based on the configuration + */ + static async getKeyManager(config: KeyManagerConfig): Promise { + // If explicitly requesting hardware and not in pre-verification mode + if (config.useHardware && !config.preVerificationMode) { + return this.getHardwareKeyManager(); + } + + // If in pre-verification mode, always use software keys + if (config.preVerificationMode) { + console.log('Using software key manager for pre-verification mode'); + return this.getSoftwareKeyManager(); + } + + // Default behavior: try hardware first, fallback to software + try { + const hardwareManager = this.getHardwareKeyManager(); + // Test if hardware is available by checking if we can call exists + await hardwareManager.exists(config.keyId); + console.log('Using hardware key manager'); + return hardwareManager; + } catch (error) { + console.log('Hardware key manager not available, falling back to software'); + return this.getSoftwareKeyManager(); + } + } + + /** + * Get hardware key manager instance (singleton) + */ + private static getHardwareKeyManager(): HardwareKeyManager { + if (!this.hardwareKeyManager) { + this.hardwareKeyManager = new HardwareKeyManager(); + } + return this.hardwareKeyManager; + } + + /** + * Get software key manager instance (singleton) + */ + private static getSoftwareKeyManager(): SoftwareKeyManager { + if (!this.softwareKeyManager) { + this.softwareKeyManager = new SoftwareKeyManager(); + } + return this.softwareKeyManager; + } + + /** + * Check if hardware key manager is available + */ + static async isHardwareAvailable(): Promise { + try { + const hardwareManager = this.getHardwareKeyManager(); + // Try to check if a test key exists to verify hardware availability + await hardwareManager.exists('test-hardware-check'); + return true; + } catch (error) { + console.log('Hardware key manager not available:', error); + return false; + } + } + + /** + * Get the appropriate key manager for a specific use case + */ + static async getKeyManagerForContext( + keyId: string, + context: 'onboarding' | 'signing' | 'verification' | 'pre-verification' + ): Promise { + const config: KeyManagerConfig = { + keyId, + useHardware: context !== 'pre-verification', + preVerificationMode: context === 'pre-verification', + }; + + return this.getKeyManager(config); + } + + /** + * Reset singleton instances (useful for testing) + */ + static reset(): void { + this.hardwareKeyManager = null; + this.softwareKeyManager = null; + } +} diff --git a/infrastructure/eid-wallet/src/lib/crypto/SoftwareKeyManager.ts b/infrastructure/eid-wallet/src/lib/crypto/SoftwareKeyManager.ts new file mode 100644 index 00000000..b559f6f5 --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/crypto/SoftwareKeyManager.ts @@ -0,0 +1,260 @@ +import type { KeyManager, SoftwareKeyPair } from './types'; +import { KeyManagerError, KeyManagerErrorCodes } from './types'; + +/** + * Software key manager implementation using Web Crypto API and localStorage + */ +export class SoftwareKeyManager implements KeyManager { + private readonly storageKey = 'eid-wallet-software-keys'; + private readonly keyPrefix = 'software-key-'; + + getType(): 'hardware' | 'software' { + return 'software'; + } + + async exists(keyId: string): Promise { + try { + const storageKey = this.getStorageKey(keyId); + const stored = localStorage.getItem(storageKey); + return stored !== null; + } catch (error) { + console.error('Software key exists check failed:', error); + throw new KeyManagerError( + 'Failed to check if software key exists', + KeyManagerErrorCodes.STORAGE_ERROR, + keyId + ); + } + } + + async generate(keyId: string): Promise { + try { + // Check if key already exists + if (await this.exists(keyId)) { + console.log(`Software key ${keyId} already exists`); + return 'key-exists'; + } + + // Generate a new key pair using Web Crypto API + const keyPair = await crypto.subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-256', + }, + true, // extractable + ['sign', 'verify'] + ); + + // Export the private key + const privateKeyBuffer = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey); + const privateKeyString = this.arrayBufferToBase64(privateKeyBuffer); + + // Export the public key + const publicKeyBuffer = await crypto.subtle.exportKey('spki', keyPair.publicKey); + const publicKeyString = this.arrayBufferToBase64(publicKeyBuffer); + + // Store the key pair + const keyPairData: SoftwareKeyPair = { + privateKey: privateKeyString, + publicKey: publicKeyString, + keyId, + createdAt: new Date().toISOString(), + }; + + const storageKey = this.getStorageKey(keyId); + localStorage.setItem(storageKey, JSON.stringify(keyPairData)); + + console.log(`Software key pair generated and stored for ${keyId}`); + return 'key-generated'; + } catch (error) { + console.error('Software key generation failed:', error); + throw new KeyManagerError( + 'Failed to generate software key', + KeyManagerErrorCodes.KEY_GENERATION_FAILED, + keyId + ); + } + } + + async getPublicKey(keyId: string): Promise { + try { + const keyPair = await this.getKeyPair(keyId); + if (!keyPair) { + throw new KeyManagerError( + 'Software key not found', + KeyManagerErrorCodes.KEY_NOT_FOUND, + keyId + ); + } + + // Convert the stored public key to a format compatible with the hardware API + // The hardware API returns multibase hex format, so we'll convert our base64 to hex + const publicKeyBuffer = this.base64ToArrayBuffer(keyPair.publicKey); + const publicKeyHex = this.arrayBufferToHex(publicKeyBuffer); + + // Add multibase prefix (assuming 'z' for base58btc, but we'll use hex for simplicity) + return `z${publicKeyHex}`; + } catch (error) { + console.error('Software public key retrieval failed:', error); + if (error instanceof KeyManagerError) { + throw error; + } + throw new KeyManagerError( + 'Failed to get software public key', + KeyManagerErrorCodes.KEY_NOT_FOUND, + keyId + ); + } + } + + async signPayload(keyId: string, payload: string): Promise { + try { + const keyPair = await this.getKeyPair(keyId); + if (!keyPair) { + throw new KeyManagerError( + 'Software key not found', + KeyManagerErrorCodes.KEY_NOT_FOUND, + keyId + ); + } + + // Import the private key + const privateKeyBuffer = this.base64ToArrayBuffer(keyPair.privateKey); + const privateKey = await crypto.subtle.importKey( + 'pkcs8', + privateKeyBuffer, + { + name: 'ECDSA', + namedCurve: 'P-256', + }, + false, + ['sign'] + ); + + // Convert payload to ArrayBuffer + const payloadBuffer = new TextEncoder().encode(payload); + + // Sign the payload + const signature = await crypto.subtle.sign( + { + name: 'ECDSA', + hash: 'SHA-256', + }, + privateKey, + payloadBuffer + ); + + // Convert signature to base64 string + const signatureString = this.arrayBufferToBase64(signature); + console.log(`Software signature created for ${keyId}`); + return signatureString; + } catch (error) { + console.error('Software signing failed:', error); + if (error instanceof KeyManagerError) { + throw error; + } + throw new KeyManagerError( + 'Failed to sign payload with software key', + KeyManagerErrorCodes.SIGNING_FAILED, + keyId + ); + } + } + + async verifySignature(keyId: string, payload: string, signature: string): Promise { + try { + const keyPair = await this.getKeyPair(keyId); + if (!keyPair) { + throw new KeyManagerError( + 'Software key not found', + KeyManagerErrorCodes.KEY_NOT_FOUND, + keyId + ); + } + + // Import the public key + const publicKeyBuffer = this.base64ToArrayBuffer(keyPair.publicKey); + const publicKey = await crypto.subtle.importKey( + 'spki', + publicKeyBuffer, + { + name: 'ECDSA', + namedCurve: 'P-256', + }, + false, + ['verify'] + ); + + // Convert payload and signature to ArrayBuffers + const payloadBuffer = new TextEncoder().encode(payload); + const signatureBuffer = this.base64ToArrayBuffer(signature); + + // Verify the signature + const isValid = await crypto.subtle.verify( + { + name: 'ECDSA', + hash: 'SHA-256', + }, + publicKey, + signatureBuffer, + payloadBuffer + ); + + console.log(`Software signature verification for ${keyId}:`, isValid); + return isValid; + } catch (error) { + console.error('Software signature verification failed:', error); + if (error instanceof KeyManagerError) { + throw error; + } + throw new KeyManagerError( + 'Failed to verify signature with software key', + KeyManagerErrorCodes.VERIFICATION_FAILED, + keyId + ); + } + } + + private async getKeyPair(keyId: string): Promise { + try { + const storageKey = this.getStorageKey(keyId); + const stored = localStorage.getItem(storageKey); + if (!stored) { + return null; + } + return JSON.parse(stored) as SoftwareKeyPair; + } catch (error) { + console.error('Failed to retrieve key pair from storage:', error); + return null; + } + } + + private getStorageKey(keyId: string): string { + return `${this.storageKey}-${this.keyPrefix}${keyId}`; + } + + private arrayBufferToBase64(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); + } + + private base64ToArrayBuffer(base64: string): ArrayBuffer { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes.buffer; + } + + private arrayBufferToHex(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + return Array.from(bytes) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + } +} diff --git a/infrastructure/eid-wallet/src/lib/crypto/index.ts b/infrastructure/eid-wallet/src/lib/crypto/index.ts new file mode 100644 index 00000000..033af0be --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/crypto/index.ts @@ -0,0 +1,6 @@ +// Export all crypto-related types and classes +export type { KeyManager, KeyManagerConfig, SoftwareKeyPair } from './types'; +export { KeyManagerError, KeyManagerErrorCodes } from './types'; +export { HardwareKeyManager } from './HardwareKeyManager'; +export { SoftwareKeyManager } from './SoftwareKeyManager'; +export { KeyManagerFactory } from './KeyManagerFactory'; \ No newline at end of file diff --git a/infrastructure/eid-wallet/src/lib/crypto/types.ts b/infrastructure/eid-wallet/src/lib/crypto/types.ts new file mode 100644 index 00000000..7d46cbf1 --- /dev/null +++ b/infrastructure/eid-wallet/src/lib/crypto/types.ts @@ -0,0 +1,77 @@ +/** + * Common interface for key management operations + * Abstracts hardware and software key storage implementations + */ +export interface KeyManager { + /** + * Check if a key exists for the given keyId + */ + exists(keyId: string): Promise; + + /** + * Generate a new key pair for the given keyId + */ + generate(keyId: string): Promise; + + /** + * Get the public key for the given keyId + */ + getPublicKey(keyId: string): Promise; + + /** + * Sign a payload with the given keyId + */ + signPayload(keyId: string, payload: string): Promise; + + /** + * Verify a signature with the given keyId and payload + */ + verifySignature(keyId: string, payload: string, signature: string): Promise; + + /** + * Get the type of key manager (hardware or software) + */ + getType(): 'hardware' | 'software'; +} + +/** + * Configuration for key managers + */ +export interface KeyManagerConfig { + keyId: string; + useHardware?: boolean; + preVerificationMode?: boolean; +} + +/** + * Key pair data structure for software storage + */ +export interface SoftwareKeyPair { + privateKey: string; + publicKey: string; + keyId: string; + createdAt: string; +} + +/** + * Error types for key management operations + */ +export class KeyManagerError extends Error { + constructor( + message: string, + public readonly code: string, + public readonly keyId?: string + ) { + super(message); + this.name = 'KeyManagerError'; + } +} + +export const KeyManagerErrorCodes = { + KEY_NOT_FOUND: 'KEY_NOT_FOUND', + KEY_GENERATION_FAILED: 'KEY_GENERATION_FAILED', + SIGNING_FAILED: 'SIGNING_FAILED', + VERIFICATION_FAILED: 'VERIFICATION_FAILED', + HARDWARE_UNAVAILABLE: 'HARDWARE_UNAVAILABLE', + STORAGE_ERROR: 'STORAGE_ERROR', +} as const; diff --git a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts index f2cbf8a7..efff0ec4 100644 --- a/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts +++ b/infrastructure/eid-wallet/src/lib/global/controllers/evault.ts @@ -104,28 +104,44 @@ export class VaultController { } /** - * Resolve eVault endpoint from registry + * Resolve eVault endpoint from registry with retry logic */ private async resolveEndpoint(w3id: string): Promise { - try { - const response = await axios.get( - new URL(`resolve?w3id=${w3id}`, PUBLIC_REGISTRY_URL).toString(), - ); - return new URL("/graphql", response.data.uri).toString(); - } catch (error) { - console.error("Error resolving eVault endpoint:", error); - throw new Error("Failed to resolve eVault endpoint"); + const maxRetries = 5; + let retryCount = 0; + + while (retryCount < maxRetries) { + try { + const response = await axios.get( + new URL(`resolve?w3id=${w3id}`, PUBLIC_REGISTRY_URL).toString(), + ); + return new URL("/graphql", response.data.uri).toString(); + } catch (error) { + retryCount++; + console.error(`Error resolving eVault endpoint (attempt ${retryCount}/${maxRetries}):`, error); + + if (retryCount >= maxRetries) { + throw new Error("Failed to resolve eVault endpoint after all retries"); + } + + // Wait before retrying (exponential backoff) + const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 10000); + console.log(`Waiting ${delay}ms before resolve retry...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + } } + + throw new Error("Failed to resolve eVault endpoint"); } /** - * Ensure we have a valid GraphQL client + * Create a new GraphQL client every time */ private async ensureClient(w3id: string): Promise { - if (!this.#endpoint || !this.#client) { - this.#endpoint = await this.resolveEndpoint(w3id); - this.#client = new GraphQLClient(this.#endpoint); - } + this.#endpoint = await this.resolveEndpoint(w3id); + this.#client = new GraphQLClient(this.#endpoint, { + timeout: 3000, // 3 second timeout for GraphQL requests + }); return this.#client; } @@ -211,13 +227,14 @@ export class VaultController { if (resolvedUser?.ename) { this.#store.set("vault", resolvedUser); // Set loading status - this.profileCreationStatus = "loading"; // Get user data for display name const userData = await this.#userController.user; const displayName = userData?.name || resolvedUser?.ename; try { + if (this.profileCreationStatus === "success") return + this.profileCreationStatus = "loading"; await this.createUserProfileInEVault( resolvedUser?.ename as string, displayName as string, @@ -240,6 +257,8 @@ export class VaultController { else if (vault?.ename) { this.#store.set("vault", vault); + + if (this.profileCreationStatus === "success") return // Set loading status this.profileCreationStatus = "loading"; diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte index 3ca20993..10846f69 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/main/+page.svelte @@ -1,77 +1,83 @@ {#if profileCreationStatus === "loading"} diff --git a/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte index eb5043b6..e25224ea 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/settings/+page.svelte @@ -126,7 +126,7 @@ on:click={handleVersionTap} disabled={isRetrying} > - Version v0.2.1.2 + Version v0.2.2.0 {#if retryMessage} diff --git a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte index f6070a62..288cc2e8 100644 --- a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte @@ -1,170 +1,198 @@
-import { goto } from "$app/navigation"; -import { - PUBLIC_PROVISIONER_URL, - PUBLIC_REGISTRY_URL, -} from "$env/static/public"; -import { Hero } from "$lib/fragments"; -import { GlobalState } from "$lib/global"; -import { ButtonAction } from "$lib/ui"; -import Drawer from "$lib/ui/Drawer/Drawer.svelte"; -import { capitalize } from "$lib/utils"; -import { - exists, - generate, - getPublicKey, - // signPayload, verifySignature -} from "@auvo/tauri-plugin-crypto-hw-api"; -import axios from "axios"; -import { getContext, onMount } from "svelte"; -import { Shadow } from "svelte-loading-spinners"; -import { v4 as uuidv4 } from "uuid"; -import Passport from "./steps/passport.svelte"; -import Selfie from "./steps/selfie.svelte"; -import { - DocFront, - Selfie as SelfiePic, - reason, - status, - verifStep, - verificaitonId, -} from "./store"; + import { goto } from "$app/navigation"; + import { + PUBLIC_PROVISIONER_URL, + PUBLIC_REGISTRY_URL, + } from "$env/static/public"; + import { Hero } from "$lib/fragments"; + import { GlobalState } from "$lib/global"; + import { ButtonAction } from "$lib/ui"; + import Drawer from "$lib/ui/Drawer/Drawer.svelte"; + import { capitalize } from "$lib/utils"; + import { KeyManagerFactory, type KeyManager } from "$lib/crypto"; + import axios from "axios"; + import { getContext, onMount } from "svelte"; + import { Shadow } from "svelte-loading-spinners"; + import { v4 as uuidv4 } from "uuid"; + import Passport from "./steps/passport.svelte"; + import Selfie from "./steps/selfie.svelte"; + import { + DocFront, + Selfie as SelfiePic, + reason, + status, + verifStep, + verificaitonId, + } from "./store"; -type Document = { - country: { value: string }; - firstIssue: Date; - licenseNumber: string; - number: { - confidenceCategory: string; - value: string; - sources: string[]; - }; - placeOfIssue: string; - processNumber: string; - residencePermitType: string; - type: { value: string }; - validFrom: { - confidenceCategory: string; - value: string; - sources: string[]; - }; - validUntil: { - confidenceCategory: string; - value: string; - sources: string[]; + type Document = { + country: { value: string }; + firstIssue: Date; + licenseNumber: string; + number: { + confidenceCategory: string; + value: string; + sources: string[]; + }; + placeOfIssue: string; + processNumber: string; + residencePermitType: string; + type: { value: string }; + validFrom: { + confidenceCategory: string; + value: string; + sources: string[]; + }; + validUntil: { + confidenceCategory: string; + value: string; + sources: string[]; + }; }; -}; -type Person = { - address: { - confidenceCategory: string; - value: string; - components: Record; - sources: string[]; - }; - dateOfBirth: { - confidenceCategory: string; - value: string; - sources: string[]; - }; - employer: string; - extraNames: string; - firstName: { - confidenceCategory: string; - value: string; - sources: string[]; - }; - foreignerStatus: string; - gender: { - confidenceCategory: string; - value: string; - sources: string[]; - }; - idNumber: { - confidenceCategory: string; - value: string; - sources: string[]; - }; - lastName: { - confidenceCategory: string; - value: string; - sources: string[]; - }; - nationality: { - confidenceCategory: string; - value: string; - sources: string[]; + type Person = { + address: { + confidenceCategory: string; + value: string; + components: Record; + sources: string[]; + }; + dateOfBirth: { + confidenceCategory: string; + value: string; + sources: string[]; + }; + employer: string; + extraNames: string; + firstName: { + confidenceCategory: string; + value: string; + sources: string[]; + }; + foreignerStatus: string; + gender: { + confidenceCategory: string; + value: string; + sources: string[]; + }; + idNumber: { + confidenceCategory: string; + value: string; + sources: string[]; + }; + lastName: { + confidenceCategory: string; + value: string; + sources: string[]; + }; + nationality: { + confidenceCategory: string; + value: string; + sources: string[]; + }; + occupation: string; + placeOfBirth: string; }; - occupation: string; - placeOfBirth: string; -}; -let globalState: GlobalState | undefined = $state(undefined); -let showVeriffModal = $state(false); -let person: Person; -let document: Document; -let loading = $state(false); + let globalState: GlobalState | undefined = $state(undefined); + let showVeriffModal = $state(false); + let person: Person; + let document: Document; + let loading = $state(false); + let keyManager: KeyManager | null = $state(null); + let websocketData: any = $state(null); // Store websocket data for duplicate case + let hardwareKeySupported = $state(false); + let hardwareKeyCheckComplete = $state(false); -async function handleVerification() { - const { data } = await axios.post( - new URL("/verification", PUBLIC_PROVISIONER_URL).toString(), - ); - verificaitonId.set(data.id); - showVeriffModal = true; - watchEventStream(data.id); -} + async function handleVerification() { + const { data } = await axios.post( + new URL("/verification", PUBLIC_PROVISIONER_URL).toString(), + ); + verificaitonId.set(data.id); + showVeriffModal = true; + watchEventStream(data.id); + } -function watchEventStream(id: string) { - const sseUrl = new URL( - `/verification/sessions/${id}`, - PUBLIC_PROVISIONER_URL, - ).toString(); - const eventSource = new EventSource(sseUrl); + function watchEventStream(id: string) { + const sseUrl = new URL( + `/verification/sessions/${id}`, + PUBLIC_PROVISIONER_URL, + ).toString(); + const eventSource = new EventSource(sseUrl); - eventSource.onopen = () => { - console.log("Successfully connected."); - }; + eventSource.onopen = () => { + console.log("Successfully connected."); + }; - eventSource.onmessage = (e) => { - const data = JSON.parse(e.data as string); - if (!data.status) console.log(data); - console.log("STATUS", data); - status.set(data.status); - reason.set(data.reason); - person = data.person; - document = data.document; - if (data.status === "resubmission_requested") { - DocFront.set(null); - SelfiePic.set(null); - } - verifStep.set(2); - }; -} + eventSource.onmessage = (e) => { + const data = JSON.parse(e.data as string); + if (!data.status) console.log(data); + console.log("STATUS", data); + status.set(data.status); + reason.set(data.reason); + person = data.person; + document = data.document; + websocketData = data; // Store the full websocket data + if (data.status === "resubmission_requested") { + DocFront.set(null); + SelfiePic.set(null); + } + verifStep.set(2); + }; + } + + // Check if hardware key is supported on this device + async function checkHardwareKeySupport() { + try { + const hardwareKeyManager = + await KeyManagerFactory.getKeyManagerForContext( + "default", + "verification", + ); -// IMO, call this function early, check if hardware even supports the app -// docs: https://github.com/auvoid/tauri-plugin-crypto-hw/blob/48d0b9db7083f9819766e7b3bfd19e39de9a77f3/examples/tauri-app/src/App.svelte#L13 -async function generateApplicationKeyPair() { - let res: string | undefined; - try { - res = await generate("default"); - console.log(res); - } catch (e) { - // Put hardware crypto missing error here - console.log(e); + // Try to generate a test key to see if hardware is available + await hardwareKeyManager.generate("test-hardware-check"); + hardwareKeySupported = true; + console.log("Hardware key is supported on this device"); + } catch (error) { + hardwareKeySupported = false; + console.log("Hardware key is NOT supported on this device:", error); + } finally { + hardwareKeyCheckComplete = true; + } } - return res; -} -async function getApplicationPublicKey() { - let res: string | undefined; - try { - res = await getPublicKey("default"); - console.log(res); - } catch (e) { - console.log(e); + // Initialize key manager for verification context + async function initializeKeyManager() { + try { + keyManager = await KeyManagerFactory.getKeyManagerForContext( + "default", + "verification", + ); + console.log(`Key manager initialized: ${keyManager.getType()}`); + return keyManager; + } catch (error) { + console.error("Failed to initialize key manager:", error); + throw error; + } } - return res; // check getPublicKey doc comments (multibase hex format) -} -let handleContinue: () => Promise; + async function generateApplicationKeyPair() { + if (!keyManager) { + await initializeKeyManager(); + } + + try { + const res = await keyManager!.generate("default"); + console.log("Key generation result:", res); + return res; + } catch (e) { + console.error("Key generation failed:", e); + throw e; + } + } -onMount(() => { - globalState = getContext<() => GlobalState>("globalState")(); - // handle verification logic + sec user data in the store + async function getApplicationPublicKey() { + if (!keyManager) { + await initializeKeyManager(); + } - // check if default key pair exists - const keyExists = exists("default"); - if (!keyExists) { - generateApplicationKeyPair(); + try { + const res = await keyManager!.getPublicKey("default"); + console.log("Public key retrieved:", res); + return res; + } catch (e) { + console.error("Public key retrieval failed:", e); + throw e; + } } - handleContinue = async () => { - if ($status !== "approved") return verifStep.set(0); - if (!globalState) throw new Error("Global state is not defined"); + let handleContinue: () => Promise = $state(async () => {}); - loading = true; - globalState.userController.user = { - name: capitalize( - `${person.firstName.value} ${person.lastName.value}`, - ), - "Date of Birth": new Date(person.dateOfBirth.value).toDateString(), - "ID submitted": `Passport - ${person.nationality.value}`, - "Passport Number": document.number.value, - }; - globalState.userController.document = { - "Valid From": new Date(document.validFrom.value).toDateString(), - "Valid Until": new Date(document.validUntil.value).toDateString(), - "Verified On": new Date().toDateString(), - }; - globalState.userController.isFake = false; - const { - data: { token: registryEntropy }, - } = await axios.get( - new URL("/entropy", PUBLIC_REGISTRY_URL).toString(), - ); - const { data } = await axios.post( - new URL("/provision", PUBLIC_PROVISIONER_URL).toString(), - { - registryEntropy, - namespace: uuidv4(), - verificationId: $verificaitonId, - publicKey: await getApplicationPublicKey(), - }, - ); - if (data.success === true) { - globalState.vaultController.vault = { - uri: data.uri, - ename: data.w3id, - }; + onMount(async () => { + globalState = getContext<() => GlobalState>("globalState")(); + // handle verification logic + sec user data in the store + + // Check hardware key support first + await checkHardwareKeySupport(); + + // Initialize key manager and check if default key pair exists + await initializeKeyManager(); + const keyExists = await keyManager!.exists("default"); + if (!keyExists) { + await generateApplicationKeyPair(); } - setTimeout(() => { - goto("/register"); - }, 10_000); - }; -}); + + handleContinue = async () => { + if ($status !== "approved" && $status !== "duplicate") + return verifStep.set(0); + if (!globalState) throw new Error("Global state is not defined"); + + loading = true; + globalState.userController.user = { + name: capitalize( + `${person.firstName.value} ${person.lastName.value}`, + ), + "Date of Birth": new Date( + person.dateOfBirth.value, + ).toDateString(), + "ID submitted": `Passport - ${person.nationality.value}`, + "Passport Number": document.number.value, + }; + globalState.userController.document = { + "Valid From": new Date(document.validFrom.value).toDateString(), + "Valid Until": new Date( + document.validUntil.value, + ).toDateString(), + "Verified On": new Date().toDateString(), + }; + globalState.userController.isFake = false; + + if ($status === "duplicate") { + // For duplicate case, skip provision and resolve the existing eVault URI + // The w3id should be provided in the websocket data + const existingW3id = websocketData?.w3id; // This should come from the websocket data + if (!existingW3id) { + throw new Error("No w3id provided for duplicate eVault"); + } + + // Resolve the eVault URI from the registry + const response = await axios.get( + new URL( + `resolve?w3id=${existingW3id}`, + PUBLIC_REGISTRY_URL, + ).toString(), + ); + // Skip profile creation for duplicates by setting status directly + globalState.vaultController.profileCreationStatus = "success"; + // For duplicates, just set the vault without triggering profile creation + // since the eVault already exists with a profile + globalState.vaultController.vault = { + uri: response.data.uri, + ename: existingW3id, + }; + } else { + // Normal flow for approved status + const { + data: { token: registryEntropy }, + } = await axios.get( + new URL("/entropy", PUBLIC_REGISTRY_URL).toString(), + ); + const { data } = await axios.post( + new URL("/provision", PUBLIC_PROVISIONER_URL).toString(), + { + registryEntropy, + namespace: uuidv4(), + verificationId: $verificaitonId, + publicKey: await getApplicationPublicKey(), + }, + ); + if (data.success === true) { + // Set vault in controller - this will trigger profile creation with retry logic + globalState.vaultController.vault = { + uri: data.uri, + ename: data.w3id, + }; + } + } + + setTimeout(() => { + goto("/register"); + }, 10_000); + }; + });
{ passport - I'm ready + {#if !hardwareKeyCheckComplete} +
+
+
+ {:else if !hardwareKeySupported} +
+

+ Hardware Security Not Available +

+

+ Your device doesn't support hardware-backed security keys + required for identity verification. Please use a device with + hardware security support or try the pre-verification option. +

+
+ {:else} + I'm ready + {/if}
{#if $verifStep === 0} @@ -261,6 +361,15 @@ onMount(() => {

Your verification was a success

You can now continue on to create your eName

+ {:else if $status === "duplicate"} +
+

Old eVault Found

+

+ We found an existing eVault associated with your + identity. You can claim it back to continue + using your account. +

+
{:else if $status === "resubmission_requested"}

Your verification failed due to the reason

{$reason}

@@ -278,7 +387,9 @@ onMount(() => { color="primary" >{$status === "approved" ? "Continue" - : "Retry"} {/if} diff --git a/infrastructure/evault-provisioner/src/config/database.ts b/infrastructure/evault-provisioner/src/config/database.ts index 2ae7b03c..44b2a807 100644 --- a/infrastructure/evault-provisioner/src/config/database.ts +++ b/infrastructure/evault-provisioner/src/config/database.ts @@ -8,7 +8,7 @@ dotenv.config({ path: join(__dirname, "../../../../.env") }) export const AppDataSource = new DataSource({ type: "postgres", - url: process.env.PROVISIONER_DATABASE_URL || "postgresql://postgres:postgres@localhost:5432/evault", + url: process.env.PROVISIONER_DATABASE_URL || "postgresql://postgres:postgres@localhost:5432/provisioner", logging: process.env.NODE_ENV !== "production", entities: [Verification], migrations: [join(__dirname, "../migrations/*.{ts,js}")], diff --git a/infrastructure/evault-provisioner/src/controllers/VerificationController.ts b/infrastructure/evault-provisioner/src/controllers/VerificationController.ts index 4f59fac7..30b91b66 100644 --- a/infrastructure/evault-provisioner/src/controllers/VerificationController.ts +++ b/infrastructure/evault-provisioner/src/controllers/VerificationController.ts @@ -118,6 +118,7 @@ export class VerificationController { // Create new verification app.post("/verification", async (req: Request, res: Response) => { + console.log("creating new session") const { referenceId } = req.body; if (referenceId) { @@ -193,9 +194,11 @@ export class VerificationController { const body = req.body; console.log(body); const id = body.vendorData; + let w3id: string | null = null const verification = await this.verificationService.findById(id); + console.log("ID", id) if (!verification) { return res .status(404) @@ -227,10 +230,10 @@ export class VerificationController { }); console.log("matched", verificationMatch); if (verificationMatch) { - approved = false; - status = "declined"; + status = "duplicate"; reason = "Document already used to create an eVault"; + w3id = verificationMatch.linkedEName } } console.log(body.data.verification.document); @@ -248,6 +251,7 @@ export class VerificationController { eventEmitter.emit(id, { reason, status, + w3id, person: body.data.verification.person ?? null, document: body.data.verification.document, }); diff --git a/infrastructure/evault-provisioner/src/entities/Verification.ts b/infrastructure/evault-provisioner/src/entities/Verification.ts index 7b258309..a9cfcb88 100644 --- a/infrastructure/evault-provisioner/src/entities/Verification.ts +++ b/infrastructure/evault-provisioner/src/entities/Verification.ts @@ -29,6 +29,9 @@ export class Verification { @Column({ default: false }) consumed!: boolean; + @Column({ nullable: true }) + linkedEName!: string; + @CreateDateColumn() createdAt!: Date; diff --git a/infrastructure/evault-provisioner/src/index.ts b/infrastructure/evault-provisioner/src/index.ts index 5673a9db..80a02c29 100644 --- a/infrastructure/evault-provisioner/src/index.ts +++ b/infrastructure/evault-provisioner/src/index.ts @@ -127,6 +127,7 @@ app.post( "This verification ID has already been used" ); } + await verificationService.findByIdAndUpdate(verificationId, { linkedEName: w3id }); const evaultId = await new W3IDBuilder().withGlobal(true).build(); const uri = await provisionEVault( w3id, diff --git a/infrastructure/evault-provisioner/src/migrations/1758389959600-migration.ts b/infrastructure/evault-provisioner/src/migrations/1758389959600-migration.ts new file mode 100644 index 00000000..2dac460d --- /dev/null +++ b/infrastructure/evault-provisioner/src/migrations/1758389959600-migration.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Migration1758389959600 implements MigrationInterface { + name = 'Migration1758389959600' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "verification" ADD "linkedEName" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "verification" DROP COLUMN "linkedEName"`); + } + +} diff --git a/platforms/cerberus/src/controllers/WebhookController.ts b/platforms/cerberus/src/controllers/WebhookController.ts index 6abd5167..5c096aff 100644 --- a/platforms/cerberus/src/controllers/WebhookController.ts +++ b/platforms/cerberus/src/controllers/WebhookController.ts @@ -171,13 +171,24 @@ export class WebhookController { console.log("Charter change detected, notifying Cerberus..."); console.log("Old charter:", oldCharter ? "exists" : "none"); console.log("New charter:", newCharter ? "exists" : "none"); + console.log("🔍 About to call processCharterChange..."); - await this.cerberusTriggerService.processCharterChange( - group.id, - group.name, - oldCharter, - newCharter - ); + try { + await this.cerberusTriggerService.processCharterChange( + group.id, + group.name, + oldCharter, + newCharter + ); + console.log("✅ processCharterChange completed successfully"); + } catch (error) { + console.error("❌ Error in processCharterChange:", error); + } + } else { + console.log("No charter change detected, skipping Cerberus notification"); + console.log("newCharter !== undefined:", newCharter !== undefined); + console.log("newCharter !== null:", newCharter !== null); + console.log("oldCharter !== newCharter:", oldCharter !== newCharter); } } else { console.log("Creating new group"); @@ -203,12 +214,21 @@ export class WebhookController { // Check if new group has a charter and send Cerberus welcome message if (group.charter) { console.log("New group with charter detected, sending Cerberus welcome..."); - await this.cerberusTriggerService.processCharterChange( - group.id, - group.name, - undefined, // No old charter for new groups - group.charter - ); + console.log("🔍 About to call processCharterChange for new group..."); + + try { + await this.cerberusTriggerService.processCharterChange( + group.id, + group.name, + undefined, // No old charter for new groups + group.charter + ); + console.log("✅ processCharterChange for new group completed successfully"); + } catch (error) { + console.error("❌ Error in processCharterChange for new group:", error); + } + } else { + console.log("New group has no charter, skipping Cerberus welcome"); } } } else if (mapping.tableName === "messages") { diff --git a/platforms/cerberus/src/services/CerberusTriggerService.ts b/platforms/cerberus/src/services/CerberusTriggerService.ts index b616af46..8b610e21 100644 --- a/platforms/cerberus/src/services/CerberusTriggerService.ts +++ b/platforms/cerberus/src/services/CerberusTriggerService.ts @@ -51,30 +51,43 @@ export class CerberusTriggerService { */ async isCerberusEnabled(groupId: string): Promise { try { + console.log(`🔍 Checking if Cerberus is enabled for group: ${groupId}`); const group = await this.groupService.getGroupById(groupId); if (!group || !group.charter) { + console.log(`🔍 Group or charter not found: group=${!!group}, charter=${!!group?.charter}`); return false; } + console.log(`🔍 Group found with charter length: ${group.charter.length}`); + // Check if the watchdog name is specifically set to "Cerberus" const charterText = group.charter.toLowerCase(); + console.log(`🔍 Charter text (first 200 chars): ${charterText.substring(0, 200)}...`); // Look for "Watchdog Name:" followed by "**Cerberus**" on next line (handles markdown) const watchdogNameMatch = charterText.match(/watchdog name:\s*\n\s*\*\*([^*]+)\*\*/); if (watchdogNameMatch) { const watchdogName = watchdogNameMatch[1].trim(); - return watchdogName === 'cerberus'; + console.log(`🔍 Found watchdog name (multi-line): "${watchdogName}"`); + const result = watchdogName === 'cerberus'; + console.log(`🔍 Multi-line match result: ${result}`); + return result; } // Alternative: look for "Watchdog Name: Cerberus" on same line const sameLineMatch = charterText.match(/watchdog name:\s*([^\n\r]+)/); if (sameLineMatch) { const watchdogName = sameLineMatch[1].trim(); - return watchdogName === 'cerberus'; + console.log(`🔍 Found watchdog name (same-line): "${watchdogName}"`); + const result = watchdogName === 'cerberus'; + console.log(`🔍 Same-line match result: ${result}`); + return result; } // Fallback: check if "Watchdog Name: Cerberus" appears anywhere - return charterText.includes('watchdog name: cerberus'); + const fallbackResult = charterText.includes('watchdog name: cerberus'); + console.log(`🔍 Fallback check result: ${fallbackResult}`); + return fallbackResult; } catch (error) { console.error("Error checking if Cerberus is enabled for group:", error); return false; @@ -124,8 +137,14 @@ export class CerberusTriggerService { */ async processCharterChange(groupId: string, groupName: string, oldCharter: string | undefined, newCharter: string): Promise { try { + console.log(`🔍 Processing charter change for group: ${groupId} (${groupName})`); + console.log(`🔍 Old charter: ${oldCharter ? 'exists' : 'none'}`); + console.log(`🔍 New charter: ${newCharter ? 'exists' : 'none'}`); + // Check if Cerberus is enabled for this group const cerberusEnabled = await this.isCerberusEnabled(groupId); + console.log(`🔍 Cerberus enabled check result: ${cerberusEnabled}`); + if (!cerberusEnabled) { console.log(`Cerberus not enabled for group ${groupId} - skipping charter change processing`); return; @@ -141,6 +160,8 @@ export class CerberusTriggerService { changeType = 'updated'; } + console.log(`🔍 Change type determined: ${changeType}`); + // Create a system message about the charter change const changeMessage = `$$system-message$$ Cerberus: Group charter has been ${changeType}. ${ changeType === 'created' ? 'New charter is now in effect.' : @@ -148,14 +169,19 @@ export class CerberusTriggerService { 'Charter has been updated and new rules are now in effect.' }`; - await this.messageService.createSystemMessageWithoutPrefix({ + console.log(`🔍 Creating system message: ${changeMessage.substring(0, 100)}...`); + + const systemMessage = await this.messageService.createSystemMessageWithoutPrefix({ text: changeMessage, groupId: groupId, }); + console.log(`✅ System message created successfully with ID: ${systemMessage.id}`); + // If charter was updated, also handle signature invalidation and detailed analysis if (changeType === 'updated' && oldCharter && newCharter) { try { + console.log(`🔍 Handling charter update with signature invalidation...`); // Import CharterSignatureService dynamically to avoid circular dependencies const { CharterSignatureService } = await import('./CharterSignatureService'); const charterSignatureService = new CharterSignatureService(); @@ -166,6 +192,7 @@ export class CerberusTriggerService { newCharter, this.messageService ); + console.log(`✅ Charter signature invalidation completed`); } catch (error) { console.error("Error handling charter signature invalidation:", error); }