Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { RootStackParamList } from '@/navigation';
* Uses a small delay to allow store updates to propagate after navigation
* (e.g., after QR code scan sets selfApp data).
*/
export function useSelfAppStalenessCheck(
export function useProofDisclosureStalenessCheck(
selfApp: SelfApp | null,
disclosureItems: Array<{ key: string; text: string }>,
navigation: NativeStackNavigationProp<RootStackParamList>,
Expand Down
30 changes: 6 additions & 24 deletions app/src/providers/authProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ import {
import { trackEvent } from '@/services/analytics';
import { useSettingStore } from '@/stores/settingStore';
import type { Mnemonic } from '@/types/mnemonic';
import {
getKeychainErrorIdentity,
isKeychainCryptoError,
isUserCancellation,
} from '@/utils/keychainErrors';

const SERVICE_NAME = 'secret';

Expand Down Expand Up @@ -151,29 +156,6 @@ let keychainCryptoFailureCallback:
| ((errorType: 'user_cancelled' | 'crypto_failed') => void)
| null = null;

function isUserCancellation(error: unknown): boolean {
const err = error as { code?: string; message?: string };
return Boolean(
err?.code === 'E_AUTHENTICATION_FAILED' ||
err?.code === 'USER_CANCELED' ||
err?.message?.includes('User canceled') ||
err?.message?.includes('Authentication canceled') ||
err?.message?.includes('cancelled by user'),
);
}

function isKeychainCryptoError(error: unknown): boolean {
const err = error as { code?: string; name?: string; message?: string };
return Boolean(
(err?.code === 'E_CRYPTO_FAILED' ||
err?.name === 'com.oblador.keychain.exceptions.CryptoFailedException' ||
err?.message?.includes('CryptoFailedException') ||
err?.message?.includes('Decryption failed') ||
err?.message?.includes('Authentication tag verification failed')) &&
!isUserCancellation(error),
);
}

async function loadOrCreateMnemonic(
keychainOptions: KeychainOptions,
): Promise<string | false> {
Expand Down Expand Up @@ -214,7 +196,7 @@ async function loadOrCreateMnemonic(
}

if (isKeychainCryptoError(error)) {
const err = error as { code?: string; name?: string };
const err = getKeychainErrorIdentity(error);
console.error('Keychain crypto error:', {
code: err?.code,
name: err?.name,
Expand Down
109 changes: 48 additions & 61 deletions app/src/providers/passportDataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ import { getAllDocuments, useSelfClient } from '@selfxyz/mobile-sdk-alpha';

import { createKeychainOptions } from '@/integrations/keychain';
import { unsafe_getPrivateKey, useAuth } from '@/providers/authProvider';
import type { KeychainErrorType } from '@/utils/keychainErrors';
import {
getKeychainErrorIdentity,
isKeychainCryptoError,
isUserCancellation,
} from '@/utils/keychainErrors';

let keychainCryptoFailureCallback:
| ((errorType: 'user_cancelled' | 'crypto_failed') => void)
Expand All @@ -78,29 +84,41 @@ export function setPassportKeychainErrorCallback(
keychainCryptoFailureCallback = callback;
}

function isUserCancellation(error: unknown): boolean {
const err = error as { code?: string; message?: string };
// User cancelled biometric/PIN authentication
return Boolean(
err?.code === 'E_AUTHENTICATION_FAILED' ||
err?.code === 'USER_CANCELED' ||
err?.message?.includes('User canceled') ||
err?.message?.includes('Authentication canceled') ||
err?.message?.includes('cancelled by user'),
);
function notifyKeychainFailure(type: KeychainErrorType) {
if (keychainCryptoFailureCallback) {
keychainCryptoFailureCallback(type);
}
}

function isKeychainCryptoError(error: unknown): boolean {
const err = error as { code?: string; name?: string; message?: string };
// Only true crypto failures, not user cancellations
return Boolean(
(err?.code === 'E_CRYPTO_FAILED' ||
err?.name === 'com.oblador.keychain.exceptions.CryptoFailedException' ||
err?.message?.includes('CryptoFailedException') ||
err?.message?.includes('Decryption failed') ||
err?.message?.includes('Authentication tag verification failed')) &&
!isUserCancellation(error),
);
function handleKeychainReadError({
contextLabel,
error,
throwOnUserCancel = false,
}: {
contextLabel: string;
error: unknown;
throwOnUserCancel?: boolean;
}) {
if (isUserCancellation(error)) {
console.log(`User cancelled authentication for ${contextLabel}`);
notifyKeychainFailure('user_cancelled');

if (throwOnUserCancel) {
throw error;
}
}

if (isKeychainCryptoError(error)) {
const err = getKeychainErrorIdentity(error);
console.error(`Keychain crypto error loading ${contextLabel}:`, {
code: err?.code,
name: err?.name,
});

notifyKeychainFailure('crypto_failed');
}

console.log(`Error loading ${contextLabel}:`, error);
}
Comment on lines +93 to 122
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential double logging of errors.

Line 121 logs every error unconditionally, but user cancellations are already logged at line 103 and crypto errors at lines 113-116. This results in duplicate log entries for recognized error types.

Consider moving the generic log to an else branch or removing it for already-handled cases.

🔧 Suggested fix
   if (isKeychainCryptoError(error)) {
     const err = getKeychainErrorIdentity(error);
     console.error(`Keychain crypto error loading ${contextLabel}:`, {
       code: err?.code,
       name: err?.name,
     });
 
     notifyKeychainFailure('crypto_failed');
+    return;
   }
 
-  console.log(`Error loading ${contextLabel}:`, error);
+  // Log unrecognized errors only
+  console.warn(`Unhandled error loading ${contextLabel}:`, error);
 }
🤖 Prompt for AI Agents
In @app/src/providers/passportDataProvider.tsx around lines 93 - 122, In
handleKeychainReadError, avoid double-logging by making the final generic
console.log conditional: check if error was handled by isUserCancellation or
isKeychainCryptoError (or simply return after handling those cases) so the
generic log only runs for unrecognized errors; update the function around the
isUserCancellation and isKeychainCryptoError branches (and any throwOnUserCancel
behavior) to either return after handling or use an else/else-if before the
final console.log, and keep notifyKeychainFailure calls unchanged.


// Create safe wrapper functions to prevent undefined errors during early initialization
Expand Down Expand Up @@ -482,25 +500,10 @@ export async function loadDocumentByIdDirectlyFromKeychain(
return JSON.parse(documentCreds.password);
}
} catch (error) {
if (isUserCancellation(error)) {
console.log(`User cancelled authentication for document ${documentId}`);
if (keychainCryptoFailureCallback) {
keychainCryptoFailureCallback('user_cancelled');
}
}

if (isKeychainCryptoError(error)) {
const err = error as { code?: string; name?: string };
console.error(`Keychain crypto error loading document ${documentId}:`, {
code: err?.code,
name: err?.name,
});

if (keychainCryptoFailureCallback) {
keychainCryptoFailureCallback('crypto_failed');
}
}
console.log(`Error loading document ${documentId}:`, error);
handleKeychainReadError({
contextLabel: `document ${documentId}`,
error,
});
}
return null;
}
Expand Down Expand Up @@ -544,27 +547,11 @@ export async function loadDocumentCatalogDirectlyFromKeychain(): Promise<Documen
return parsed;
}
} catch (error) {
if (isUserCancellation(error)) {
console.log('User cancelled authentication for document catalog');
if (keychainCryptoFailureCallback) {
keychainCryptoFailureCallback('user_cancelled');
}

throw error;
}

if (isKeychainCryptoError(error)) {
const err = error as { code?: string; name?: string };
console.error('Keychain crypto error loading document catalog:', {
code: err?.code,
name: err?.name,
});

if (keychainCryptoFailureCallback) {
keychainCryptoFailureCallback('crypto_failed');
}
}
console.log('Error loading document catalog:', error);
handleKeychainReadError({
contextLabel: 'document catalog',
error,
throwOnUserCancel: true,
});
}

// Return empty catalog if none exists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import {
WalletAddressModal,
} from '@/components/proof-request';
import { useSelfAppData } from '@/hooks/useSelfAppData';
import { useSelfAppStalenessCheck } from '@/hooks/useSelfAppStalenessCheck';
import type { RootStackParamList } from '@/navigation';
import { usePassport } from '@/providers/passportDataProvider';
import { getDocumentTypeName } from '@/utils/documentUtils';
Expand Down Expand Up @@ -120,9 +119,6 @@ const DocumentSelectorForProvingScreen: React.FC = () => {
const { logoSource, url, formattedUserId, disclosureItems } =
useSelfAppData(selfApp);

// Check for stale data and navigate to Home if needed
useSelfAppStalenessCheck(selfApp, disclosureItems, navigation);

const [documentCatalog, setDocumentCatalog] = useState<DocumentCatalog>({
documents: [],
});
Expand Down
7 changes: 0 additions & 7 deletions app/src/screens/verification/ProveScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import {
WalletAddressModal,
} from '@/components/proof-request';
import { useSelfAppData } from '@/hooks/useSelfAppData';
import { useSelfAppStalenessCheck } from '@/hooks/useSelfAppStalenessCheck';
import { buttonTap } from '@/integrations/haptics';
import type { RootStackParamList } from '@/navigation';
import {
Expand Down Expand Up @@ -73,12 +72,6 @@ const ProveScreen: React.FC = () => {
const { logoSource, url, formattedUserId, disclosureItems } =
useSelfAppData(selectedApp);

// Check for stale data and navigate to Home if needed
useSelfAppStalenessCheck(
selectedApp,
disclosureItems,
navigation as NativeStackNavigationProp<RootStackParamList>,
);
const selectedAppRef = useRef<typeof selectedApp>(null);
const processedSessionsRef = useRef<Set<string>>(new Set());

Expand Down
9 changes: 6 additions & 3 deletions app/src/stores/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const STALE_PROOF_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes

SQLite.enablePromise(true);

const toInsertId = (result: SQLite.ResultSet) =>
result.insertId ? result.insertId.toString() : '0';

async function openDatabase() {
return SQLite.openDatabase({
name: DB_NAME,
Expand Down Expand Up @@ -129,7 +132,7 @@ export const database: ProofDB = {
);
// Handle case where INSERT OR IGNORE skips insertion due to duplicate sessionId
return {
id: insertResult.insertId ? insertResult.insertId.toString() : '0',
id: toInsertId(insertResult),
timestamp,
rowsAffected: insertResult.rowsAffected,
};
Expand Down Expand Up @@ -157,7 +160,7 @@ export const database: ProofDB = {
);
// Handle case where INSERT OR IGNORE skips insertion due to duplicate sessionId
return {
id: insertResult.insertId ? insertResult.insertId.toString() : '0',
id: toInsertId(insertResult),
timestamp,
rowsAffected: insertResult.rowsAffected,
};
Expand Down Expand Up @@ -186,7 +189,7 @@ export const database: ProofDB = {
);
// Handle case where INSERT OR IGNORE skips insertion due to duplicate sessionId
return {
id: insertResult.insertId ? insertResult.insertId.toString() : '0',
id: toInsertId(insertResult),
timestamp,
rowsAffected: insertResult.rowsAffected,
};
Expand Down
46 changes: 46 additions & 0 deletions app/src/utils/keychainErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.

export type KeychainErrorIdentity = {
code?: string;
name?: string;
};

type KeychainError = {
code?: string;
message?: string;
name?: string;
};

export type KeychainErrorType = 'user_cancelled' | 'crypto_failed';

export function getKeychainErrorIdentity(
error: unknown,
): KeychainErrorIdentity {
const err = error as KeychainError;
return { code: err?.code, name: err?.name };
}

export function isKeychainCryptoError(error: unknown): boolean {
const err = error as KeychainError;
return Boolean(
(err?.code === 'E_CRYPTO_FAILED' ||
err?.name === 'com.oblador.keychain.exceptions.CryptoFailedException' ||
err?.message?.includes('CryptoFailedException') ||
err?.message?.includes('Decryption failed') ||
err?.message?.includes('Authentication tag verification failed')) &&
!isUserCancellation(error),
);
}

export function isUserCancellation(error: unknown): boolean {
const err = error as KeychainError;
return Boolean(
err?.code === 'E_AUTHENTICATION_FAILED' ||
err?.code === 'USER_CANCELED' ||
err?.message?.includes('User canceled') ||
err?.message?.includes('Authentication canceled') ||
err?.message?.includes('cancelled by user'),
);
}
Loading