From c763b79c3cfb7537db4cf1c832e82edcd589873d Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Sat, 3 May 2025 08:51:06 +0200 Subject: [PATCH] Extract service icons during both credential create and update (#771) --- .../src/utils/EncryptionUtility.ts | 5 ++- .../app/(tabs)/credentials/add-edit.tsx | 40 +++++++++---------- .../Autofill/CredentialIdentityStore.swift | 11 +++-- apps/mobile-app/utils/EncryptionUtility.tsx | 4 +- apps/mobile-app/utils/SqliteClient.tsx | 14 +++---- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/apps/browser-extension/src/utils/EncryptionUtility.ts b/apps/browser-extension/src/utils/EncryptionUtility.ts index 49fdb4bb9..24539fe9d 100644 --- a/apps/browser-extension/src/utils/EncryptionUtility.ts +++ b/apps/browser-extension/src/utils/EncryptionUtility.ts @@ -123,8 +123,9 @@ export class EncryptionUtility { */ public static async generateRsaKeyPair(): Promise<{ publicKey: string, privateKey: string }> { /** - * TODO: ensure the key pair is generated in the correct format where private key is in expected - * JWK format that the WASM app already outputs. + * TODO: this method is currently unused. When we enable the browser extension to actually generate keys, + * check if the key pair is generated in the correct format where private key is in expected JWK format + * that the WASM app already outputs. */ const keyPair = await crypto.subtle.generateKey( { diff --git a/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx b/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx index 248848f30..4e35667ec 100644 --- a/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx +++ b/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx @@ -120,8 +120,8 @@ export default function AddEditCredentialScreen() { } // Assemble the credential to save - let credentialToSave = { - Id: isEditMode ? id : undefined, + let credentialToSave: Credential = { + Id: isEditMode ? id : '', Username: watch('Username'), Password: watch('Password'), ServiceName: watch('ServiceName'), @@ -139,28 +139,28 @@ export default function AddEditCredentialScreen() { // Convert user birthdate entry format (yyyy-mm-dd) into valid ISO 8601 format for database storage credentialToSave.Alias.BirthDate = IdentityHelperUtils.normalizeBirthDateForDb(credentialToSave.Alias.BirthDate); + // Extract favicon from service URL if the credential has one + if (credentialToSave.ServiceUrl) { + try { + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Favicon extraction timed out')), 5000) + ); + + const faviconPromise = webApi.get('Favicon/Extract?url=' + credentialToSave.ServiceUrl); + const faviconResponse = await Promise.race([faviconPromise, timeoutPromise]) as FaviconExtractModel; + if (faviconResponse?.image) { + const decodedImage = Uint8Array.from(Buffer.from(faviconResponse.image as string, 'base64')); + credentialToSave.Logo = decodedImage; + } + } catch (error) { + console.log('Favicon extraction failed or timed out:', error); + } + } + await executeVaultMutation(async () => { if (isEditMode) { await dbContext.sqliteClient!.updateCredentialById(credentialToSave); } else { - // For new credentials, try to extract favicon - if (credentialToSave.ServiceUrl) { - try { - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Favicon extraction timed out')), 5000) - ); - - const faviconPromise = webApi.get('Favicon/Extract?url=' + credentialToSave.ServiceUrl); - const faviconResponse = await Promise.race([faviconPromise, timeoutPromise]) as FaviconExtractModel; - if (faviconResponse?.image) { - const decodedImage = Uint8Array.from(Buffer.from(faviconResponse.image as string, 'base64')); - credentialToSave.Logo = decodedImage; - } - } catch (error) { - console.log('Favicon extraction failed or timed out:', error); - } - } - const credentialId = await dbContext.sqliteClient!.createCredential(credentialToSave); credentialToSave.Id = credentialId; } diff --git a/apps/mobile-app/ios/Autofill/CredentialIdentityStore.swift b/apps/mobile-app/ios/Autofill/CredentialIdentityStore.swift index afa24b7e4..1824a4e40 100644 --- a/apps/mobile-app/ios/Autofill/CredentialIdentityStore.swift +++ b/apps/mobile-app/ios/Autofill/CredentialIdentityStore.swift @@ -23,7 +23,7 @@ class CredentialIdentityStore { guard let username = credential.username, !username.isEmpty else { return nil } - + let effectiveDomain = Self.effectiveDomain(from: host) return ASPasswordCredentialIdentity( @@ -43,7 +43,7 @@ class CredentialIdentityStore { print("Credential identity store is not enabled.") return } - + do { try await store.saveCredentialIdentities(identities) } catch { @@ -65,14 +65,13 @@ class CredentialIdentityStore { return ASPasswordCredentialIdentity( serviceIdentifier: serviceIdentifier, user: credential.username ?? "", - // TODO: Use the actual record identifier when implementing the actual vault - recordIdentifier: UUID().uuidString + recordIdentifier: credential.id.uuidString ) } try await store.removeCredentialIdentities(identities) } - + private func storeState() async -> ASCredentialIdentityStoreState { await withCheckedContinuation { continuation in store.getState { state in @@ -80,7 +79,7 @@ class CredentialIdentityStore { } } } - + private static func effectiveDomain(from host: String) -> String { let parts = host.split(separator: ".") guard parts.count >= 2 else { return host } diff --git a/apps/mobile-app/utils/EncryptionUtility.tsx b/apps/mobile-app/utils/EncryptionUtility.tsx index 5b8e6f216..244248922 100644 --- a/apps/mobile-app/utils/EncryptionUtility.tsx +++ b/apps/mobile-app/utils/EncryptionUtility.tsx @@ -110,8 +110,8 @@ class EncryptionUtility { * Generates a new RSA key pair for asymmetric encryption */ public static async generateRsaKeyPair(): Promise<{ publicKey: string, privateKey: string }> { -// TODO: ensure the key pair is generated in the correct format where private key is in expected - // JWK format that the WASM app already outputs. + // TODO: this method is currently unused. When we enable the app to actually generate keys, check if the key pair is + // generated in the correct format where private key is in expected JWK format that the WASM app already outputs. const keyPair = await crypto.subtle.generateKey( { name: "RSA-OAEP", diff --git a/apps/mobile-app/utils/SqliteClient.tsx b/apps/mobile-app/utils/SqliteClient.tsx index 5609a13ff..fd5af10db 100644 --- a/apps/mobile-app/utils/SqliteClient.tsx +++ b/apps/mobile-app/utils/SqliteClient.tsx @@ -559,7 +559,7 @@ class SqliteClient { try { /* * Check if TotpCodes table exists (for backward compatibility). - * TODO: whenever the browser extension has a minimum client DB version of 1.5.0+, + * TODO: whenever the mobile app has a minimum client DB version of 1.5.0+, * we can remove this check as the TotpCodes table then is guaranteed to exist. */ if (!await this.tableExists('TotpCodes')) { @@ -679,13 +679,11 @@ class SqliteClient { } // 1. Update Service - // TODO: make Logo update optional, currently not supported as its becoming null. - // Logo = ?, - const serviceQuery = ` UPDATE Services SET Name = ?, Url = ?, + Logo = COALESCE(?, Logo), UpdatedAt = ? WHERE Id = ( SELECT ServiceId @@ -693,12 +691,14 @@ class SqliteClient { WHERE Id = ? )`; - /*let logoData = null; + let logoData = null; try { if (credential.Logo) { + // Handle object-like array conversion if (typeof credential.Logo === 'object' && !ArrayBuffer.isView(credential.Logo)) { const values = Object.values(credential.Logo); logoData = new Uint8Array(values); + // Handle existing array types } else if (Array.isArray(credential.Logo) || credential.Logo instanceof ArrayBuffer || credential.Logo instanceof Uint8Array) { logoData = new Uint8Array(credential.Logo); } @@ -706,12 +706,12 @@ class SqliteClient { } catch (error) { console.warn('Failed to convert logo to Uint8Array:', error); logoData = null; - }*/ + } await this.executeUpdate(serviceQuery, [ credential.ServiceName, credential.ServiceUrl ?? null, - //logoData, + logoData, currentDateTime, credential.Id ]);