SELF-1497: add keychain patch (#1607)

* add keychain patch - wip

* centralise useStrongbox flag usage

* set allowBackup to false

* bump to version 2.9.12

* bump android build for 2.9.12

* improve keychain error detection

* Disable Strongbox by default

---------

Co-authored-by: Justin Hernandez <justin.hernandez@self.xyz>
This commit is contained in:
Seshanth.S
2026-01-20 23:35:41 +05:30
committed by GitHub
parent 13d81c53bf
commit d5b843db5b
10 changed files with 9135 additions and 11 deletions

View File

@@ -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"

View File

@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.9.11</string>
<string>2.9.12</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -546,7 +546,7 @@
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/MoproKit/Libs",
);
MARKETING_VERSION = 2.9.11;
MARKETING_VERSION = 2.9.12;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -686,7 +686,7 @@
"$(PROJECT_DIR)",
"$(PROJECT_DIR)/MoproKit/Libs",
);
MARKETING_VERSION = 2.9.11;
MARKETING_VERSION = 2.9.12;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",

View File

@@ -1,6 +1,6 @@
{
"name": "@selfxyz/mobile-app",
"version": "2.9.11",
"version": "2.9.12",
"private": true,
"type": "module",
"scripts": {

View File

@@ -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
*/
@@ -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;
}
/**
@@ -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;
@@ -78,7 +83,7 @@ export async function createKeychainOptions(
options: GetSecureOptions,
capabilities?: SecurityCapabilities,
): Promise<{
setOptions: SetOptions;
setOptions: ExtendedSetOptions;
getOptions: GetOptions;
}> {
const config = await getAdaptiveSecurityConfig(
@@ -86,10 +91,14 @@ export async function createKeychainOptions(
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,
};
const getOptions: GetOptions = {

View File

@@ -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';
@@ -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);
@@ -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"

View File

@@ -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 {
@@ -147,6 +149,10 @@ export const useSettingStore = create<SettingsState>()(
setSkipDocumentSelectorIfSingle: (value: boolean) =>
set({ skipDocumentSelectorIfSingle: value }),
// StrongBox setting for Android keystore (default: false)
useStrongBox: false,
setUseStrongBox: (useStrongBox: boolean) => set({ useStrongBox }),
// Non-persisted state (will not be saved to storage)
hideNetworkModal: false,
setHideNetworkModal: (hideNetworkModal: boolean) => {

View File

@@ -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;
};

View File

@@ -29,6 +29,8 @@ export function isKeychainCryptoError(error: unknown): boolean {
err?.name === 'com.oblador.keychain.exceptions.CryptoFailedException' ||
err?.message?.includes('CryptoFailedException') ||
err?.message?.includes('Decryption failed') ||
err?.message?.includes('Could not encrypt data') ||
err?.message?.includes('Keystore operation failed') ||
err?.message?.includes('Authentication tag verification failed')) &&
!isUserCancellation(error),
);
@@ -41,6 +43,13 @@ export function isUserCancellation(error: unknown): boolean {
err?.code === 'USER_CANCELED' ||
err?.message?.includes('User canceled') ||
err?.message?.includes('Authentication canceled') ||
err?.message?.includes('cancelled by user'),
err?.message?.includes('cancelled by user') ||
err?.message?.includes('Fingerprint operation cancelled') ||
err?.message?.includes('operation cancelled') ||
err?.message?.includes("Can't verify face") ||
err?.message?.includes('code: 5') || // ERROR_CANCELED
err?.message?.includes('code: 2') || // ERROR_UNABLE_TO_PROCESS (biometric verification failed)
err?.message?.includes('code: 10') || // ERROR_USER_CANCELED
err?.message?.includes('code: 13'), // ERROR_NEGATIVE_BUTTON - for ref (https://developer.android.com/reference/androidx/biometric/BiometricPrompt#ERROR_NEGATIVE_BUTTON())
);
}

File diff suppressed because one or more lines are too long