Skip to content

Commit

Permalink
fix: android secure storage
Browse files Browse the repository at this point in the history
  • Loading branch information
edgarkhanzadian committed Jan 16, 2025
1 parent 81ab5f9 commit b3beee3
Show file tree
Hide file tree
Showing 15 changed files with 450 additions and 263 deletions.
2 changes: 1 addition & 1 deletion apps/mobile/.eas/build/maestro-test-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ build:

- run_maestro_tests
# TODO: Enable secure tests when we can support Biometrics on Android and an Android emulator with biometrics in EAS (LEA-1671)
# - run_secure_maestro_tests
- run_secure_maestro_tests
108 changes: 54 additions & 54 deletions apps/mobile/ios/Podfile.lock

Large diffs are not rendered by default.

66 changes: 49 additions & 17 deletions apps/mobile/src/app/(home)/create-new-wallet.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { ReactNode, useEffect, useState } from 'react';
import { Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { MnemonicDisplay } from '@/components/create-new-wallet/mnemonic-display';
Expand All @@ -9,6 +10,7 @@ import { useSettings } from '@/store/settings/settings';
import { tempMnemonicStore } from '@/store/storage-persistors';
import { t } from '@lingui/macro';
import { useTheme } from '@shopify/restyle';
import { ImageBackground } from 'expo-image';

import { generateMnemonic } from '@leather.io/crypto';
import {
Expand All @@ -22,11 +24,53 @@ import {
legacyTouchablePressEffect,
} from '@leather.io/ui/native';

function SecretBanner({ children, isHidden }: { children: ReactNode; isHidden: boolean }) {
const { themeDerivedFromThemePreference } = useSettings();
const { whenTheme } = useSettings();
return Platform.select({
ios: (
<BlurView
experimentalBlurMethod="dimezisBlurView"
themeVariant={themeDerivedFromThemePreference}
intensity={isHidden ? 30 : 0}
style={{
position: 'absolute',
top: -20,
bottom: -20,
left: -20,
right: -20,
zIndex: 10,
}}
>
{children}
</BlurView>
),
android: (
<ImageBackground
contentFit="fill"
imageStyle={{}}
source={whenTheme({
light: require('../../assets/secret-blurred-light.png'),
dark: require('../../assets/secret-blurred-dark.png'),
})}
style={{
position: 'absolute',
top: -20,
bottom: -20,
left: -20,
right: -20,
zIndex: 10,
}}
>
{children}
</ImageBackground>
),
});
}

export default function CreateNewWallet() {
const { bottom } = useSafeAreaInsets();
const theme = useTheme<Theme>();

const { themeDerivedFromThemePreference } = useSettings();
const [isHidden, setIsHidden] = useState(true);
const [mnemonic, setMnemonic] = useState<string | null>(null);
const { navigateAndCreateWallet } = useCreateWallet();
Expand Down Expand Up @@ -59,19 +103,7 @@ export default function CreateNewWallet() {

<Box my="5">
{isHidden && (
<BlurView
experimentalBlurMethod="dimezisBlurView"
themeVariant={themeDerivedFromThemePreference}
intensity={isHidden ? 30 : 0}
style={{
position: 'absolute',
top: -20,
bottom: -20,
left: -20,
right: -20,
zIndex: 10,
}}
>
<SecretBanner isHidden={isHidden}>
<Pressable
onPress={() => setIsHidden(false)}
height="100%"
Expand All @@ -98,7 +130,7 @@ export default function CreateNewWallet() {
</Text>
</Box>
</Pressable>
</BlurView>
</SecretBanner>
)}
<MnemonicDisplay mnemonic={mnemonic} />
</Box>
Expand Down
6 changes: 4 additions & 2 deletions apps/mobile/src/app/(home)/secure-your-wallet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useRef } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { useAuthentication } from '@/common/use-authentication';
import { AnimatedHeaderScreenLayout } from '@/components/headers/animated-header/animated-header-screen.layout';
import { SkipSecureWalletSheet } from '@/components/secure-your-wallet/skip-secure-wallet-sheet';
import { useCreateWallet } from '@/hooks/use-create-wallet';
Expand All @@ -14,6 +15,7 @@ export default function SecureYourWalletScreen() {
const theme = useTheme<Theme>();
const sheetRef = useRef<SheetRef>(null);
const { createWallet } = useCreateWallet();
const { callIfEnrolled } = useAuthentication();

return (
<>
Expand Down Expand Up @@ -55,8 +57,8 @@ export default function SecureYourWalletScreen() {
})}
/>
<Button
onPress={async () => {
await createWallet({ biometrics: true });
onPress={() => {
void callIfEnrolled(() => createWallet({ biometrics: true }));
}}
buttonState="default"
title={t({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRef, useState } from 'react';

import { useAuthentication } from '@/common/use-authentication';
import { AddWalletSheet } from '@/components/add-wallet/';
import { Divider } from '@/components/divider';
import { AnimatedHeaderScreenLayout } from '@/components/headers/animated-header/animated-header-screen.layout';
Expand All @@ -24,7 +25,6 @@ import { userRenamesWallet } from '@/store/wallets/wallets.write';
import { t } from '@lingui/macro';
import { useTheme } from '@shopify/restyle';
import dayjs from 'dayjs';
import * as LocalAuthentication from 'expo-local-authentication';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { z } from 'zod';

Expand Down Expand Up @@ -104,6 +104,7 @@ function ConfigureWallet({ wallet }: ConfigureWalletProps) {
const removeWalletSheetRef = useRef<SheetRef>(null);
const dispatch = useAppDispatch();
const { securityLevelPreference } = useSettings();
const { authenticate } = useAuthentication();

const { displayToast } = useToastContext();

Expand Down Expand Up @@ -134,8 +135,8 @@ function ConfigureWallet({ wallet }: ConfigureWalletProps) {

async function secureRemoveWallet() {
if (securityLevelPreference === 'secure') {
const result = await LocalAuthentication.authenticateAsync();
if (result.success) {
const result = await authenticate();
if (result && result.success) {
removeWallet();
} else {
displayToast({
Expand Down
24 changes: 12 additions & 12 deletions apps/mobile/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,21 @@ export default function RootLayout() {
<QueryClientProvider client={queryClient}>
<LeatherQueryProvider>
<ThemeProvider>
<SplashScreenGuard>
<HapticsProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<SheetNavigatorWrapper>
<SheetProvider>
<ToastWrapper>
<GestureHandlerRootView style={{ flex: 1 }}>
<ToastWrapper>
<SplashScreenGuard>
<HapticsProvider>
<SheetNavigatorWrapper>
<SheetProvider>
<AppRouter />
<SendSheet />
<ReceiveSheet />
</ToastWrapper>
</SheetProvider>
</SheetNavigatorWrapper>
</GestureHandlerRootView>
</HapticsProvider>
</SplashScreenGuard>
</SheetProvider>
</SheetNavigatorWrapper>
</HapticsProvider>
</SplashScreenGuard>
</ToastWrapper>
</GestureHandlerRootView>
</ThemeProvider>
</LeatherQueryProvider>
</QueryClientProvider>
Expand Down
Binary file added apps/mobile/src/assets/secret-blurred-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile/src/assets/secret-blurred-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 9 additions & 2 deletions apps/mobile/src/assets/wallet-creation-animation-dark.json
Original file line number Diff line number Diff line change
Expand Up @@ -534,9 +534,16 @@
"o": { "a": 0, "k": 100, "ix": 2 },
"r": 1,
"bm": 0,
"g": { "p": 1, "k": { "a": 0, "k": [0, 0, 0, 0, 0, 1], "ix": 2 } },
"g": {
"p": 2,
"k": {
"a": 0,
"k": [0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0],
"ix": 2
}
},
"s": { "a": 0, "k": [0, 0], "ix": 2 },
"e": { "a": 0, "k": [0, 0], "ix": 2 },
"e": { "a": 0, "k": [392, 490], "ix": 2 },
"t": 2
},
{
Expand Down
43 changes: 43 additions & 0 deletions apps/mobile/src/common/use-authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useToastContext } from '@/components/toast/toast-context';
import { t } from '@lingui/macro';
import * as LocalAuthentication from 'expo-local-authentication';

async function assertEnrolled() {
const enrollment = await LocalAuthentication.getEnrolledLevelAsync();
if (enrollment === LocalAuthentication.SecurityLevel.NONE) {
throw new Error('USER_NOT_ENROLLED');
}
}

export function useAuthentication() {
const { displayToast } = useToastContext();

function displayError() {
displayToast({
title: t({
id: 'authentication.user-not-enrolled',
message: 'Device is not enrolled with PIN or biometrics',
}),
type: 'error',
});
}

async function callIfEnrolled<T extends (...args: any) => any>(
cb: T
): Promise<ReturnType<T> | undefined> {
try {
await assertEnrolled();
return cb();
} catch {
displayError();
return;
}
}

return {
callIfEnrolled,
async authenticate() {
return await callIfEnrolled(LocalAuthentication.authenticateAsync);
},
};
}
16 changes: 12 additions & 4 deletions apps/mobile/src/components/splash-screen-guard/use-auth-state.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useCallback, useState } from 'react';

import { useAuthentication } from '@/common/use-authentication';
import { useAppState } from '@/hooks/use-app-state';
import { useSettings } from '@/store/settings/settings';
import { analytics } from '@/utils/analytics';
import * as LocalAuthentication from 'expo-local-authentication';

const unlockTimeout = 60 * 1000;
type AuthState = 'cold-start' | 'started' | 'failed' | 'passed-on-first' | 'passed-afterwards';
Expand All @@ -17,6 +17,7 @@ export function useAuthState({
}) {
const { securityLevelPreference, userLeavesApp, lastActive } = useSettings();
const [authState, setAuthState] = useState<AuthState>('cold-start');
const { authenticate } = useAuthentication();

const checkUnlockTime = useCallback(() => {
return !!lastActive && lastActive > +new Date() - unlockTimeout;
Expand All @@ -38,8 +39,8 @@ export function useAuthState({
return;
}

const result = await LocalAuthentication.authenticateAsync();
if (result.success) {
const result = await authenticate();
if (result && result.success) {
playSplash();
if (firstTry) {
setAuthState('passed-on-first');
Expand All @@ -52,7 +53,14 @@ export function useAuthState({
setAuthState('failed');
}
},
[securityLevelPreference, checkUnlockTime, authState, playSplash, setAnimationFinished]
[
securityLevelPreference,
checkUnlockTime,
authState,
playSplash,
setAnimationFinished,
authenticate,
]
);

const onAppForeground = useCallback(() => {
Expand Down
26 changes: 16 additions & 10 deletions apps/mobile/src/features/settings/app-authentication-sheet.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { RefObject } from 'react';
import { Linking } from 'react-native';

import { useAuthentication } from '@/common/use-authentication';
import { SettingsList } from '@/components/settings/settings-list';
import { SettingsListItem } from '@/components/settings/settings-list-item';
import { useToastContext } from '@/components/toast/toast-context';
import { LEATHER_GUIDES_MOBILE_APP_AUTHENTICATION } from '@/shared/constants';
import { useSettings } from '@/store/settings/settings';
import { t } from '@lingui/macro';
import * as LocalAuthentication from 'expo-local-authentication';

import { KeyholeIcon, SheetRef } from '@leather.io/ui/native';

Expand All @@ -19,10 +19,16 @@ interface AppAuthenticationSheetProps {
export function AppAuthenticationSheet({ sheetRef }: AppAuthenticationSheetProps) {
const settings = useSettings();
const { displayToast } = useToastContext();
const { authenticate } = useAuthentication();

function onUpdateAppAuth() {
LocalAuthentication.authenticateAsync()
authenticate()
.then(result => {
if (!result) {
// Do nothing if result is undefined, as that means it's already handled by useAuthentication hook
return;
}

if (result.success) {
settings.changeSecurityLevelPreference(
settings.securityLevelPreference === 'secure' ? 'insecure' : 'secure'
Expand All @@ -34,15 +40,15 @@ export function AppAuthenticationSheet({ sheetRef }: AppAuthenticationSheetProps
}),
type: 'success',
});
return;
} else {
displayToast({
title: t({
id: 'app_auth.toast_title_error',
message: 'Failed to authenticate',
}),
type: 'error',
});
}
displayToast({
title: t({
id: 'app_auth.toast_title_error',
message: 'Failed to authenticate',
}),
type: 'error',
});
})
.catch(() =>
displayToast({
Expand Down
Loading

0 comments on commit b3beee3

Please sign in to comment.