Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion app/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:extractNativeLibs="false"
tools:replace="android:icon, android:roundIcon, android:name, android:extractNativeLibs"
android:allowBackup="false"
tools:replace="android:icon, android:roundIcon, android:name, android:extractNativeLibs, android:allowBackup"
android:theme="@style/AppTheme"
android:supportsRtl="true"
android:usesCleartextTraffic="false"
Expand Down
17 changes: 13 additions & 4 deletions app/src/integrations/keychain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import type {
ACCESSIBLE,
GetOptions,
SECURITY_LEVEL,
SetOptions,
} from 'react-native-keychain';
import Keychain from 'react-native-keychain';

import { useSettingStore } from '@/stores/settingStore';
import type { ExtendedSetOptions } from '@/types/react-native-keychain';

/**
* Security configuration for keychain operations
*/
Expand All @@ -23,6 +25,8 @@ export interface AdaptiveSecurityConfig {
export interface GetSecureOptions {
requireAuth?: boolean;
promptMessage?: string;
/** Whether to use StrongBox-backed key generation on Android. Default: true */
useStrongBox?: boolean;
}

/**
Expand Down Expand Up @@ -61,7 +65,8 @@ export async function checkPasscodeAvailable(): Promise<boolean> {
await Keychain.setGenericPassword('test', 'test', {
service: testService,
accessible: Keychain.ACCESSIBLE.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY,
});
useStrongBox: false,
} as ExtendedSetOptions);
// Clean up test entry
await Keychain.resetGenericPassword({ service: testService });
return true;
Expand All @@ -78,18 +83,22 @@ export async function createKeychainOptions(
options: GetSecureOptions,
capabilities?: SecurityCapabilities,
): Promise<{
setOptions: SetOptions;
setOptions: ExtendedSetOptions;
getOptions: GetOptions;
}> {
const config = await getAdaptiveSecurityConfig(
options.requireAuth,
capabilities,
);

const setOptions: SetOptions = {
const useStrongBox =
options.useStrongBox ?? useSettingStore.getState().useStrongBox;

const setOptions: ExtendedSetOptions = {
accessible: config.accessible,
...(config.securityLevel && { securityLevel: config.securityLevel }),
...(config.accessControl && { accessControl: config.accessControl }),
useStrongBox,
Comment on lines +94 to +101
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 15, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential race condition with store rehydration.

useSettingStore.getState().useStrongBox accesses the persisted store synchronously. If createKeychainOptions is called early during app startup before AsyncStorage rehydration completes, it may read the default true instead of the user's persisted preference.

Consider checking useSettingStore.persist.hasHydrated() or subscribing to onRehydrateStorage if this function can be invoked during initial app boot.

🛠️ Potential safeguard
  const useStrongBox =
-   options.useStrongBox ?? useSettingStore.getState().useStrongBox;
+   options.useStrongBox ??
+   (useSettingStore.persist.hasHydrated()
+     ? useSettingStore.getState().useStrongBox
+     : true);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const useStrongBox =
options.useStrongBox ?? useSettingStore.getState().useStrongBox;
const setOptions: ExtendedSetOptions = {
accessible: config.accessible,
...(config.securityLevel && { securityLevel: config.securityLevel }),
...(config.accessControl && { accessControl: config.accessControl }),
useStrongBox,
const useStrongBox =
options.useStrongBox ??
(useSettingStore.persist.hasHydrated()
? useSettingStore.getState().useStrongBox
: true);
const setOptions: ExtendedSetOptions = {
accessible: config.accessible,
...(config.securityLevel && { securityLevel: config.securityLevel }),
...(config.accessControl && { accessControl: config.accessControl }),
useStrongBox,
🤖 Prompt for AI Agents
In `@app/src/integrations/keychain/index.ts` around lines 94 - 101,
createKeychainOptions reads useSettingStore.getState().useStrongBox
synchronously which can return a default value before AsyncStorage rehydration;
guard this by checking store hydration before falling back to getState (e.g.,
use useSettingStore.persist.hasHydrated() and only read getState() if hydrated,
or subscribe to onRehydrateStorage to defer resolving useStrongBox), and update
the logic around useStrongBox (the options.useStrongBox ?? ...) so it prefers
the explicit option, then the persisted value only when persist.hasHydrated() is
true, otherwise use a safe fallback until rehydration completes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That’s acceptable

Copy link
Contributor

Choose a reason for hiding this comment

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

@seshanthS, understood. I've noted your decision. The implementation will proceed with the current approach.


🧠 Learnings used
Learnt from: CR
Repo: selfxyz/self PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-25T14:06:55.970Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use Keychain for secure storage of sensitive data in React Native.

Learnt from: CR
Repo: selfxyz/self PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-25T14:06:55.970Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use AsyncStorage for simple data, SQLite for complex data, and Keychain for sensitive data in React Native.

Learnt from: transphorm
Repo: selfxyz/self PR: 1516
File: app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx:51-52
Timestamp: 2025-12-17T15:27:42.298Z
Learning: In app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx, ethers.Mnemonic.isValidMnemonic should not be used as a gate for pasting recovery phrases because it produces false negatives and rejects valid mnemonics. Allow users to paste any clipboard content; validation should only occur when the user attempts to restore the account.

};

const getOptions: GetOptions = {
Expand Down
32 changes: 31 additions & 1 deletion app/src/screens/dev/DevSettingsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import React, {
useState,
} from 'react';
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
import { Alert, ScrollView, TouchableOpacity } from 'react-native';
import { Alert, Platform, ScrollView, TouchableOpacity } from 'react-native';
import { Button, Sheet, Text, XStack, YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
Expand Down Expand Up @@ -399,6 +399,8 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
const subscribedTopics = useSettingStore(state => state.subscribedTopics);
const loggingSeverity = useSettingStore(state => state.loggingSeverity);
const setLoggingSeverity = useSettingStore(state => state.setLoggingSeverity);
const useStrongBox = useSettingStore(state => state.useStrongBox);
const setUseStrongBox = useSettingStore(state => state.setUseStrongBox);
const [hasNotificationPermission, setHasNotificationPermission] =
useState(false);
const paddingBottom = useSafeBottomPadding(20);
Expand Down Expand Up @@ -754,6 +756,34 @@ const DevSettingsScreen: React.FC<DevSettingsScreenProps> = ({}) => {
/>
</ParameterSection>

{Platform.OS === 'android' && (
<ParameterSection
icon={<BugIcon />}
title="Android Keystore"
description="Configure keystore security options"
>
<TopicToggleButton
label="Use StrongBox"
isSubscribed={useStrongBox}
onToggle={() => {
Alert.alert(
useStrongBox ? 'Disable StrongBox' : 'Enable StrongBox',
useStrongBox
? 'New keys will be generated without StrongBox hardware backing. Existing keys will continue to work.'
: 'New keys will attempt to use StrongBox hardware backing for enhanced security.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: useStrongBox ? 'Disable' : 'Enable',
onPress: () => setUseStrongBox(!useStrongBox),
},
],
);
}}
/>
</ParameterSection>
)}

<ParameterSection
icon={<WarningIcon color={yellow500} />}
title="Danger Zone"
Expand Down
6 changes: 6 additions & 0 deletions app/src/stores/settingStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ interface PersistedSettingsState {
setSkipDocumentSelectorIfSingle: (value: boolean) => void;
setSubscribedTopics: (topics: string[]) => void;
setTurnkeyBackupEnabled: (turnkeyBackupEnabled: boolean) => void;
setUseStrongBox: (useStrongBox: boolean) => void;
skipDocumentSelector: boolean;
skipDocumentSelectorIfSingle: boolean;
subscribedTopics: string[];
toggleCloudBackupEnabled: () => void;
turnkeyBackupEnabled: boolean;
useStrongBox: boolean;
}

interface NonPersistedSettingsState {
Expand Down Expand Up @@ -147,6 +149,10 @@ export const useSettingStore = create<SettingsState>()(
setSkipDocumentSelectorIfSingle: (value: boolean) =>
set({ skipDocumentSelectorIfSingle: value }),

// StrongBox setting for Android keystore (default: true)
useStrongBox: true,
setUseStrongBox: (useStrongBox: boolean) => set({ useStrongBox }),

// Non-persisted state (will not be saved to storage)
hideNetworkModal: false,
setHideNetworkModal: (hideNetworkModal: boolean) => {
Expand Down
21 changes: 21 additions & 0 deletions app/src/types/react-native-keychain.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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.

import type { SetOptions } from 'react-native-keychain';

/**
* Extended SetOptions with useStrongBox property added by our patch.
* Use this type when you need the useStrongBox option for Android keystore.
*/
export type ExtendedSetOptions = SetOptions & {
/**
* Whether to attempt StrongBox-backed key generation on Android.
* When true (default), the library will try to use StrongBox hardware
* security module if available, falling back to regular secure hardware.
* When false, StrongBox is skipped and regular secure hardware is used directly.
* @platform Android
* @default true
*/
useStrongBox?: boolean;
};
Loading
Loading