From 942d9d0eb822adf8d4b4220b4d67d0b1916e1f8b Mon Sep 17 00:00:00 2001 From: Caroline Lucas Calheirani <97122568+CarolineLCa@users.noreply.github.com> Date: Wed, 21 Feb 2024 10:11:56 -0300 Subject: [PATCH] feat: improving loginAttempt storage (#1067) Signed-off-by: Caroline Lucas Calheirani --- packages/legacy/core/App/constants.ts | 2 +- .../core/App/contexts/reducers/store.ts | 3 ++- packages/legacy/core/App/index.ts | 2 ++ packages/legacy/core/App/screens/Splash.tsx | 6 ++--- packages/legacy/core/App/services/keychain.ts | 23 +++++++++++++++++- .../core/__tests__/screens/Splash.test.tsx | 24 ++++++++++++------- 6 files changed, 45 insertions(+), 15 deletions(-) diff --git a/packages/legacy/core/App/constants.ts b/packages/legacy/core/App/constants.ts index 3b06618b99..1f16e16886 100644 --- a/packages/legacy/core/App/constants.ts +++ b/packages/legacy/core/App/constants.ts @@ -12,7 +12,6 @@ export const testIdPrefix = 'com.ariesbifold:id/' export enum LocalStorageKeys { Onboarding = 'OnboardingState', - LoginAttempts = 'LoginAttempts', // FIXME: Once hooks are updated this should no longer be necessary RevokedCredentials = 'RevokedCredentials', RevokedCredentialsMessageDismissed = 'RevokedCredentialsMessageDismissed', @@ -24,6 +23,7 @@ export enum LocalStorageKeys { export enum KeychainServices { Salt = 'secret.wallet.salt', Key = 'secret.wallet.key', + LoginAttempt = 'wallet.loginAttempt', } export enum EventTypes { diff --git a/packages/legacy/core/App/contexts/reducers/store.ts b/packages/legacy/core/App/contexts/reducers/store.ts index 4e3e374f6b..cbfbef484c 100644 --- a/packages/legacy/core/App/contexts/reducers/store.ts +++ b/packages/legacy/core/App/contexts/reducers/store.ts @@ -1,6 +1,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import { LocalStorageKeys } from '../../constants' +import { storeLoginAttempt } from '../../services/keychain' import { Preferences as PreferencesState, Tours as ToursState, @@ -415,7 +416,7 @@ export const reducer = (state: S, action: ReducerAction { }) const loadAuthAttempts = async (): Promise => { - const attemptsData = await AsyncStorage.getItem(LocalStorageKeys.LoginAttempts) - if (attemptsData) { - const attempts = JSON.parse(attemptsData) as LoginAttemptState + const attempts = await loadLoginAttempt() + if (attempts) { dispatch({ type: DispatchAction.ATTEMPT_UPDATED, payload: [attempts], diff --git a/packages/legacy/core/App/services/keychain.ts b/packages/legacy/core/App/services/keychain.ts index 6d7e1cc21e..0bf0d8877c 100644 --- a/packages/legacy/core/App/services/keychain.ts +++ b/packages/legacy/core/App/services/keychain.ts @@ -4,11 +4,12 @@ import uuid from 'react-native-uuid' import { walletId, KeychainServices } from '../constants' import { WalletSecret } from '../types/security' +import { LoginAttempt } from '../types/state' import { hashPIN } from '../utils/crypto' const keyFauxUserName = 'WalletFauxPINUserName' const saltFauxUserName = 'WalletFauxSaltUserName' - +const loginAttemptFauxUserName = 'WalletFauxLoginAttemptUserName' export interface WalletSalt { id: string salt: string @@ -72,6 +73,13 @@ export const storeWalletSalt = async (secret: WalletSalt): Promise => { return typeof result === 'boolean' ? false : true } +export const storeLoginAttempt = async (loginAttempt: LoginAttempt): Promise => { + const opts = optionsForKeychainAccess(KeychainServices.LoginAttempt, false) + const loginAttemptAsString = JSON.stringify(loginAttempt) + const result = await Keychain.setGenericPassword(loginAttemptFauxUserName, loginAttemptAsString, opts) + return typeof result !== 'boolean' +} + export const storeWalletSecret = async (secret: WalletSecret, useBiometrics = false): Promise => { let keyResult = false if (secret.key) { @@ -95,6 +103,19 @@ export const loadWalletSalt = async (): Promise => { return JSON.parse(result.password) as WalletSalt } +export const loadLoginAttempt = async (): Promise => { + const opts: Keychain.Options = { + service: KeychainServices.LoginAttempt, + } + + const result = await Keychain.getGenericPassword(opts) + if (!result) { + return + } + + return JSON.parse(result.password) as LoginAttempt +} + export const loadWalletKey = async (title?: string, description?: string): Promise => { let opts: Keychain.Options = { service: KeychainServices.Key, diff --git a/packages/legacy/core/__tests__/screens/Splash.test.tsx b/packages/legacy/core/__tests__/screens/Splash.test.tsx index 1a76695ab2..065545e3db 100644 --- a/packages/legacy/core/__tests__/screens/Splash.test.tsx +++ b/packages/legacy/core/__tests__/screens/Splash.test.tsx @@ -9,6 +9,7 @@ import Splash from '../../App/screens/Splash' import AsyncStorage from '../../__mocks__/@react-native-async-storage/async-storage' import authContext from '../contexts/auth' import configurationContext from '../contexts/configuration' +import { loadLoginAttempt } from '../../App/services/keychain' jest.mock('@hyperledger/aries-askar-react-native', () => ({})) jest.mock('@hyperledger/anoncreds-react-native', () => ({})) @@ -24,12 +25,15 @@ jest.mock('@react-navigation/core', () => { }), } }) +jest.mock('../../App/services/keychain', () => ({ + loadLoginAttempt: jest.fn(), +})) describe('Splash Screen', () => { - beforeAll(()=>{ + beforeAll(() => { jest.useFakeTimers() }) - afterAll(()=>{ + afterAll(() => { jest.useRealTimers() }) test('Renders default correctly', async () => { @@ -40,18 +44,18 @@ describe('Splash Screen', () => { ) - await act(()=>{ jest.runAllTimers() }) + await act(() => { + jest.runAllTimers() + }) expect(tree).toMatchSnapshot() }) test('Starts onboarding correctly', async () => { + // @ts-ignore-next-line + loadLoginAttempt.mockReturnValue({ servedPenalty: true, loginAttempts: 0 }) + AsyncStorage.getItem = jest.fn().mockImplementation((key: string) => { switch (key) { - case LocalStorageKeys.LoginAttempts: - return JSON.stringify({ - servedPenalty: true, - loginAttempts: 0, - }) case LocalStorageKeys.Preferences: return JSON.stringify({ useBiometry: false, @@ -101,7 +105,9 @@ describe('Splash Screen', () => { ) }) - await act(()=>{ jest.runAllTimers() }) + await act(() => { + jest.runAllTimers() + }) expect(mockedDispatch).toHaveBeenCalled() }) })