Skip to content

Commit

Permalink
Backups v2 IDFA check (#5653)
Browse files Browse the repository at this point in the history
* idfa check

* idfa changes

* update idfa check to check through all wallets not just a single wallet

* latest onfailure changes

* move check to only authenticated users

* final code review

* fix lint
  • Loading branch information
walmat committed May 31, 2024
1 parent 0b2610e commit cfde334
Show file tree
Hide file tree
Showing 18 changed files with 387 additions and 5 deletions.
6 changes: 6 additions & 0 deletions ios/RCTDeviceUUID.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#import "React/RCTBridgeModule.h"
#import "React/RCTLog.h"

@interface RCTDeviceUUID : NSObject <RCTBridgeModule>

@end
16 changes: 16 additions & 0 deletions ios/RCTDeviceUUID.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#import "RCTDeviceUUID.h"

@implementation RCTDeviceUUID

@synthesize bridge = _bridge;

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(getUUID:(RCTResponseSenderBlock)callback)
{
NSUUID *deviceId = [UIDevice currentDevice].identifierForVendor;

callback(@[[NSNull null], [NSArray arrayWithObjects: [deviceId UUIDString], nil]]);
}

@end
6 changes: 6 additions & 0 deletions ios/Rainbow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
C1EB01312731B68400830E70 /* TokenDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EB012E2731B68400830E70 /* TokenDetails.swift */; };
C72F456C99A646399192517D /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 98AED33BAB4247CEBEF8464D /* libz.tbd */; };
C85BADF4CAE1A2FA1D99C521 /* libPods-Rainbow.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13F1E920F312479DF774F1FB /* libPods-Rainbow.a */; };
C97EAD8D2BD6C6DF00322D53 /* RCTDeviceUUID.m in Sources */ = {isa = PBXBuildFile; fileRef = C97EAD8B2BD6C6DF00322D53 /* RCTDeviceUUID.m */; };
ED2971652150620600B7C4FE /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED2971642150620600B7C4FE /* JavaScriptCore.framework */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -320,6 +321,8 @@
C1C61A81272CBDA100E5C0B3 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
C1C61A902731A05700E5C0B3 /* RainbowTokenList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RainbowTokenList.swift; sourceTree = "<group>"; };
C1EB012E2731B68400830E70 /* TokenDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenDetails.swift; sourceTree = "<group>"; };
C97EAD8B2BD6C6DF00322D53 /* RCTDeviceUUID.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDeviceUUID.m; sourceTree = "<group>"; };
C97EAD8C2BD6C6DF00322D53 /* RCTDeviceUUID.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDeviceUUID.h; sourceTree = "<group>"; };
CBE43422951BC4BC1A49163C /* Pods-SelectTokenIntent.localrelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SelectTokenIntent.localrelease.xcconfig"; path = "Target Support Files/Pods-SelectTokenIntent/Pods-SelectTokenIntent.localrelease.xcconfig"; sourceTree = "<group>"; };
D755E71324B04FEE9C691D14 /* libRNFirebase.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFirebase.a; sourceTree = "<group>"; };
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -564,6 +567,8 @@
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
C97EAD8C2BD6C6DF00322D53 /* RCTDeviceUUID.h */,
C97EAD8B2BD6C6DF00322D53 /* RCTDeviceUUID.m */,
);
name = Libraries;
sourceTree = "<group>";
Expand Down Expand Up @@ -1303,6 +1308,7 @@
C18FCD32273C62230079CE28 /* PriceWidgetView.swift in Sources */,
15E531D5242B28EF00797B89 /* UIImageViewWithPersistentAnimations.swift in Sources */,
C18FCD3B273C64CF0079CE28 /* UIImage.swift in Sources */,
C97EAD8D2BD6C6DF00322D53 /* RCTDeviceUUID.m in Sources */,
A4277D9F23CBD1910042BAF4 /* Extensions.swift in Sources */,
A4D04BAC23D12FD5008C1DEC /* ButtonManager.m in Sources */,
C18FCD3C273C64D10079CE28 /* TokenDetails.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { handleReviewPromptAction } from '@/utils/reviewAlert';
import { RemotePromoSheetProvider } from '@/components/remote-promo-sheet/RemotePromoSheetProvider';
import { RemoteCardProvider } from '@/components/cards/remote-cards';
import { initializeRemoteConfig } from '@/model/remoteConfig';
import { checkIdentifierOnLaunch } from './model/backup';

if (__DEV__) {
reactNativeDisableYellowBox && LogBox.ignoreAllLogs();
Expand Down Expand Up @@ -109,6 +110,7 @@ class OldApp extends Component {
if (!__DEV__ && isTestFlight) {
logger.info(`Test flight usage - ${isTestFlight}`);
}

this.identifyFlow();
const eventSub = AppState?.addEventListener('change', this?.handleAppStateChange);
this.setState({ eventSubscription: eventSub });
Expand Down Expand Up @@ -150,6 +152,8 @@ class OldApp extends Component {
handleReviewPromptAction(ReviewPromptAction.TimesLaunchedSinceInstall);
}, 10_000);
});

checkIdentifierOnLaunch();
}

const initialRoute = address ? Routes.SWIPE_LAYOUT : Routes.WELCOME_SCREEN;
Expand Down
Binary file added src/assets/RestoreYourWallet.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 src/assets/RestoreYourWallet@2x.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 src/assets/RestoreYourWallet@3x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/components/backup/useCreateBackup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Navigation, useNavigation } from '@/navigation';
import { InteractionManager } from 'react-native';
import { DelayedAlert } from '../alerts';
import { useDispatch } from 'react-redux';
import { AllRainbowWallets } from '@/model/wallet';

type UseCreateBackupProps = {
walletId?: string;
Expand Down Expand Up @@ -82,7 +83,7 @@ export const useCreateBackup = ({ walletId, navigateToRoute }: UseCreateBackupPr
return;
}
backupAllWalletsToCloud({
wallets,
wallets: wallets as AllRainbowWallets,
password,
latestBackup,
onError,
Expand Down
1 change: 1 addition & 0 deletions src/helpers/walletBackupStepTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export default {
restore_from_backup: 'restore_from_backup',
backup_now_to_cloud: 'cloud',
backup_now_manually: 'manual',
check_identifier: 'check_identifier',
};
17 changes: 17 additions & 0 deletions src/languages/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,23 @@
"description": "You can also long press your\naddress above to copy it."
}
},
"check_identifier": {
"title": "Authentication Needed",
"description": "It looks like you may have switched devices or reinstalled Rainbow. Please authenticate so that we can validate your keychain data is still in a healthy state.",
"action": "Authenticate",
"dismiss": "Dismiss",
"error_alert": {
"title": "Error Validating Keychain",
"message": "We encountered an error while validating your keychain data. Please try again. If the issue persists, please contact support.",
"contact_support": "Contact Support",
"cancel": "Cancel"
},
"failure_alert": {
"title": "Invalid Keychain Data",
"message": "We were unable to verify the integrity of your keychain data. Please restore your wallet and try again. If the issue persists, please contact support.",
"action": "Continue"
}
},
"cloud": {
"backup_success": "Your wallet has been backed up successfully!"
},
Expand Down
143 changes: 139 additions & 4 deletions src/model/backup.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { NativeModules } from 'react-native';
import { captureException } from '@sentry/react-native';
import { endsWith } from 'lodash';
import { CLOUD_BACKUP_ERRORS, encryptAndSaveDataToCloud, getDataFromCloud } from '@/handlers/cloudBackup';
import WalletBackupTypes from '../helpers/walletBackupTypes';
import WalletTypes from '../helpers/walletTypes';
import { allWalletsKey, pinKey, privateKeyKey, seedPhraseKey, selectedWalletKey } from '@/utils/keychainConstants';
import { Alert } from '@/components/alerts';
import { allWalletsKey, pinKey, privateKeyKey, seedPhraseKey, selectedWalletKey, identifierForVendorKey } from '@/utils/keychainConstants';
import * as keychain from '@/model/keychain';
import * as kc from '@/keychain';
import { AllRainbowWallets, allWalletsVersion, createWallet, RainbowWallet } from './wallet';
import { analytics } from '@/analytics';
import oldLogger from '@/utils/logger';
import { logger, RainbowError } from '@/logger';
import { IS_ANDROID } from '@/env';
import { IS_ANDROID, IS_DEV } from '@/env';
import AesEncryptor from '../handlers/aesEncryption';
import { authenticateWithPIN, authenticateWithPINAndCreateIfNeeded, decryptPIN } from '@/handlers/authentication';
import * as i18n from '@/languages';
import { getUserError } from '@/hooks/useWalletCloudBackup';
import { cloudPlatform } from '@/utils/platform';
import { setAllWalletsWithIdsAsBackedUp } from '@/redux/wallets';
import { Navigation } from '@/navigation';
import Routes from '@/navigation/routesNames';
import { clearAllStorages } from './mmkv';
import walletBackupStepTypes from '@/helpers/walletBackupStepTypes';

const { DeviceUUID } = NativeModules;
const encryptor = new AesEncryptor();
const PIN_REGEX = /^\d{4}$/;

Expand Down Expand Up @@ -622,7 +630,7 @@ async function decryptSecretFromBackupPin({ secret, backupPIN }: { secret?: stri
// Attempts to save the password to decrypt the backup from the iCloud keychain
export async function saveBackupPassword(password: BackupPassword): Promise<void> {
try {
if (ios) {
if (!IS_ANDROID) {
await kc.setSharedWebCredentials('Backup Password', password);
analytics.track('Saved backup password on iCloud');
}
Expand Down Expand Up @@ -653,7 +661,7 @@ export async function saveLocalBackupPassword(password: string) {

// Attempts to fetch the password to decrypt the backup from the iCloud keychain
export async function fetchBackupPassword(): Promise<null | BackupPassword> {
if (android) {
if (IS_ANDROID) {
return null;
}

Expand All @@ -669,3 +677,130 @@ export async function fetchBackupPassword(): Promise<null | BackupPassword> {
return null;
}
}

export async function getDeviceUUID(): Promise<string | null> {
if (IS_ANDROID) {
return null;
}

return new Promise(resolve => {
DeviceUUID.getUUID((error: unknown, uuid: string[]) => {
if (error) {
logger.error(new RainbowError('Received error when trying to get uuid from Native side'), {
error,
});
resolve(null);
} else {
resolve(uuid[0]);
}
});
});
}

const FailureAlert = () =>
Alert({
buttons: [
{
style: 'cancel',
text: i18n.t(i18n.l.check_identifier.failure_alert.action),
},
],
message: i18n.t(i18n.l.check_identifier.failure_alert.message),
title: i18n.t(i18n.l.check_identifier.failure_alert.title),
});

/**
* Checks if the identifier is the same as the one stored in localstorage
* The identifier can get out of sync in two instances:
* 1. when the user reinstalls the app
* 2. when the user migrates phones (we really only care about this instance)
*
* The goal here is to not allow them into the app if they have broken keychain data from a phone migration
*
* @returns a promise function to be ran after successful biometric authentication
*/
export async function checkIdentifierOnLaunch() {
// Unable to really persist things on Android, so let's just exit early...
if (IS_ANDROID) return;

try {
const uuid = await getDeviceUUID();
if (!uuid) {
throw new Error('Unable to retrieve identifier for vendor');
}

const currentIdentifier = await kc.get(identifierForVendorKey);
if (currentIdentifier.error) {
switch (currentIdentifier.error) {
case kc.ErrorType.Unavailable: {
logger.debug('Value for current identifier not found, setting it to new UUID...', {
uuid,
error: currentIdentifier.error,
});
await kc.set(identifierForVendorKey, uuid);
return;
}

default:
logger.error(new RainbowError('Error while checking identifier on launch'), {
error: currentIdentifier.error,
});
break;
}

throw new Error('Unable to retrieve current identifier');
}

// NOTE: This can only happen on a fresh install
if (!currentIdentifier.value) {
await kc.set(identifierForVendorKey, uuid);
return;
}

if (!IS_DEV) {
// if our identifiers match up, we can assume no reinstall/migration
if (currentIdentifier.value === uuid) {
return;
}
}

return new Promise(resolve => {
Navigation.handleAction(Routes.CHECK_IDENTIFIER_SCREEN, {
step: walletBackupStepTypes.check_identifier,
// NOTE: Just a reinstall, let's update the identifer and send them back to the app
onSuccess: async () => {
await kc.set(identifierForVendorKey, uuid);
Navigation.goBack();
resolve(true);
},
// NOTE: Detected a phone migration, let's remove keychain keys and send them back to the welcome screen
onFailure: async () => {
FailureAlert();
// wipe keychain
await kc.clear();

// re-add the IDFA uuid
await kc.set(identifierForVendorKey, uuid);

// clear async storage
await AsyncStorage.clear();

// clear mmkv
clearAllStorages();

// send user back to welcome screen
Navigation.handleAction(Routes.WELCOME_SCREEN, {});
resolve(false);
},
});
});
} catch (error) {
logger.error(new RainbowError('Error while checking identifier on launch'), {
extra: {
error,
},
});
}

return false;
}
3 changes: 3 additions & 0 deletions src/navigation/Routes.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
positionSheetConfig,
appIconUnlockSheetConfig,
swapConfig,
checkIdentifierSheetConfig,
recieveModalSheetConfig,
} from './config';
import { addCashSheet, emojiPreset, emojiPresetWallet, overlayExpandedPreset, sheetPreset } from './effects';
Expand Down Expand Up @@ -101,6 +102,7 @@ import AppIconUnlockSheet from '@/screens/AppIconUnlockSheet';
import { SwapScreen } from '@/__swaps__/screens/Swap/Swap';
import { useRemoteConfig } from '@/model/remoteConfig';
import { SwapProvider } from '@/__swaps__/screens/Swap/providers/swap-provider';
import CheckIdentifierScreen from '@/screens/CheckIdentifierScreen';
import { ControlPanel } from '@/components/DappBrowser/control-panel/ControlPanel';

type StackNavigatorParams = {
Expand Down Expand Up @@ -229,6 +231,7 @@ function NativeStackNavigator() {
transitionDuration: 0.25,
}}
/>
<NativeStack.Screen component={CheckIdentifierScreen} name={Routes.CHECK_IDENTIFIER_SCREEN} {...checkIdentifierSheetConfig} />
<NativeStack.Screen component={BackupSheet} name={Routes.BACKUP_SHEET} {...backupSheetConfig} />
<NativeStack.Screen
component={ModalScreen}
Expand Down
23 changes: 23 additions & 0 deletions src/navigation/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const backupSheetSizes = {
: deviceUtils.dimensions.height + safeAreaInsetValues.bottom + sharedCoolModalTopOffset + SheetHandleFixedToTopHeight,
medium: 550,
short: 424,
check_identifier: 414,
shorter: 364,
};

Expand All @@ -103,13 +104,35 @@ export const getHeightForStep = (step: string) => {
return backupSheetSizes.long;
case WalletBackupStepTypes.no_provider:
return backupSheetSizes.medium;
case WalletBackupStepTypes.check_identifier:
return backupSheetSizes.check_identifier;
case WalletBackupStepTypes.backup_now_manually:
return backupSheetSizes.shorter;
default:
return backupSheetSizes.short;
}
};

export const checkIdentifierSheetConfig: PartialNavigatorConfigOptions = {
options: ({ navigation, route }) => {
const { params: { longFormHeight, step, ...params } = {} } = route as {
params: any;
};

const heightForStep = getHeightForStep(step);
if (longFormHeight !== heightForStep) {
navigation.setParams({
longFormHeight: heightForStep,
});
}

return buildCoolModalConfig({
...params,
longFormHeight: heightForStep,
});
},
};

export const backupSheetConfig: PartialNavigatorConfigOptions = {
options: ({ navigation, route }) => {
const { params: { longFormHeight, step, ...params } = {} } = route as {
Expand Down
1 change: 1 addition & 0 deletions src/navigation/routesNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const Routes = {
BACKUP_SHEET: 'BackupSheet',
CHANGE_WALLET_SHEET: 'ChangeWalletSheet',
CHANGE_WALLET_SHEET_NAVIGATOR: 'ChangeWalletSheetNavigator',
CHECK_IDENTIFIER_SCREEN: 'CheckIdentifierScreen',
CONFIRM_REQUEST: 'ConfirmRequest',
CONNECTED_DAPPS: 'ConnectedDapps',
CONSOLE_SHEET: 'ConsoleSheet',
Expand Down
4 changes: 4 additions & 0 deletions src/navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,8 @@ export type RootStackParamList = {
ensName: string;
mode: REGISTRATION_MODES;
};
[Routes.CHECK_IDENTIFIER_SCREEN]: {
onSuccess: () => Promise<void>;
onFailure: () => Promise<void>;
};
};
Loading

0 comments on commit cfde334

Please sign in to comment.