[INJI-628 & INJI-630]: Local backup creation (#1160)

* [INJI-630]: add backup machine and screens for data backup

Signed-off-by: Pooja Babusingh <68894211+PoojaBabusingh@users.noreply.github.com>

* [INJI-630]: add argon2 configs for hashing salt, password and phoneNumber

Signed-off-by: Pooja Babusingh <68894211+PoojaBabusingh@users.noreply.github.com>

* [INJI-630]: add key encryption and store hashed encryption key

Signed-off-by: Pooja Babusingh <68894211+PoojaBabusingh@users.noreply.github.com>

* [INJI-630]: add toggle backup check to make modal invisible in ios

Signed-off-by: Pooja Babusingh <68894211+PoojaBabusingh@users.noreply.github.com>

* (INJI-630): update package-lock.json

Signed-off-by: Sreenadh S <32409698+sree96@users.noreply.github.com>

* [INJI-628]: restrict logic to double stringify JSON object before saving to DB

Signed-off-by: Alka <prasadalka1998@gmail.com>

* [INJI-628]: create local compressed backup file

Signed-off-by: Alka <prasadalka1998@gmail.com>

* [INJI-628]: move some functions to appropriate files

Signed-off-by: Alka <prasadalka1998@gmail.com>

* [INJI-628]: remove RN aes crypto library and use existing library to generate encryption key

Signed-off-by: Alka <prasadalka1998@gmail.com>

* [INJI-628]: update logic to check storage availability for backup creation

Signed-off-by: Alka <prasadalka1998@gmail.com>

* [INJI-628]: declare the variable and use then use it

Signed-off-by: Alka <prasadalka1998@gmail.com>

---------

Signed-off-by: Pooja Babusingh <68894211+PoojaBabusingh@users.noreply.github.com>
Signed-off-by: PoojaBabusing <115976560+PoojaBabusing@users.noreply.github.com>
Signed-off-by: Sreenadh S <32409698+sree96@users.noreply.github.com>
Signed-off-by: Alka <prasadalka1998@gmail.com>
Signed-off-by: Alka Prasad <Alka1703@users.noreply.github.com>
Co-authored-by: Pooja Babusingh <68894211+PoojaBabusingh@users.noreply.github.com>
Co-authored-by: Sreenadh S <32409698+sree96@users.noreply.github.com>
Co-authored-by: Alka <prasadalka1998@gmail.com>
Co-authored-by: Alka Prasad <Alka1703@users.noreply.github.com>
This commit is contained in:
PoojaBabusing
2024-01-19 18:24:55 +05:30
committed by GitHub
parent f340420627
commit d778b94403
22 changed files with 1132 additions and 40 deletions

2
.env
View File

@@ -13,6 +13,8 @@ APPLICATION_THEME=orange
#environment can be changed if it is toggled
CREDENTIAL_REGISTRY_EDIT=true
#DataBackup can enable if it is toggled
DATA_BACKUP=true
DEBUG_MODE=false
#supported languages( en, fil, ar, hi, kn, ta)

View File

@@ -2,7 +2,7 @@ fileignoreconfig:
- filename: package.json
checksum: 4770aabfda162fbc0b9a8c53d7dee483ce29b82c6cd3e17e81e3e628d93dbadc
- filename: package-lock.json
checksum: 82c06097d5f030255c1aed47bae1f2df241759612fdd0715b4f66b0e1535cbb9
checksum: a53fc6cfa6f3308ae8080d6375d5a57d6fe83e16b8fc9260931fe5242be8816e
- filename: lib/jsonld-signatures/suites/ed255192018/ed25519.ts
checksum: 493b6e31144116cb612c24d98b97d8adcad5609c0a52c865a6847ced0a0ddc3a
- filename: components/PasscodeVerify.tsx
@@ -36,17 +36,17 @@ fileignoreconfig:
- filename: shared/telemetry/TelemetryUtils.js
checksum: ffe9aac2dcc590b98b0d588885c088eff189504ade653a77f74b67312bfd27ad
- filename: shared/fileStorage.ts
checksum: f86dc7aa4a69e7109310e7ab5529a8599f38f15eb79f3f4da545aceaaf90d731
checksum: 5c4c9bc78446e6b25bf6e6ac276918757d6275a27ba93d253338ca629ebc0240
- filename: shared/storage.ts
checksum: c31270346f2ef717a31168a93d0311ce6f925434eb613ec7cf86553222630cdb
checksum: 837dda0fd0a3e160e62f10df44003d29e29dfce897550a85a8e74fe77a2a06db
- filename: screens/Issuers/IssuersScreen.tsx
checksum: 9c53e3770dbefe26e0de67ee4b7d5cc9c52d9823cbb136a1a5104dcb0a101071
- filename: ios/Podfile.lock
checksum: 2487c4e11fb1bd95032cc4511435d9420fc0dfc62f3c015d177213fa089df7f2
checksum: 1da328d6edcd284799be962d59281b120a5bb2a6873ca6a01da321857e59a7d3
- filename: screens/Home/IntroSlidersScreen.tsx
checksum: 72ef913857448ef05763e52e32356faa2d1f3de8130a1c638d1897f44823031f
- filename: shared/commonUtil.ts
checksum: b9ff87d627c74ba1cf2f1d0bfab6c11192573c45a4c581c3beadb3c612bfe1ab
checksum: 4a53bb615f2ea0fbf687bd7027c4c246e819dd88bc273941ed611e763d9d2356
- filename: screens/Home/MyVcs/GetIdInputModal.tsx
checksum: 5c736ed79a372d0ffa7c02eb33d0dc06edbbb08d120978ff287f5f06cd6c7746
- filename: shared/openId4VCI/Utils.ts
@@ -82,9 +82,11 @@ fileignoreconfig:
- filename: assets/fingerprint_icon.svg
checksum: b2d3a50ca1336f60123d96a8cc8ea663c3316ed2d8c31833bce7e393ca51695b
- filename: machines/store.ts
checksum: ca2328c39d2757ffebf85c0b6663da1073eb58349f2630c9418b2f16c350cbd1
checksum: 9970da0ea685018a90f7306fb88945d135dd019439099dd56b6109d915a8a24f
- filename: assets/Flip_Camera_Icon.svg
checksum: 736b5a7ddb86bd4376229ce198dbf8a663e7ac89fc3311bd4f19afd4a2b36ffd
- filename: ios/fastlane/Fastfile
checksum: 086080bc7a04accf5094c457b5acf84d9fec5d7dfa72eaaaf02e433ecf4f996b
- filename: assets/Finger_Print_Icon.svg
checksum: 776d4fe4fc4b54d185ccf97daf0511b9fe2c0e0f7c1a809047020e5e8a100db6
- filename: android/app/build.gradle
@@ -100,7 +102,33 @@ fileignoreconfig:
- filename: assets/Issuer_search_clearing_button.svg
checksum: f4e8a054fc4168e08bc9e9fe3e644cebabacdfc31ef0cbe36dd281766f47df5e
- filename: screens/Home/MyVcs/IdInputModal.tsx
checksum: 7ee46d8ef4761c0e9b59f3e602e6e30be5f47221817c819e91ab10ca2203089f
checksum: 7ee46d8ef4761c0e9b59f3e602e6e30be5f47221817c819e91ab10ca2203089f
- filename: .env
checksum: d3023fb22734e6c7bd626f24007ba7b93e791cb0bcb8a086cb8e99f04bc31c7c
- filename: machines/backup.ts
checksum: d4b95b075ce39ed80b119014188f8903dbb46d46dd19a3757eea2ae8a06ba7ad
- filename: machines/backup.typegen.ts
checksum: 4ae5dbf36353cb7568a278628256e68caded071f1e91d6e1b25ac9aefb1f81cf
- filename: screens/Settings/BackupController.tsx
checksum: 054665c377b4a8e246ae1bfb419ab86d0cb1c120ae92cdc27ceed9cf1f39679b
- filename: screens/Settings/BackupViaPassword.tsx
checksum: 5fb2f98fb8a4efeb6b691b8a29e62eba64a99e9b2129bc0af1d11bc56bdfb374
- filename: screens/Settings/DataBackup.tsx
checksum: 6f3eaf26a58712b3ddd36dfa49d0d608eb873455ca91374545148c23e36fd43b
- filename: shared/constants.ts
checksum: 062fb9bc4ba7dc7c91558caee5a4fd41ba748b0dcd108c62b50a2fdd06eaa289
- filename: shared/commonUtil.ts
checksum: 4f353f525ce0d2c9c7caf734aa9ce4a7a5ce8d526528a491974052256b8cbe76
- filename: screens/Settings/BackupController.tsx
checksum: d2a355356bcaf8f7ef3b53ba93710cec15fefd0fdf31efd779eebd2bfab61c19
- filename: shared/constants.ts
checksum: 3961940d2df158b6c287cb689b8841d278eae273a4e5553ae2ae612340f8b8c8
- filename: shared/api.ts
checksum: 96a87e2128c30d16526cb1cb91b7da58f266d8c32d830adff11ca4d04e8c459f
- filename: machines/backupWithEncryption.ts
checksum: 038c12d30b2312fcbd9230a1c6ddb494d2e561fe0d09741335fa80ab67e2c550
- filename: machines/backupWithEncryption.typegen.ts
checksum: f5f9a71082e3f30f89e98a7913535327b31b942709ee8b5efb40e18c73ddee2a
- filename: screens/Home/MyVcs/OtpVerificationModal.tsx
checksum: 1db1f39701019383e1e40e6ed5278177e6c9bb3d28def0935cf6d4bd9e41e63a
- filename: screens/Home/MyVcs/GetIdInputModal.tsx

View File

@@ -12,7 +12,7 @@ PODS:
- BiometricSdk (0.5.9):
- TensorFlowLiteObjC (= 2.12.0)
- boost (1.76.0)
- BVLinearGradient (2.8.2):
- BVLinearGradient (2.8.3):
- React-Core
- CatCrypto (0.3.2)
- CrcSwift (0.0.3)
@@ -483,8 +483,16 @@ PODS:
- React
- RNSVG (13.4.0):
- React-Core
- RNZipArchive (6.1.0):
- React-Core
- RNZipArchive/Core (= 6.1.0)
- SSZipArchive (~> 2.2)
- RNZipArchive/Core (6.1.0):
- React-Core
- SSZipArchive (~> 2.2)
- secure-keystore (0.1.5):
- React-Core
- SSZipArchive (2.4.3)
- TensorFlowLiteC (2.12.0):
- TensorFlowLiteC/Core (= 2.12.0)
- TensorFlowLiteC/Core (2.12.0)
@@ -586,6 +594,7 @@ DEPENDENCIES:
- RNScreens (from `../node_modules/react-native-screens`)
- RNSecureRandom (from `../node_modules/react-native-securerandom`)
- RNSVG (from `../node_modules/react-native-svg`)
- RNZipArchive (from `../node_modules/react-native-zip-archive`)
- "secure-keystore (from `../node_modules/@mosip/secure-keystore`)"
- "tuvali (from `../node_modules/@mosip/tuvali`)"
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -603,6 +612,7 @@ SPEC REPOS:
- MMKV
- MMKVCore
- ReachabilitySwift
- SSZipArchive
- TensorFlowLiteC
- TensorFlowLiteObjC
- ZXingObjC
@@ -768,6 +778,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-securerandom"
RNSVG:
:path: "../node_modules/react-native-svg"
RNZipArchive:
:path: "../node_modules/react-native-zip-archive"
secure-keystore:
:path: "../node_modules/@mosip/secure-keystore"
tuvali:
@@ -781,7 +793,7 @@ SPEC CHECKSUMS:
biometric-sdk-react-native: d2a3a1279013cc4a7514a1b43fe557eb76e4e4c1
BiometricSdk: 303e7329404ea4d922dc14108449d10d21574f77
boost: 64032b9e9b938fda23325e68a3771f0fabf414dc
BVLinearGradient: 916632041121a658c704df89d99f04acb038de0f
BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3
CatCrypto: a477899b6be4954e75be4897e732da098cc0a5a8
CrcSwift: f85dea6b41dddb5f98bb3743fd777ce58b77bc2e
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
@@ -867,7 +879,9 @@ SPEC CHECKSUMS:
RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f
RNSecureRandom: 07efbdf2cd99efe13497433668e54acd7df49fef
RNSVG: 07dbd870b0dcdecc99b3a202fa37c8ca163caec2
RNZipArchive: ef9451b849c45a29509bf44e65b788829ab07801
secure-keystore: 21c03ba81520aefa99621383770ce00b3e306c72
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
TensorFlowLiteC: 20785a69299185a379ba9852b6625f00afd7984a
TensorFlowLiteObjC: 9a46a29a76661c513172cfffd3bf712b11ef25c3
tuvali: 9c3aad61844f6fcbd48ec7967cd6805418c3f8da
@@ -876,4 +890,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 01f58b130fa221dabb14b2d82d981ef24dcaba53
COCOAPODS: 1.14.3
COCOAPODS: 1.12.1

View File

@@ -25,6 +25,7 @@ import {
SETTINGS_STORE_KEY,
} from '../shared/constants';
import {logState} from '../shared/commonUtil';
import {backupMachine, createBackupMachine} from './backup';
const model = createModel(
{
@@ -260,6 +261,11 @@ export const appMachine = model.createMachine(
settingsMachine.id,
);
serviceRefs.backup = spawn(
createBackupMachine(serviceRefs),
backupMachine.id,
);
serviceRefs.activityLog = spawn(
createActivityLogMachine(serviceRefs),
activityLogMachine.id,
@@ -293,6 +299,7 @@ export const appMachine = model.createMachine(
context.serviceRefs.settings.subscribe(logState);
context.serviceRefs.activityLog.subscribe(logState);
context.serviceRefs.scan.subscribe(logState);
context.serviceRefs.backup.subscribe(logState);
if (isAndroid()) {
context.serviceRefs.request.subscribe(logState);

219
machines/backup.ts Normal file
View File

@@ -0,0 +1,219 @@
import {EventFrom, StateFrom, send} from 'xstate';
import {createModel} from 'xstate/lib/model';
import {AppServices} from '../shared/GlobalContext';
import {StoreEvents} from './store';
import Storage, {
isMinimumLimitForBackupReached,
writeToBackupFile,
} from '../shared/storage';
import {compressAndRemoveFile} from '../shared/fileStorage';
import {
getEndEventData,
getImpressionEventData,
getStartEventData,
sendEndEvent,
sendImpressionEvent,
sendStartEvent,
} from '../shared/telemetry/TelemetryUtils';
import {TelemetryConstants} from '../shared/telemetry/TelemetryConstants';
const model = createModel(
{
serviceRefs: {} as AppServices,
dataFromStorage: {},
fileName: '',
},
{
events: {
DATA_BACKUP: () => ({}),
OK: () => ({}),
FETCH_DATA: () => ({}),
DISMISS: () => ({}),
STORE_RESPONSE: (response: unknown) => ({response}),
FILE_NAME: (filename: string) => ({filename}),
},
},
);
export const BackupEvents = model.events;
export const backupMachine = model.createMachine(
{
predictableActionArguments: true,
preserveActionOrder: true,
tsTypes: {} as import('./backup.typegen').Typegen0,
schema: {
context: model.initialContext,
events: {} as EventFrom<typeof model>,
},
id: 'backup',
initial: 'init',
states: {
init: {
on: {
DATA_BACKUP: [
{
target: 'backingUp',
},
],
},
},
backingUp: {
initial: 'idle',
states: {
idle: {},
checkStorageAvailability: {
entry: ['sendDataBackupStartEvent'],
invoke: {
src: 'checkStorageAvailability',
onDone: [
{
cond: 'isMinimumStorageRequiredForBackupReached',
target: 'failure',
},
{
target: 'fetchDataFromDB',
},
],
},
},
fetchDataFromDB: {
entry: ['fetchAllDataFromDB'],
on: {
STORE_RESPONSE: {
actions: 'setDataFromStorage',
target: 'writeDataToFile',
},
},
},
writeDataToFile: {
invoke: {
src: 'writeDataToFile',
},
on: {
FILE_NAME: {
actions: 'setFileName',
target: 'zipBackupFile',
},
},
},
zipBackupFile: {
invoke: {
src: 'zipBackupFile',
onDone: {
target: 'success',
},
onError: {
target: 'failure',
},
},
},
success: {
entry: 'sendDataBackupSuccessEvent',
},
failure: {
entry: 'sendDataBackupFailureEvent',
},
},
on: {
FETCH_DATA: {
target: '.checkStorageAvailability',
},
OK: {
target: '.idle',
},
DISMISS: {
target: 'init',
},
},
},
},
},
{
actions: {
setDataFromStorage: model.assign({
dataFromStorage: (_context, event) => {
return event.response;
},
}),
setFileName: model.assign({
fileName: (_context, event) => {
return event.filename;
},
}),
fetchAllDataFromDB: send(StoreEvents.EXPORT(), {
to: context => context.serviceRefs.store,
}),
sendDataBackupStartEvent: () => {
sendStartEvent(
getStartEventData(TelemetryConstants.FlowType.dataBackup),
);
sendImpressionEvent(
getImpressionEventData(
TelemetryConstants.FlowType.dataBackup,
TelemetryConstants.Screens.dataBackupScreen,
),
);
},
sendDataBackupSuccessEvent: () => {
sendEndEvent(
getEndEventData(
TelemetryConstants.FlowType.dataBackup,
TelemetryConstants.EndEventStatus.success,
),
);
},
sendDataBackupFailureEvent: () => {
sendEndEvent(
getEndEventData(
TelemetryConstants.FlowType.dataBackup,
TelemetryConstants.EndEventStatus.failure,
),
);
},
},
services: {
checkStorageAvailability: () => async () => {
return Promise.resolve(isMinimumLimitForBackupReached());
},
writeDataToFile: context => async callack => {
const fileName = await writeToBackupFile(context.dataFromStorage);
callack(model.events.FILE_NAME(fileName));
},
zipBackupFile: context => async () => {
const result = await compressAndRemoveFile(context.fileName);
return result;
},
},
guards: {
isMinimumStorageRequiredForBackupReached: (_context, event) =>
Boolean(event.data),
},
},
);
export function createBackupMachine(serviceRefs: AppServices) {
return backupMachine.withContext({
...backupMachine.context,
serviceRefs,
});
}
export function selectIsBackingUp(state: State) {
return state.matches('backingUp');
}
export function selectIsBackingUpSuccess(state: State) {
return state.matches('backingUp.success');
}
export function selectIsBackingUpSFailure(state: State) {
return state.matches('backingUp.failure');
}
type State = StateFrom<typeof backupMachine>;

View File

@@ -0,0 +1,73 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true;
internalEvents: {
'done.invoke.backup.backingUp.checkStorageAvailability:invocation[0]': {
type: 'done.invoke.backup.backingUp.checkStorageAvailability:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'done.invoke.backup.backingUp.zipBackupFile:invocation[0]': {
type: 'done.invoke.backup.backingUp.zipBackupFile:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'error.platform.backup.backingUp.zipBackupFile:invocation[0]': {
type: 'error.platform.backup.backingUp.zipBackupFile:invocation[0]';
data: unknown;
};
'xstate.init': {type: 'xstate.init'};
};
invokeSrcNameMap: {
checkStorageAvailability: 'done.invoke.backup.backingUp.checkStorageAvailability:invocation[0]';
writeDataToFile: 'done.invoke.backup.backingUp.writeDataToFile:invocation[0]';
zipBackupFile: 'done.invoke.backup.backingUp.zipBackupFile:invocation[0]';
};
missingImplementations: {
actions: never;
delays: never;
guards: never;
services: never;
};
eventsCausingActions: {
fetchAllDataFromDB: 'done.invoke.backup.backingUp.checkStorageAvailability:invocation[0]';
sendDataBackupFailureEvent:
| 'done.invoke.backup.backingUp.checkStorageAvailability:invocation[0]'
| 'error.platform.backup.backingUp.zipBackupFile:invocation[0]';
sendDataBackupStartEvent: 'FETCH_DATA';
sendDataBackupSuccessEvent: 'done.invoke.backup.backingUp.zipBackupFile:invocation[0]';
setDataFromStorage: 'STORE_RESPONSE';
setFileName: 'FILE_NAME';
};
eventsCausingDelays: {};
eventsCausingGuards: {
isMinimumStorageRequiredForBackupReached: 'done.invoke.backup.backingUp.checkStorageAvailability:invocation[0]';
};
eventsCausingServices: {
checkStorageAvailability: 'FETCH_DATA';
writeDataToFile: 'STORE_RESPONSE';
zipBackupFile: 'FILE_NAME';
};
matchesStates:
| 'backingUp'
| 'backingUp.checkStorageAvailability'
| 'backingUp.failure'
| 'backingUp.fetchDataFromDB'
| 'backingUp.idle'
| 'backingUp.success'
| 'backingUp.writeDataToFile'
| 'backingUp.zipBackupFile'
| 'init'
| {
backingUp?:
| 'checkStorageAvailability'
| 'failure'
| 'fetchDataFromDB'
| 'idle'
| 'success'
| 'writeDataToFile'
| 'zipBackupFile';
};
tags: never;
}

View File

@@ -0,0 +1,305 @@
import {DoneInvokeEvent, EventFrom, StateFrom, send} from 'xstate';
import {createModel} from 'xstate/lib/model';
import {AppServices} from '../shared/GlobalContext';
import {
BACKUP_ENC_KEY,
BACKUP_ENC_KEY_TYPE,
BACKUP_ENC_TYPE_VAL_PASSWORD,
BACKUP_ENC_TYPE_VAL_PHONE,
argon2iConfigForPasswordAndPhoneNumber,
argon2iSalt,
} from '../shared/constants';
import {hashData} from '../shared/commonUtil';
import {StoreEvents} from './store';
import Storage from '../shared/storage';
import {compressData} from '../shared/cryptoutil/cryptoUtil';
const model = createModel(
{
serviceRefs: {} as AppServices,
otp: '',
baseEncKey: '',
dataFromStorage: {},
hashedEncKey: '',
fileName: '',
},
{
events: {
DATA_BACKUP: () => ({}),
YES: () => ({}),
PASSWORD: () => ({}),
SET_BASE_ENC_KEY: (baseEncKey: string) => ({baseEncKey}),
FILE_NAME: (filename: string) => ({filename}),
PHONE_NUMBER: () => ({}),
SEND_OTP: () => ({}),
INPUT_OTP: (otp: string) => ({otp}),
BACK: () => ({}),
CANCEL: () => ({}),
WAIT: () => ({}),
CANCEL_DOWNLOAD: () => ({}),
STORE_RESPONSE: (response: unknown) => ({response}),
},
},
);
export const BackupWithEncryptionEvents = model.events;
export const backupWithEncryptionMachine = model.createMachine(
{
predictableActionArguments: true,
preserveActionOrder: true,
tsTypes: {} as import('./backupWithEncryption.typegen').Typegen0,
schema: {
context: model.initialContext,
events: {} as EventFrom<typeof model>,
},
id: 'WithEncryption',
initial: 'init',
states: {
init: {
on: {
DATA_BACKUP: [
{
target: 'backUp',
},
],
},
},
backUp: {
on: {
YES: {
target: 'selectPref',
},
},
},
selectPref: {
on: {
PASSWORD: {
target: 'passwordBackup',
},
PHONE_NUMBER: {
target: 'phoneNumberBackup',
},
},
},
passwordBackup: {
on: {
SET_BASE_ENC_KEY: {
actions: ['setBaseEncKey', 'storePasswordKeyType'],
},
STORE_RESPONSE: {
target: 'hashKey',
},
},
},
phoneNumberBackup: {
on: {
SET_BASE_ENC_KEY: {
actions: 'setBaseEncKey',
},
SEND_OTP: {
target: 'requestOtp',
},
},
},
requestOtp: {
on: {
WAIT: {},
CANCEL: {},
CANCEL_DOWNLOAD: {},
INPUT_OTP: {
actions: ['setOtp', 'storePhoneNumberKeyType'], // TODO: we should also do the otp Verification here
target: 'hashKey',
},
},
invoke: {
src: 'requestOtp',
onDone: [
{
target: '',
},
],
onError: [
{
actions: '',
target: '',
},
],
},
},
hashKey: {
invoke: {
src: 'hashEncKey',
onDone: {
actions: ['setHashedKey', 'storeHashedEncKey'],
},
},
on: {
STORE_RESPONSE: {
target: 'backingUp',
},
},
},
backingUp: {
initial: 'checkStorageAvailability',
states: {
idle: {},
checkStorageAvailability: {
entry: ['sendDataBackupStartEvent'],
invoke: {
src: 'checkStorageAvailability',
onDone: [
{
cond: 'isMinimumStorageRequiredForBackupReached',
target: 'failure',
},
{
target: 'fetchDataFromDB',
},
],
},
},
fetchDataFromDB: {
entry: ['fetchAllDataFromDB'],
on: {
STORE_RESPONSE: {
actions: 'setDataFromStorage',
target: 'writeDataToFile',
},
},
},
writeDataToFile: {
invoke: {
src: 'writeDataToFile',
},
on: {
FILE_NAME: {
actions: 'setFileName',
target: 'zipBackupFile',
},
},
},
zipBackupFile: {
invoke: {
src: 'zipBackupFile',
onDone: {
target: 'success',
},
onError: {
target: 'failure',
},
},
},
success: {
entry: 'sendDataBackupSuccessEvent',
},
failure: {
entry: 'sendDataBackupFailureEvent',
},
},
},
},
},
{
actions: {
setOtp: model.assign({
otp: (_context, event) => {
return event.otp;
},
}),
setDataFromStorage: model.assign({
dataFromStorage: (_context, event) => {
return event.response;
},
}),
setBaseEncKey: model.assign({
baseEncKey: (_context, event) => {
return event.baseEncKey;
},
}),
setHashedKey: model.assign({
hashedEncKey: (_context, event) =>
(event as DoneInvokeEvent<string>).data,
}),
storeHashedEncKey: send(
context => StoreEvents.SET(BACKUP_ENC_KEY, context.hashedEncKey),
{
to: context => context.serviceRefs.store,
},
),
storePasswordKeyType: send(
() =>
StoreEvents.SET(BACKUP_ENC_KEY_TYPE, BACKUP_ENC_TYPE_VAL_PASSWORD),
{
to: context => context.serviceRefs.store,
},
),
storePhoneNumberKeyType: send(
() => StoreEvents.SET(BACKUP_ENC_KEY_TYPE, BACKUP_ENC_TYPE_VAL_PHONE),
{
to: context => context.serviceRefs.store,
},
),
fetchAllDataFromDB: send(StoreEvents.EXPORT(), {
to: context => context.serviceRefs.store,
}),
},
services: {
hashEncKey: async context => {
return await hashData(
context.baseEncKey,
argon2iSalt,
argon2iConfigForPasswordAndPhoneNumber,
).then(value => value);
},
writeDataToFile: context => async callack => {
await Storage.writeToBackupFile(context.dataFromStorage);
},
zipBackupFile: context => async callback => {
const result = await compressData(context.fileName);
return result;
},
},
guards: {},
},
);
export function createBackupMachine(serviceRefs: AppServices) {
return backupWithEncryptionMachine.withContext({
...backupWithEncryptionMachine.context,
serviceRefs,
});
}
export function selectIsEnableBackup(state: State) {
return state.matches('backUp');
}
export function selectIsBackupPref(state: State) {
return state.matches('selectPref');
}
export function selectIsBackupViaPassword(state: State) {
return state.matches('passwordBackup');
}
export function selectIsBackupViaPhoneNumber(state: State) {
return state.matches('phoneNumberBackup');
}
export function selectIsRequestOtp(state: State) {
return state.matches('requestOtp');
}
export function selectIsBackingUp(state: State) {
return state.matches('backingUp');
}
export function selectIsCancellingDownload(state: State) {
// TODO: check cancelDownload based on state
return false;
}
type State = StateFrom<typeof backupWithEncryptionMachine>;

View File

@@ -0,0 +1,104 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true;
internalEvents: {
'done.invoke.WithEncryption.backingUp.checkStorageAvailability:invocation[0]': {
type: 'done.invoke.WithEncryption.backingUp.checkStorageAvailability:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'done.invoke.WithEncryption.backingUp.zipBackupFile:invocation[0]': {
type: 'done.invoke.WithEncryption.backingUp.zipBackupFile:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'done.invoke.WithEncryption.hashKey:invocation[0]': {
type: 'done.invoke.WithEncryption.hashKey:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'error.platform.WithEncryption.backingUp.zipBackupFile:invocation[0]': {
type: 'error.platform.WithEncryption.backingUp.zipBackupFile:invocation[0]';
data: unknown;
};
'error.platform.WithEncryption.requestOtp:invocation[0]': {
type: 'error.platform.WithEncryption.requestOtp:invocation[0]';
data: unknown;
};
'xstate.init': {type: 'xstate.init'};
};
invokeSrcNameMap: {
checkStorageAvailability: 'done.invoke.WithEncryption.backingUp.checkStorageAvailability:invocation[0]';
hashEncKey: 'done.invoke.WithEncryption.hashKey:invocation[0]';
requestOtp: 'done.invoke.WithEncryption.requestOtp:invocation[0]';
writeDataToFile: 'done.invoke.WithEncryption.backingUp.writeDataToFile:invocation[0]';
zipBackupFile: 'done.invoke.WithEncryption.backingUp.zipBackupFile:invocation[0]';
};
missingImplementations: {
actions:
| ''
| 'sendDataBackupFailureEvent'
| 'sendDataBackupStartEvent'
| 'sendDataBackupSuccessEvent'
| 'setFileName';
delays: never;
guards: 'isMinimumStorageRequiredForBackupReached';
services: 'checkStorageAvailability' | 'requestOtp';
};
eventsCausingActions: {
'': 'error.platform.WithEncryption.requestOtp:invocation[0]';
fetchAllDataFromDB: 'done.invoke.WithEncryption.backingUp.checkStorageAvailability:invocation[0]';
sendDataBackupFailureEvent:
| 'done.invoke.WithEncryption.backingUp.checkStorageAvailability:invocation[0]'
| 'error.platform.WithEncryption.backingUp.zipBackupFile:invocation[0]';
sendDataBackupStartEvent: 'STORE_RESPONSE';
sendDataBackupSuccessEvent: 'done.invoke.WithEncryption.backingUp.zipBackupFile:invocation[0]';
setBaseEncKey: 'SET_BASE_ENC_KEY';
setDataFromStorage: 'STORE_RESPONSE';
setFileName: 'FILE_NAME';
setHashedKey: 'done.invoke.WithEncryption.hashKey:invocation[0]';
setOtp: 'INPUT_OTP';
storeHashedEncKey: 'done.invoke.WithEncryption.hashKey:invocation[0]';
storePasswordKeyType: 'SET_BASE_ENC_KEY';
storePhoneNumberKeyType: 'INPUT_OTP';
};
eventsCausingDelays: {};
eventsCausingGuards: {
isMinimumStorageRequiredForBackupReached: 'done.invoke.WithEncryption.backingUp.checkStorageAvailability:invocation[0]';
};
eventsCausingServices: {
checkStorageAvailability: 'STORE_RESPONSE';
hashEncKey: 'INPUT_OTP' | 'STORE_RESPONSE';
requestOtp: 'SEND_OTP';
writeDataToFile: 'STORE_RESPONSE';
zipBackupFile: 'FILE_NAME';
};
matchesStates:
| 'backUp'
| 'backingUp'
| 'backingUp.checkStorageAvailability'
| 'backingUp.failure'
| 'backingUp.fetchDataFromDB'
| 'backingUp.idle'
| 'backingUp.success'
| 'backingUp.writeDataToFile'
| 'backingUp.zipBackupFile'
| 'hashKey'
| 'init'
| 'passwordBackup'
| 'phoneNumberBackup'
| 'requestOtp'
| 'selectPref'
| {
backingUp?:
| 'checkStorageAvailability'
| 'failure'
| 'fetchDataFromDB'
| 'idle'
| 'success'
| 'writeDataToFile'
| 'zipBackupFile';
};
tags: never;
}

View File

@@ -48,6 +48,7 @@ const model = createModel(
TRY_AGAIN: () => ({}),
IGNORE: () => ({}),
GET: (key: string) => ({key}),
EXPORT: () => ({}),
DECRYPT_ERROR: () => ({}),
KEY_INVALIDATE_ERROR: () => ({}),
BIOMETRIC_CANCELLED: (requester?: string) => ({requester}),
@@ -189,6 +190,9 @@ export const storeMachine =
GET: {
actions: 'forwardStoreRequest',
},
EXPORT: {
actions: 'forwardStoreRequest',
},
SET: {
actions: 'forwardStoreRequest',
},
@@ -345,6 +349,10 @@ export const storeMachine =
);
break;
}
case 'EXPORT': {
response = await exportData(context.encryptionKey);
break;
}
case 'SET': {
await setItem(event.key, event.value, context.encryptionKey);
response = event.value;
@@ -547,6 +555,10 @@ export async function setItem(
}
}
export async function exportData(encryptionKey: string) {
return Storage.exportData(encryptionKey);
}
export async function getItem(
key: string,
defaultValue: unknown,

22
package-lock.json generated
View File

@@ -79,6 +79,7 @@
"react-native-spinkit": "^1.5.1",
"react-native-svg": "13.4.0",
"react-native-vector-icons": "^10.0.0",
"react-native-zip-archive": "^6.1.0",
"short-unique-id": "^4.4.4",
"simple-pem2jwk": "^0.2.4",
"telemetry-sdk": "git://github.com/mosip/sunbird-telemetry-sdk.git#f762be5732ee552c0c70bdd540aa4e2701554c71",
@@ -24961,6 +24962,15 @@
"node": ">=10"
}
},
"node_modules/react-native-zip-archive": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-native-zip-archive/-/react-native-zip-archive-6.1.0.tgz",
"integrity": "sha512-FEt6O8YD/Is48HGXuHndFktod7S2cUdf0C+B2XRHWeS3zs/gXlzOGo1gcwUxdMyqQwEwnFuAqlYvUK4BNGQsDg==",
"peerDependencies": {
"react": ">=16.8.6",
"react-native": ">=0.60.0"
}
},
"node_modules/react-native/node_modules/metro-runtime": {
"version": "0.73.9",
"resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.73.9.tgz",
@@ -30952,7 +30962,7 @@
"bs58": "^4.0.1",
"crypto-ld": "^4.0.2",
"esm": "^3.2.25",
"node-forge": "^1.3.1",
"node-forge": "~0.9.1",
"semver": "^7.3.2",
"sodium-native": "^3.1.1"
},
@@ -31293,7 +31303,7 @@
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.5.tgz",
"integrity": "sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==",
"requires": {
"node-forge": "^1.3.1",
"node-forge": "^1.2.1",
"nullthrows": "^1.1.1"
}
},
@@ -45845,7 +45855,7 @@
"es6-promise": "^4.2.8",
"lodash": "^4.17.21",
"long": "^5.2.0",
"node-forge": "^1.3.1",
"node-forge": "^1.2.1",
"pako": "^2.0.4",
"process": "^0.11.10",
"uuid": "^9.0.0"
@@ -47669,6 +47679,12 @@
}
}
},
"react-native-zip-archive": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-native-zip-archive/-/react-native-zip-archive-6.1.0.tgz",
"integrity": "sha512-FEt6O8YD/Is48HGXuHndFktod7S2cUdf0C+B2XRHWeS3zs/gXlzOGo1gcwUxdMyqQwEwnFuAqlYvUK4BNGQsDg==",
"requires": {}
},
"react-refresh": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.4.3.tgz",

View File

@@ -81,6 +81,7 @@
"react-native-spinkit": "^1.5.1",
"react-native-svg": "13.4.0",
"react-native-vector-icons": "^10.0.0",
"react-native-zip-archive": "^6.1.0",
"short-unique-id": "^4.4.4",
"simple-pem2jwk": "^0.2.4",
"telemetry-sdk": "git://github.com/mosip/sunbird-telemetry-sdk.git#f762be5732ee552c0c70bdd540aa4e2701554c71",

View File

@@ -0,0 +1,45 @@
import {useSelector} from '@xstate/react';
import {useContext} from 'react';
import {
BackupEvents,
selectIsBackingUp,
selectIsBackingUpSuccess,
selectIsBackingUpSFailure,
} from '../../machines/backup';
import {GlobalContext} from '../../shared/GlobalContext';
export function useBackupScreen() {
const {appService} = useContext(GlobalContext);
const backupService = appService.children.get('backup');
return {
isBackingUp: useSelector(backupService, selectIsBackingUp),
isBackingUpSuccess: useSelector(backupService, selectIsBackingUpSuccess),
isBackingUpFailure: useSelector(backupService, selectIsBackingUpSFailure),
DATA_BACKUP: () => {
backupService.send(BackupEvents.DATA_BACKUP());
},
OK: () => {
backupService.send(BackupEvents.OK());
},
DISMISS: () => {
backupService.send(BackupEvents.DISMISS());
},
FETCH_DATA: () => {
backupService.send(BackupEvents.FETCH_DATA());
},
PASSWORD: () => {
backupService.send(BackupEvents.PASSWORD());
},
SET_BASE_ENC_KEY: (key: string) => {
backupService.send(BackupEvents.SET_BASE_ENC_KEY(key));
},
PHONE_NUMBER: () => {
backupService.send(BackupEvents.PHONE_NUMBER());
},
SEND_OTP: () => {
backupService.send(BackupEvents.SEND_OTP());
},
INPUT_OTP: (otp: string) => backupService.send(BackupEvents.INPUT_OTP(otp)),
};
}

View File

@@ -0,0 +1,67 @@
import React, {useState} from 'react';
import {Switch} from 'react-native-elements';
import {Text} from '../../components/ui';
import {Modal} from '../../components/ui/Modal';
import {Theme} from '../../components/ui/styleUtils';
import {useBackupScreen} from './BackupController';
import {Platform} from 'react-native';
import {MessageOverlay} from '../../components/MessageOverlay';
export const BackupToggle: React.FC<BackupToggleProps> = props => {
const [dataBackup, setDataBackup] = useState(false);
const controller = useBackupScreen(props);
const toggleSwitch = () => {
setDataBackup(!dataBackup);
if (!dataBackup) {
controller.FETCH_DATA();
}
};
return (
<React.Fragment>
<Modal
isVisible={props.isVisible}
headerTitle={'Data Backup Toggle'}
headerElevation={2}
arrowLeft={true}
onDismiss={props.onDismiss}>
<Text> Enable Data backup</Text>
<Switch
value={dataBackup}
onValueChange={toggleSwitch}
trackColor={{
false: Theme.Colors.switchTrackFalse,
true:
Platform.OS == 'ios'
? Theme.Colors.switchHead
: Theme.Colors.switchTrackTrue,
}}
color={Theme.Colors.switchHead}
/>
</Modal>
<MessageOverlay
isVisible={controller.isBackingUpSuccess}
onButtonPress={() => {
controller.OK(), setDataBackup(false);
}}
buttonText="OK"
title={'Backup Successful'}
/>
<MessageOverlay
isVisible={controller.isBackingUpFailure}
onButtonPress={() => {
controller.OK(), setDataBackup(false);
}}
buttonText="OK"
title={'Backup Failed'}
/>
</React.Fragment>
);
};
interface BackupToggleProps {
isVisible: boolean;
onDismiss: () => void;
}

View File

@@ -0,0 +1,45 @@
import React, {useState} from 'react';
import {Pressable} from 'react-native';
import {Icon, ListItem} from 'react-native-elements';
import {Text} from '../../components/ui';
import {Theme} from '../../components/ui/styleUtils';
import {useBackupScreen} from './BackupController';
import {BackupToggle} from './BackupToggle';
export const DataBackup: React.FC = ({} = props => {
const controller = useBackupScreen(props);
// TODO : Check if the setup is already done
return (
<React.Fragment>
<Pressable
onPress={() => {
controller.DATA_BACKUP();
}}>
<ListItem topDivider bottomDivider>
<Icon
type={'feather'}
name={'file'}
color={Theme.Colors.Icon}
size={25}
/>
<ListItem.Content>
<ListItem.Title style={{paddingTop: 3}}>
<Text weight="semibold" color={Theme.Colors.settingsLabel}>
Data Backup
</Text>
</ListItem.Title>
</ListItem.Content>
</ListItem>
</Pressable>
{controller.isBackingUp && (
<BackupToggle
isVisible={controller.isBackingUp}
onDismiss={() => controller.DISMISS()}
/>
)}
</React.Fragment>
);
});

View File

@@ -10,13 +10,14 @@ import {useTranslation} from 'react-i18next';
import {LanguageSelector} from '../../components/LanguageSelector';
import {ScrollView} from 'react-native-gesture-handler';
import {Modal} from '../../components/ui/Modal';
import {CREDENTIAL_REGISTRY_EDIT} from 'react-native-dotenv';
import {CREDENTIAL_REGISTRY_EDIT, DATA_BACKUP} from 'react-native-dotenv';
import {AboutInji} from './AboutInji';
import {EditableListItem} from '../../components/EditableListItem';
import {RequestRouteProps, RootRouteProps} from '../../routes';
import {ReceivedCards} from './ReceivedCards';
import testIDProps from '../../shared/commonUtil';
import {SvgImage} from '../../components/ui/svg';
import {DataBackup} from './DataBackup';
const LanguageSetting: React.FC = () => {
const {t} = useTranslation('SettingScreen');
@@ -159,6 +160,8 @@ export const SettingScreen: React.FC<
<AboutInji appId={controller.appId} />
{DATA_BACKUP === 'true' && <DataBackup />}
{CREDENTIAL_REGISTRY_EDIT === 'true' && (
<EditableListItem
testID="credentialRegistry"

View File

@@ -9,6 +9,7 @@ import {settingsMachine} from '../machines/settings';
import {storeMachine} from '../machines/store';
import {vcMachine} from '../machines/vc';
import {revokeVidsMachine} from '../machines/revoke';
import {backupMachine} from '../machines/backup';
export const GlobalContext = createContext({} as GlobalServices);
@@ -25,4 +26,5 @@ export interface AppServices {
request: ActorRefFrom<typeof requestMachine>;
scan: ActorRefFrom<typeof scanMachine>;
revoke: ActorRefFrom<typeof revokeVidsMachine>;
backup: ActorRefFrom<typeof backupMachine>;
}

View File

@@ -178,21 +178,19 @@ async function generateCacheAPIFunctionWithCachePreference(
) {
const existingCredentials = await Keychain.getGenericPassword();
try {
const response = (await getItem(
const response = await getItem(
cacheKey,
null,
existingCredentials?.password,
)) as string;
);
if (response) {
return JSON.parse(response);
return response;
} else {
const response = await fetchCall();
setItem(
cacheKey,
JSON.stringify(response),
existingCredentials?.password,
).then(() => console.log('Cached response for ' + cacheKey));
setItem(cacheKey, response, existingCredentials?.password).then(() =>
console.log('Cached response for ' + cacheKey),
);
return response;
}
@@ -219,11 +217,9 @@ async function generateCacheAPIFunctionWithAPIPreference(
const existingCredentials = await Keychain.getGenericPassword();
try {
const response = await fetchCall();
setItem(
cacheKey,
JSON.stringify(response),
existingCredentials.password,
).then(() => console.log('Cached response for ' + cacheKey));
setItem(cacheKey, response, existingCredentials.password).then(() =>
console.log('Cached response for ' + cacheKey),
);
return response;
} catch (error) {
console.warn(`Failed to load due to network issue in API preferred api call.
@@ -233,14 +229,14 @@ async function generateCacheAPIFunctionWithAPIPreference(
console.log(error);
const response = (await getItem(
const response = await getItem(
cacheKey,
null,
existingCredentials.password,
)) as string;
);
if (response) {
return JSON.parse(response);
return response;
} else {
if (response == null) {
throw error;

View File

@@ -2,6 +2,8 @@ import argon2 from 'react-native-argon2';
import {AnyState} from 'xstate';
import {getDeviceNameSync} from 'react-native-device-info';
import {isAndroid} from './constants';
import {generateSecureRandom} from 'react-native-securerandom';
import forge from 'node-forge';
export const hashData = async (
data: string,
@@ -12,6 +14,21 @@ export const hashData = async (
return result.rawHash as string;
};
export const generateRandomString = async () => {
const randomBytes = await generateSecureRandom(64);
const randomString = randomBytes.reduce(
(acc, byte) => acc + byte.toString(16).padStart(2, '0'),
'',
);
return randomString;
};
export const generateBackupEncryptionKey = (
password: string,
salt: string,
iterations: number,
length: number,
) => forge.pkcs5.pbkdf2(password, salt, iterations, length);
export interface Argon2iConfig {
iterations: number;
memory: number;
@@ -75,3 +92,7 @@ export const faceMatchConfig = (resp: string) => {
},
};
};
export const getBackupFileName = () => {
return `backup_${Date.now()}`;
};

View File

@@ -16,6 +16,14 @@ export const RECEIVED_VCS_STORE_KEY = 'receivedVCs';
export const MY_LOGIN_STORE_KEY = 'myLogins';
export const BACKUP_ENC_KEY = 'backupEncKey';
export const BACKUP_ENC_KEY_TYPE = 'backupEncKeyType';
export const BACKUP_ENC_TYPE_VAL_PASSWORD = 'password';
export const BACKUP_ENC_TYPE_VAL_PHONE = 'phone';
export let individualId = {id: '', idType: 'UIN' as VcIdType};
export const GET_INDIVIDUAL_ID = (currentIndividualId: IndividualId) => {
@@ -63,6 +71,22 @@ export const argon2iConfigForUinVid: Argon2iConfig = {
mode: 'argon2i',
};
export const argon2iConfigForBackupFileName: Argon2iConfig = {
iterations: 5,
memory: 16 * 1024,
parallelism: 2,
hashLength: 8,
mode: 'argon2id',
};
export const argon2iConfigForPasswordAndPhoneNumber: Argon2iConfig = {
// TODO: expected iterations for hashing password and phone Number is 600000
iterations: 500,
memory: 16 * 1024,
parallelism: 2,
hashLength: 30,
mode: 'argon2id',
};
export const argon2iSalt =
'1234567891011121314151617181920212223242526272829303132333435363';

View File

@@ -6,6 +6,8 @@ import {
stat,
unlink,
writeFile,
readDir,
ReadDirItem,
} from 'react-native-fs';
interface CacheData {
@@ -17,6 +19,7 @@ interface Cache {
[key: string]: CacheData;
}
import * as RNZipArchive from 'react-native-zip-archive';
class FileStorage {
cache: Cache = {};
@@ -24,6 +27,10 @@ class FileStorage {
return await readFile(path, 'utf8');
}
async getAllFilesInDirectory(path: string) {
return await readDir(path);
}
async writeFile(path: string, data: string) {
return await writeFile(path, data, 'utf8');
}
@@ -52,8 +59,41 @@ export default new FileStorage();
* android: /data/user/0/io.mosip.residentapp/files/inji/VC/<filename>
* These paths are coming from DocumentDirectoryPath in react-native-fs.
*/
export const vcDirectoryPath = `${DocumentDirectoryPath}/inji/VC`;
export const backupDirectoryPath = `${DocumentDirectoryPath}/inji/backup`;
export const zipFilePath = (filename: string) =>
`${DocumentDirectoryPath}/inji/backup/${filename}.zip`;
export const getFilePath = (key: string) => {
return `${vcDirectoryPath}/${key}.txt`;
};
export const vcDirectoryPath = `${DocumentDirectoryPath}/inji/VC`;
export const getBackupFilePath = (key: string) => {
return `${backupDirectoryPath}/${key}.injibackup`;
};
export async function compressAndRemoveFile(fileName: string): Promise<string> {
const result = await compressFile(fileName);
await removeFile(fileName);
return result;
}
async function compressFile(fileName: string): Promise<string> {
return await RNZipArchive.zip(backupDirectoryPath, zipFilePath(fileName));
}
async function removeFile(fileName: string) {
await new FileStorage().removeItem(getBackupFilePath(fileName));
}
export async function getDirectorySize(path: string) {
const directorySize = await new FileStorage()
.getAllFilesInDirectory(path)
.then((result: ReadDirItem[]) => {
let folderEntriesSizeInBytes = 0;
result.forEach(fileItem => {
folderEntriesSizeInBytes += Number(fileItem.size);
});
return folderEntriesSizeInBytes;
});
return directorySize;
}

View File

@@ -13,27 +13,24 @@ import {
isHardwareKeystoreExists,
} from './cryptoutil/cryptoUtil';
import {VCMetadata} from './VCMetadata';
import {ENOENT, getItem} from '../machines/store';
import {ENOENT} from '../machines/store';
import {
androidVersion,
isAndroid,
MY_VCS_STORE_KEY,
RECEIVED_VCS_STORE_KEY,
SETTINGS_STORE_KEY,
} from './constants';
import FileStorage, {
backupDirectoryPath,
getBackupFilePath,
getFilePath,
getFilePathOfEncryptedHmac,
getDirectorySize,
vcDirectoryPath,
} from './fileStorage';
import {__AppId} from './GlobalVariables';
import {
getErrorEventData,
getImpressionEventData,
sendErrorEvent,
sendImpressionEvent,
} from './telemetry/TelemetryUtils';
import {getErrorEventData, sendErrorEvent} from './telemetry/TelemetryUtils';
import {TelemetryConstants} from './telemetry/TelemetryConstants';
import {getBackupFileName} from './commonUtil';
export const MMKV = new MMKVLoader().initialize();
@@ -56,6 +53,45 @@ async function generateHmac(
}
class Storage {
static exportData = async (encryptionKey: string) => {
const completeBackupData = {};
const dataFromDB: Record<string, any> = {};
const allKeysInDB = await MMKV.indexer.strings.getKeys();
const keysToBeExported = allKeysInDB.filter(key =>
key.includes('CACHE_FETCH_ISSUER_WELLKNOWN_CONFIG_'),
);
keysToBeExported.push(MY_VCS_STORE_KEY);
const encryptedDataPromises = keysToBeExported.map(key =>
MMKV.getItem(key),
);
Promise.all(encryptedDataPromises).then(encryptedDataList => {
keysToBeExported.forEach(async (key, index) => {
let encryptedData = encryptedDataList[index];
if (encryptedData != null) {
const decryptedData = await decryptJson(encryptionKey, encryptedData);
dataFromDB[key] = JSON.parse(decryptedData);
}
});
});
completeBackupData['dataFromDB'] = dataFromDB;
completeBackupData['VC_Records'] = {};
let vcKeys = allKeysInDB.filter(key => key.indexOf('VC_') === 0);
for (let ind in vcKeys) {
const key = vcKeys[ind];
const vc = await Storage.readVCFromFile(key);
const decryptedVCData = await decryptJson(encryptionKey, vc);
const deactivatedVC =
removeWalletBindingDataBeforeBackup(decryptedVCData);
completeBackupData['VC_Records'][key] = deactivatedVC;
}
return completeBackupData;
};
static isVCStorageInitialised = async (): Promise<boolean> => {
try {
const res = await FileStorage.getInfo(vcDirectoryPath);
@@ -252,3 +288,33 @@ class Storage {
}
export default Storage;
export async function writeToBackupFile(data): Promise<string> {
const fileName = getBackupFileName();
const isDirectoryExists = await FileStorage.exists(backupDirectoryPath);
if (isDirectoryExists) {
await FileStorage.removeItem(backupDirectoryPath);
}
await FileStorage.createDirectory(backupDirectoryPath);
const path = getBackupFilePath(fileName);
await FileStorage.writeFile(path, JSON.stringify(data));
return fileName;
}
function removeWalletBindingDataBeforeBackup(data: string) {
const vcData = JSON.parse(data);
vcData.walletBindingResponse = null;
vcData.publicKey = null;
vcData.privateKey = null;
return vcData;
}
export async function isMinimumLimitForBackupReached() {
const directorySize = await getDirectorySize(vcDirectoryPath);
const freeDiskStorageInBytes =
isAndroid() && androidVersion < 29
? getFreeDiskStorageOldSync()
: getFreeDiskStorageSync();
return freeDiskStorageInBytes <= 2 * directorySize;
}

View File

@@ -12,6 +12,7 @@ export const TelemetryConstants = {
vcLockOrRevoke: 'VC Lock / VC Revoke',
getVcUsingAid: 'Get VC using AID',
fetchData: 'Fetch Data',
dataBackup: 'Data Backup',
}),
EndEventStatus: Object.freeze({
@@ -62,5 +63,6 @@ export const TelemetryConstants = {
vcList: 'VC List',
vcShareSuccessPage: 'VC Successfully Shared Page',
vcReceivedSuccessPage: 'VC Successfully Received Page',
dataBackupScreen: 'Data Backup Screen',
}),
};