diff --git a/App.tsx b/App.tsx index d4aeaba87b..adc105eef2 100644 --- a/App.tsx +++ b/App.tsx @@ -15,7 +15,12 @@ import { import {DualMessageOverlay} from './components/DualMessageOverlay'; import {useApp} from './screens/AppController'; import {Alert} from 'react-native'; -import {configureTelemetry} from './shared/telemetry/TelemetryUtils'; +import { + TelemetryConstants, + configureTelemetry, + getErrorEventData, + sendErrorEvent, +} from './shared/telemetry/TelemetryUtils'; import {MessageOverlay} from './components/MessageOverlay'; import SecureKeystore from 'react-native-secure-keystore'; import {isHardwareKeystoreExists} from './shared/cryptoutil/cryptoUtil'; @@ -59,6 +64,18 @@ const AppLoadingWrapper: React.FC = () => { ); const controller = useApp(); const {t} = useTranslation('WelcomeScreen'); + useEffect(() => { + if (isKeyInvalidateError) { + configureTelemetry(); + sendErrorEvent( + getErrorEventData( + TelemetryConstants.FlowType.appLogin, + TelemetryConstants.ErrorId.appWasReset, + TelemetryConstants.ErrorMessage.appWasReset, + ), + ); + } + }, [isKeyInvalidateError]); return ( <> diff --git a/machines/QrLoginMachine.typegen.ts b/machines/QrLoginMachine.typegen.ts index ae58a354c3..95f31f8cb9 100644 --- a/machines/QrLoginMachine.typegen.ts +++ b/machines/QrLoginMachine.typegen.ts @@ -1,56 +1,85 @@ +// This file was automatically generated. Edits will be overwritten - // This file was automatically generated. Edits will be overwritten - - export interface Typegen0 { - '@@xstate/typegen': true; - internalEvents: { - "done.invoke.QrLogin.linkTransaction:invocation[0]": { type: "done.invoke.QrLogin.linkTransaction:invocation[0]"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." }; -"done.invoke.QrLogin.sendingAuthenticate:invocation[0]": { type: "done.invoke.QrLogin.sendingAuthenticate:invocation[0]"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." }; -"error.platform.QrLogin.linkTransaction:invocation[0]": { type: "error.platform.QrLogin.linkTransaction:invocation[0]"; data: unknown }; -"error.platform.QrLogin.sendingAuthenticate:invocation[0]": { type: "error.platform.QrLogin.sendingAuthenticate:invocation[0]"; data: unknown }; -"error.platform.QrLogin.sendingConsent:invocation[0]": { type: "error.platform.QrLogin.sendingConsent:invocation[0]"; data: unknown }; -"xstate.init": { type: "xstate.init" }; - }; - invokeSrcNameMap: { - "linkTransaction": "done.invoke.QrLogin.linkTransaction:invocation[0]"; -"sendAuthenticate": "done.invoke.QrLogin.sendingAuthenticate:invocation[0]"; -"sendConsent": "done.invoke.QrLogin.sendingConsent:invocation[0]"; - }; - missingImplementations: { - actions: never; - delays: never; - guards: never; - services: never; - }; - eventsCausingActions: { - "SetErrorMessage": "error.platform.QrLogin.linkTransaction:invocation[0]" | "error.platform.QrLogin.sendingAuthenticate:invocation[0]" | "error.platform.QrLogin.sendingConsent:invocation[0]"; -"expandLinkTransResp": "done.invoke.QrLogin.linkTransaction:invocation[0]"; -"forwardToParent": "DISMISS"; -"loadMyVcs": "done.invoke.QrLogin.linkTransaction:invocation[0]"; -"loadThumbprint": "FACE_VALID"; -"resetLinkTransactionId": "GET"; -"resetSelectedVoluntaryClaims": "GET"; -"setClaims": "done.invoke.QrLogin.linkTransaction:invocation[0]"; -"setConsentClaims": "TOGGLE_CONSENT_CLAIM"; -"setLinkedTransactionId": "done.invoke.QrLogin.sendingAuthenticate:invocation[0]"; -"setMyVcs": "STORE_RESPONSE"; -"setScanData": "GET"; -"setSelectedVc": "SELECT_VC"; -"setThumbprint": "STORE_RESPONSE"; -"setlinkTransactionResponse": "done.invoke.QrLogin.linkTransaction:invocation[0]"; - }; - eventsCausingDelays: { - - }; - eventsCausingGuards: { - "isConsentAlreadyCaptured": "done.invoke.QrLogin.sendingAuthenticate:invocation[0]"; - }; - eventsCausingServices: { - "linkTransaction": "GET"; -"sendAuthenticate": "STORE_RESPONSE"; -"sendConsent": "CONFIRM"; - }; - matchesStates: "ShowError" | "done" | "faceAuth" | "invalidIdentity" | "linkTransaction" | "loadMyVcs" | "loadingThumbprint" | "requestConsent" | "sendingAuthenticate" | "sendingConsent" | "showvcList" | "success" | "waitingForData"; - tags: never; - } - \ No newline at end of file +export interface Typegen0 { + '@@xstate/typegen': true; + internalEvents: { + 'done.invoke.QrLogin.linkTransaction:invocation[0]': { + type: 'done.invoke.QrLogin.linkTransaction:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'done.invoke.QrLogin.sendingAuthenticate:invocation[0]': { + type: 'done.invoke.QrLogin.sendingAuthenticate:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'error.platform.QrLogin.linkTransaction:invocation[0]': { + type: 'error.platform.QrLogin.linkTransaction:invocation[0]'; + data: unknown; + }; + 'error.platform.QrLogin.sendingAuthenticate:invocation[0]': { + type: 'error.platform.QrLogin.sendingAuthenticate:invocation[0]'; + data: unknown; + }; + 'error.platform.QrLogin.sendingConsent:invocation[0]': { + type: 'error.platform.QrLogin.sendingConsent:invocation[0]'; + data: unknown; + }; + 'xstate.init': {type: 'xstate.init'}; + }; + invokeSrcNameMap: { + linkTransaction: 'done.invoke.QrLogin.linkTransaction:invocation[0]'; + sendAuthenticate: 'done.invoke.QrLogin.sendingAuthenticate:invocation[0]'; + sendConsent: 'done.invoke.QrLogin.sendingConsent:invocation[0]'; + }; + missingImplementations: { + actions: never; + delays: never; + guards: never; + services: never; + }; + eventsCausingActions: { + SetErrorMessage: + | 'error.platform.QrLogin.linkTransaction:invocation[0]' + | 'error.platform.QrLogin.sendingAuthenticate:invocation[0]' + | 'error.platform.QrLogin.sendingConsent:invocation[0]'; + expandLinkTransResp: 'done.invoke.QrLogin.linkTransaction:invocation[0]'; + forwardToParent: 'DISMISS'; + loadMyVcs: 'done.invoke.QrLogin.linkTransaction:invocation[0]'; + loadThumbprint: 'FACE_VALID'; + resetLinkTransactionId: 'GET'; + resetSelectedVoluntaryClaims: 'GET'; + setClaims: 'done.invoke.QrLogin.linkTransaction:invocation[0]'; + setConsentClaims: 'TOGGLE_CONSENT_CLAIM'; + setLinkedTransactionId: 'done.invoke.QrLogin.sendingAuthenticate:invocation[0]'; + setMyVcs: 'STORE_RESPONSE'; + setScanData: 'GET'; + setSelectedVc: 'SELECT_VC'; + setThumbprint: 'STORE_RESPONSE'; + setlinkTransactionResponse: 'done.invoke.QrLogin.linkTransaction:invocation[0]'; + }; + eventsCausingDelays: {}; + eventsCausingGuards: { + isConsentAlreadyCaptured: 'done.invoke.QrLogin.sendingAuthenticate:invocation[0]'; + }; + eventsCausingServices: { + linkTransaction: 'GET'; + sendAuthenticate: 'STORE_RESPONSE'; + sendConsent: 'CONFIRM'; + }; + matchesStates: + | 'ShowError' + | 'done' + | 'faceAuth' + | 'invalidIdentity' + | 'linkTransaction' + | 'loadMyVcs' + | 'loadingThumbprint' + | 'requestConsent' + | 'sendingAuthenticate' + | 'sendingConsent' + | 'showvcList' + | 'success' + | 'waitingForData'; + tags: never; +} diff --git a/machines/revoke.typegen.ts b/machines/revoke.typegen.ts index 02b7d0222c..da3480cde1 100644 --- a/machines/revoke.typegen.ts +++ b/machines/revoke.typegen.ts @@ -1,45 +1,62 @@ +// This file was automatically generated. Edits will be overwritten - // This file was automatically generated. Edits will be overwritten - - export interface Typegen0 { - '@@xstate/typegen': true; - internalEvents: { - "done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]": { type: "done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." }; -"error.platform.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]": { type: "error.platform.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]"; data: unknown }; -"xstate.init": { type: "xstate.init" }; - }; - invokeSrcNameMap: { - "requestOtp": "done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]"; -"requestRevoke": "done.invoke.RevokeVids.requestingRevoke:invocation[0]"; - }; - missingImplementations: { - actions: never; - delays: never; - guards: never; - services: never; - }; - eventsCausingActions: { - "clearOtp": "DISMISS" | "ERROR" | "REVOKE_VCS" | "done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]" | "xstate.init"; -"logRevoked": "STORE_RESPONSE"; -"revokeVID": "SUCCESS"; -"setIdBackendError": "error.platform.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]"; -"setOtp": "INPUT_OTP"; -"setOtpError": "ERROR"; -"setTransactionId": "DISMISS" | "REVOKE_VCS" | "xstate.init"; -"setVIDs": "REVOKE_VCS"; - }; - eventsCausingDelays: { - - }; - eventsCausingGuards: { - - }; - eventsCausingServices: { - "requestOtp": never; -"requestRevoke": "INPUT_OTP"; - }; - matchesStates: "acceptingOtpInput" | "acceptingVIDs" | "acceptingVIDs.idle" | "acceptingVIDs.requestingOtp" | "idle" | "invalid" | "invalid.backend" | "invalid.otp" | "loggingRevoke" | "requestingRevoke" | "revokingVc" | { "acceptingVIDs"?: "idle" | "requestingOtp"; -"invalid"?: "backend" | "otp"; }; - tags: never; - } - \ No newline at end of file +export interface Typegen0 { + '@@xstate/typegen': true; + internalEvents: { + 'done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]': { + type: 'done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'error.platform.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]': { + type: 'error.platform.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]'; + data: unknown; + }; + 'xstate.init': {type: 'xstate.init'}; + }; + invokeSrcNameMap: { + requestOtp: 'done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]'; + requestRevoke: 'done.invoke.RevokeVids.requestingRevoke:invocation[0]'; + }; + missingImplementations: { + actions: never; + delays: never; + guards: never; + services: never; + }; + eventsCausingActions: { + clearOtp: + | 'DISMISS' + | 'ERROR' + | 'REVOKE_VCS' + | 'done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]' + | 'xstate.init'; + logRevoked: 'STORE_RESPONSE'; + revokeVID: 'SUCCESS'; + setIdBackendError: 'error.platform.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]'; + setOtp: 'INPUT_OTP'; + setOtpError: 'ERROR'; + setTransactionId: 'DISMISS' | 'REVOKE_VCS' | 'xstate.init'; + setVIDs: 'REVOKE_VCS'; + }; + eventsCausingDelays: {}; + eventsCausingGuards: {}; + eventsCausingServices: { + requestOtp: never; + requestRevoke: 'INPUT_OTP'; + }; + matchesStates: + | 'acceptingOtpInput' + | 'acceptingVIDs' + | 'acceptingVIDs.idle' + | 'acceptingVIDs.requestingOtp' + | 'idle' + | 'invalid' + | 'invalid.backend' + | 'invalid.otp' + | 'loggingRevoke' + | 'requestingRevoke' + | 'revokingVc' + | {acceptingVIDs?: 'idle' | 'requestingOtp'; invalid?: 'backend' | 'otp'}; + tags: never; +} diff --git a/machines/settings.ts b/machines/settings.ts index 5c1559e08d..2a9feee326 100644 --- a/machines/settings.ts +++ b/machines/settings.ts @@ -164,8 +164,13 @@ export const settingsMachine = model.createMachine( }), updateDefaults: model.assign({ - appId: () => { - const appId = generateAppId(); + appId: (_, event) => { + const appId = + event.response != null && + event.response.encryptedData == null && + event.response.appId != null + ? event.response.appId + : generateAppId(); __AppId.setValue(appId); return appId; }, @@ -246,7 +251,10 @@ export const settingsMachine = model.createMachine( }, guards: { - hasData: (_, event) => event.response != null, + hasData: (_, event) => + event.response != null && + event.response.encryptedData != null && + event.response.appId != null, hasPartialData: (_, event) => event.response != null && event.response.appId == null, }, diff --git a/machines/settings.typegen.ts b/machines/settings.typegen.ts index 31ee28ed65..6534f02f14 100644 --- a/machines/settings.typegen.ts +++ b/machines/settings.typegen.ts @@ -18,19 +18,12 @@ export interface Typegen0 { resetInjiProps: 'done.invoke.settings.resetInjiProps:invocation[0]'; }; missingImplementations: { - actions: 'injiTourGuide'; + actions: never; delays: never; guards: never; services: never; }; eventsCausingActions: { - injiTourGuide: - | 'ACCEPT_HARDWARE_SUPPORT_NOT_EXISTS' - | 'BACK' - | 'CANCEL' - | 'STORE_RESPONSE' - | 'done.invoke.settings.resetInjiProps:invocation[0]' - | 'error.platform.settings.resetInjiProps:invocation[0]'; requestStoredContext: 'xstate.init'; resetCredentialRegistry: 'CANCEL' | 'UPDATE_MIMOTO_HOST'; setContext: 'STORE_RESPONSE'; @@ -64,7 +57,6 @@ export interface Typegen0 { matchesStates: | 'idle' | 'init' - | 'injiTourGuide' | 'resetInjiProps' | 'showInjiTourGuide' | 'storingDefaults'; diff --git a/machines/store.ts b/machines/store.ts index 4a99821a80..6cdd49da21 100644 --- a/machines/store.ts +++ b/machines/store.ts @@ -12,7 +12,7 @@ import { import {createModel} from 'xstate/lib/model'; import {generateSecureRandom} from 'react-native-securerandom'; import {log} from 'xstate/lib/actions'; -import {MY_VCS_STORE_KEY} from '../shared/constants'; +import {MY_VCS_STORE_KEY, SETTINGS_STORE_KEY} from '../shared/constants'; import SecureKeystore from 'react-native-secure-keystore'; import { AUTH_TIMEOUT, @@ -485,8 +485,18 @@ export async function setItem( encryptionKey: string, ) { try { - const data = JSON.stringify(value); - const encryptedData = await encryptJson(encryptionKey, data); + let encryptedData; + if (key === SETTINGS_STORE_KEY) { + const appId = value.appId; + delete value.appId; + const settings = { + encryptedData: await encryptJson(encryptionKey, JSON.stringify(value)), + appId, + }; + encryptedData = JSON.stringify(settings); + } else { + encryptedData = await encryptJson(encryptionKey, JSON.stringify(value)); + } await Storage.setItem(key, encryptedData, encryptionKey); } catch (e) { console.error('error setItem:', e); @@ -502,7 +512,19 @@ export async function getItem( try { const data = await Storage.getItem(key, encryptionKey); if (data != null) { - const decryptedData = await decryptJson(encryptionKey, data); + let decryptedData; + if (key === SETTINGS_STORE_KEY) { + let parsedData = JSON.parse(data); + if (parsedData.encryptedData) { + decryptedData = await decryptJson( + encryptionKey, + parsedData.encryptedData, + ); + parsedData.encryptedData = JSON.parse(decryptedData); + } + return parsedData; + } + decryptedData = await decryptJson(encryptionKey, data); return JSON.parse(decryptedData); } if (data === null && VCMetadata.isVCKey(key)) { diff --git a/screens/Home/MyVcsTab.tsx b/screens/Home/MyVcsTab.tsx index b40be95c15..ae4ec54734 100644 --- a/screens/Home/MyVcsTab.tsx +++ b/screens/Home/MyVcsTab.tsx @@ -61,7 +61,21 @@ export const MyVcsTab: React.FC = props => { ), ); } - }, [controller.areAllVcsLoaded, controller.inProgressVcDownloads]); + + if (controller.isTampered) { + sendErrorEvent( + getErrorEventData( + TelemetryConstants.FlowType.appLogin, + TelemetryConstants.ErrorId.vcsAreTampered, + TelemetryConstants.ErrorMessage.vcsAreTampered, + ), + ); + } + }, [ + controller.areAllVcsLoaded, + controller.inProgressVcDownloads, + controller.isTampered, + ]); let failedVCsList = []; controller.downloadFailedVcs.forEach(vc => { diff --git a/shared/storage.ts b/shared/storage.ts index 1dec2fc15d..1d3696759f 100644 --- a/shared/storage.ts +++ b/shared/storage.ts @@ -15,8 +15,14 @@ import { } from './cryptoutil/cryptoUtil'; import {VCMetadata} from './VCMetadata'; import {ENOENT, getItem} from '../machines/store'; -import {isAndroid, MY_VCS_STORE_KEY, RECEIVED_VCS_STORE_KEY} from './constants'; +import { + isAndroid, + MY_VCS_STORE_KEY, + RECEIVED_VCS_STORE_KEY, + SETTINGS_STORE_KEY, +} from './constants'; import FileStorage, {getFilePath, vcDirectoryPath} from './fileStorage'; +import {__AppId} from './GlobalVariables'; export const MMKV = new MMKVLoader().initialize(); @@ -184,7 +190,11 @@ class Storage { try { (await FileStorage.exists(`${vcDirectoryPath}`)) && (await FileStorage.removeItem(`${vcDirectoryPath}`)); + const settings = await MMKV.getItem(SETTINGS_STORE_KEY); + const appId = JSON.parse(settings).appId; + __AppId.setValue(appId); MMKV.clearStore(); + await MMKV.setItem(SETTINGS_STORE_KEY, JSON.stringify({appId: appId})); } catch (e) { console.log('Error Occurred while Clearing Storage.', e); } diff --git a/shared/telemetry/TelemetryUtils.js b/shared/telemetry/TelemetryUtils.js index be51421832..3a64ba69b5 100644 --- a/shared/telemetry/TelemetryUtils.js +++ b/shared/telemetry/TelemetryUtils.js @@ -210,6 +210,10 @@ export const TelemetryConstants = { hardwareKeyStore: 'Some security features will be unavailable as hardware key store is not available', activationCancelled: 'Activation Cancelled', + appWasReset: + 'Due to the fingerprint / facial recognition update, app security was impacted, and downloaded cards were removed. Please download again', + vcsAreTampered: + 'Tampered cards detected and removed for security reasons. Please download again', }), ErrorId: Object.freeze({ @@ -218,6 +222,8 @@ export const TelemetryConstants = { userCancel: 'USER_CANCEL', resend: 'RESEND', activationFailed: 'ACTIVATION_FAILED', + appWasReset: 'APP_WAS_RESET', + vcsAreTampered: 'VCS_ARE_TAMPERED', }), Screens: Object.freeze({