mirror of
https://github.com/mosip/inji-wallet.git
synced 2026-01-09 13:38:01 -05:00
[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:
2
.env
2
.env
@@ -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)
|
||||
|
||||
42
.talismanrc
42
.talismanrc
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
219
machines/backup.ts
Normal 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>;
|
||||
73
machines/backup.typegen.ts
Normal file
73
machines/backup.typegen.ts
Normal 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;
|
||||
}
|
||||
305
machines/backupWithEncryption.ts
Normal file
305
machines/backupWithEncryption.ts
Normal 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>;
|
||||
104
machines/backupWithEncryption.typegen.ts
Normal file
104
machines/backupWithEncryption.typegen.ts
Normal 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;
|
||||
}
|
||||
@@ -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
22
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
45
screens/Settings/BackupController.tsx
Normal file
45
screens/Settings/BackupController.tsx
Normal 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)),
|
||||
};
|
||||
}
|
||||
67
screens/Settings/BackupToggle.tsx
Normal file
67
screens/Settings/BackupToggle.tsx
Normal 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;
|
||||
}
|
||||
45
screens/Settings/DataBackup.tsx
Normal file
45
screens/Settings/DataBackup.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()}`;
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user