mirror of
https://github.com/mosip/inji-wallet.git
synced 2026-01-07 20:53:54 -05:00
* [INJI-314] initial commit Signed-off-by: Sri Kanth Kola <srikanthsri7447@gmail.com> * [INJI-314] add WIP ut stuff Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-314] fix tsconfig for project type Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-314] kludge - skip checking types during unit tests * mock image imports globally * enable tests to run from any dir Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-314]: mocked modules Signed-off-by: srikanth716 <srikanthsri7447@gmail.com> * [INJIMOB-314]: mock react-native-google-signin Signed-off-by: srikanth716 <srikanthsri7447@gmail.com> * [INJIMOB-314]: use defined mock from google-signin lib other details: - mock base58.., rn-linear-gradient, expo-camera - add @react-native/assets-registry to make jest-expo babel preset work Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-314] reorganise global const mocks into jest's setupFiles Co-authored-by: srikanth716 <srikanthsri7447@gmail.com> Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-900] write tests for ActivityLogEvent * init relevant mocks * remove redundant global mocks from test files Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-900] fixup mocks of mmkv, zip-archive, rnfs Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-900]: write unit test for commonUtil Signed-off-by: srikanth716 <srikanthsri7447@gmail.com> * [INJIMOB-900]: write unit test for commonUtil Signed-off-by: srikanth716 <srikanthsri7447@gmail.com> * [INJIMOB-900]: sample unit test for settingsScreen Signed-off-by: srikanth716 <srikanthsri7447@gmail.com> * [INJIMOB-900] add SettingScreen test Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-900] update sample tests for commonUtil Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-900]: refactoring the settingsScreen test for Android and IOS Signed-off-by: srikanth716 <srikanthsri7447@gmail.com> * [INJIMOB-900] update test snapshots and add kludge in auth for testing kludge: auth state machine now exports a selector with optional fields as state machine isn't running in a test environment Co-authored-by: srikanth716 <srikanthsri7447@gmail.com> Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-900]: snapshot test to settings screen Signed-off-by: srikanth716 <srikanthsri7447@gmail.com> * [INJIMOB-900] update mocks for testing and ACK reviews Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-900] downgrade expo to match compatibility matrix Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-900] upgrade expo to 49 to build the iOS app(kludge) Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> * [INJIMOB-900] configure react-native-vector-icons as per README Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> --------- Signed-off-by: Sri Kanth Kola <srikanthsri7447@gmail.com> Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> Signed-off-by: srikanth716 <srikanthsri7447@gmail.com> Co-authored-by: Sri Kanth Kola <srikanthsri7447@gmail.com>
519 lines
16 KiB
TypeScript
519 lines
16 KiB
TypeScript
import {MMKVLoader} from 'react-native-mmkv-storage';
|
|
import getAllConfigurations from './api';
|
|
import {
|
|
getFreeDiskStorageOldSync,
|
|
getFreeDiskStorageSync,
|
|
} from 'react-native-device-info';
|
|
import SecureKeystore from '@mosip/secure-keystore';
|
|
import {
|
|
decryptJson,
|
|
encryptJson,
|
|
HMAC_ALIAS,
|
|
hmacSHA,
|
|
isHardwareKeystoreExists,
|
|
} from './cryptoutil/cryptoUtil';
|
|
import {VCMetadata} from './VCMetadata';
|
|
import {
|
|
androidVersion,
|
|
isAndroid,
|
|
MY_VCS_STORE_KEY,
|
|
SETTINGS_STORE_KEY,
|
|
ENOENT,
|
|
} from './constants';
|
|
import FileStorage, {
|
|
getFilePath,
|
|
getDirectorySize,
|
|
vcDirectoryPath,
|
|
} from './fileStorage';
|
|
import {__AppId} from './GlobalVariables';
|
|
import {getErrorEventData, sendErrorEvent} from './telemetry/TelemetryUtils';
|
|
import {TelemetryConstants} from './telemetry/TelemetryConstants';
|
|
import {BYTES_IN_MEGABYTE} from './commonUtil';
|
|
import fileStorage from './fileStorage';
|
|
import {DocumentDirectoryPath, ReadDirItem} from 'react-native-fs';
|
|
|
|
export const MMKV = new MMKVLoader().initialize();
|
|
|
|
async function generateHmac(
|
|
encryptionKey: string,
|
|
data: string,
|
|
): Promise<string> {
|
|
if (!isHardwareKeystoreExists) {
|
|
return hmacSHA(encryptionKey, data);
|
|
}
|
|
return await SecureKeystore.generateHmacSha(HMAC_ALIAS, data);
|
|
}
|
|
|
|
class Storage {
|
|
static exportData = async (encryptionKey: string) => {
|
|
try {
|
|
const completeBackupData = {};
|
|
let 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),
|
|
);
|
|
const encryptedDataList = await Promise.all(encryptedDataPromises);
|
|
for (let index = 0; index < keysToBeExported.length; index++) {
|
|
const key = keysToBeExported[index];
|
|
let encryptedData = encryptedDataList[index];
|
|
if (encryptedData != null) {
|
|
const decryptedData = await decryptJson(encryptionKey, encryptedData);
|
|
dataFromDB[key] = JSON.parse(decryptedData);
|
|
}
|
|
}
|
|
|
|
dataFromDB[MY_VCS_STORE_KEY].map(myVcMetadata => {
|
|
myVcMetadata.isPinned = false;
|
|
});
|
|
|
|
completeBackupData['VC_Records'] = {};
|
|
|
|
let vcKeys = allKeysInDB.filter(key => key.indexOf('VC_') === 0);
|
|
for (let ind in vcKeys) {
|
|
const key = vcKeys[ind];
|
|
const vc = await this.getItem(key, encryptionKey);
|
|
|
|
if (vc) {
|
|
const decryptedVCData = await decryptJson(encryptionKey, vc);
|
|
const deactivatedVC =
|
|
removeWalletBindingDataBeforeBackup(decryptedVCData);
|
|
completeBackupData['VC_Records'][key] = deactivatedVC;
|
|
} else {
|
|
dataFromDB.myVCs = dataFromDB.myVCs.filter(vcMetaData => {
|
|
return (
|
|
VCMetadata.fromVcMetadataString(vcMetaData).getVcKey() !== key
|
|
);
|
|
});
|
|
}
|
|
}
|
|
completeBackupData['dataFromDB'] = dataFromDB;
|
|
|
|
return completeBackupData;
|
|
} catch (error) {
|
|
sendErrorEvent(
|
|
getErrorEventData(
|
|
TelemetryConstants.FlowType.dataBackup,
|
|
error.message,
|
|
error.stack,
|
|
),
|
|
);
|
|
console.error('exporting data is failed due to this error:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
static loadBackupData = async (data, encryptionKey) => {
|
|
try {
|
|
// 0. check for previous backup states
|
|
const prevBkpState = `${DocumentDirectoryPath}/.prev`;
|
|
const previousBackupExists = await fileStorage.exists(prevBkpState);
|
|
let previousRestoreTimestamp: string = '';
|
|
if (previousBackupExists) {
|
|
// 0. Remove partial restored files
|
|
previousRestoreTimestamp = await fileStorage.readFile(prevBkpState);
|
|
previousRestoreTimestamp = previousRestoreTimestamp.trim();
|
|
this.unloadVCs(encryptionKey, parseInt(previousRestoreTimestamp));
|
|
}
|
|
// 1. opening the file
|
|
const completeBackupData = JSON.parse(data);
|
|
// 2. Load and store VC_records & MMKV things
|
|
const backupStartState = Date.now().toString();
|
|
// record the state to help with cleanup activities post partial backup
|
|
await fileStorage.writeFile(prevBkpState, backupStartState);
|
|
const dataFromDB = await Storage.loadVCs(
|
|
completeBackupData,
|
|
encryptionKey,
|
|
);
|
|
// 3. Update the Well Known configs of the VCs
|
|
const allKeysFromDB = Object.keys(dataFromDB);
|
|
const cacheKeys = allKeysFromDB.filter(key =>
|
|
key.includes('CACHE_FETCH_ISSUER_WELLKNOWN_CONFIG_'),
|
|
);
|
|
cacheKeys.forEach(async key => {
|
|
const value = dataFromDB[key];
|
|
const encryptedValue = await encryptJson(
|
|
encryptionKey,
|
|
JSON.stringify(value),
|
|
);
|
|
await this.setItem(key, encryptedValue, encryptionKey);
|
|
return true;
|
|
});
|
|
} catch (error) {
|
|
return error;
|
|
}
|
|
};
|
|
static isVCStorageInitialised = async (): Promise<boolean> => {
|
|
try {
|
|
const res = await FileStorage.getInfo(vcDirectoryPath);
|
|
return res.isDirectory();
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
static setItem = async (
|
|
key: string,
|
|
data: string,
|
|
encryptionKey?: string,
|
|
) => {
|
|
try {
|
|
const isSavingVC = VCMetadata.isVCKey(key);
|
|
if (isSavingVC) {
|
|
await this.storeVC(key, data);
|
|
return await this.storeVcHmac(encryptionKey, data, key);
|
|
}
|
|
|
|
await MMKV.setItem(key, data);
|
|
} catch (error) {
|
|
console.error('Error Occurred while saving in Storage.', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
static getItem = async (key: string, encryptionKey?: string) => {
|
|
try {
|
|
const isVCKey = VCMetadata.isVCKey(key);
|
|
|
|
if (isVCKey) {
|
|
const data = await this.readVCFromFile(key);
|
|
const isCorrupted = await this.isCorruptedVC(key, encryptionKey, data);
|
|
|
|
if (isCorrupted) {
|
|
sendErrorEvent(
|
|
getErrorEventData(
|
|
TelemetryConstants.FlowType.fetchData,
|
|
TelemetryConstants.ErrorId.tampered,
|
|
'VC is corrupted and will be deleted from storage',
|
|
),
|
|
);
|
|
console.debug(
|
|
'[Inji-406]: VC is corrupted and will be deleted from storage',
|
|
);
|
|
console.debug('[Inji-406]: VC key: ', key);
|
|
console.debug('[Inji-406]: is Data null', data === null);
|
|
}
|
|
|
|
return isCorrupted ? null : data;
|
|
}
|
|
|
|
return await MMKV.getItem(key);
|
|
} catch (error) {
|
|
const isVCKey = VCMetadata.isVCKey(key);
|
|
|
|
if (isVCKey) {
|
|
const isDownloaded = await this.isVCAlreadyDownloaded(
|
|
key,
|
|
encryptionKey,
|
|
);
|
|
|
|
if (isDownloaded && error.message.includes(ENOENT)) {
|
|
sendErrorEvent(
|
|
getErrorEventData(
|
|
TelemetryConstants.FlowType.fetchData,
|
|
TelemetryConstants.ErrorId.dataRetrieval,
|
|
error.message,
|
|
),
|
|
);
|
|
throw new Error(ENOENT);
|
|
}
|
|
}
|
|
sendErrorEvent(
|
|
getErrorEventData(
|
|
TelemetryConstants.FlowType.fetchData,
|
|
TelemetryConstants.ErrorId.dataRetrieval,
|
|
'Error Occurred while retrieving from Storage',
|
|
),
|
|
);
|
|
|
|
console.error('Error Occurred while retrieving from Storage.', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* unloadVCs will remove a set of VCs against a particular time range
|
|
*
|
|
* @param cutOffTimestamp the timestamp of the VC backup start time to current time
|
|
*/
|
|
private static async unloadVCs(encryptionKey: any, cutOffTimestamp: number) {
|
|
try {
|
|
// 1. Find the VCs in the inji directory which have the said timestamp
|
|
const file: ReadDirItem[] = await fileStorage.getAllFilesInDirectory(
|
|
`${DocumentDirectoryPath}/inji/VC/`,
|
|
);
|
|
const isGreaterThanEq = function (fName: string, ts: number): boolean {
|
|
fName = fName.split('.')[0];
|
|
const curr = fName.split('_')[1];
|
|
return parseInt(curr) >= ts;
|
|
};
|
|
for (let i = 0; i < file.length; i++) {
|
|
const f = file[i];
|
|
if (isGreaterThanEq(f.name, cutOffTimestamp)) {
|
|
await fileStorage.removeItem(f.path);
|
|
}
|
|
}
|
|
// TODO: should this be done via the Store state machine to avoid popups?
|
|
// 3. Remove the keys from MMKV which have the same timestamp
|
|
let myVCsEnc = await MMKV.getItem(MY_VCS_STORE_KEY, encryptionKey);
|
|
if (myVCsEnc !== null) {
|
|
let mmkvVCs = await decryptJson(encryptionKey, myVCsEnc);
|
|
let vcList: VCMetadata[] = JSON.parse(mmkvVCs);
|
|
let newVCList: VCMetadata[] = [];
|
|
vcList.forEach(d => {
|
|
if (d.timestamp && parseInt(d.timestamp) < cutOffTimestamp) {
|
|
newVCList.push(d);
|
|
}
|
|
});
|
|
const finalVC = await encryptJson(
|
|
encryptionKey,
|
|
JSON.stringify(newVCList),
|
|
);
|
|
await MMKV.setItem(MY_VCS_STORE_KEY, finalVC);
|
|
}
|
|
} catch (e) {
|
|
console.error('error while unloadVcs:', e);
|
|
sendErrorEvent(
|
|
getErrorEventData(
|
|
TelemetryConstants.FlowType.dataRestore,
|
|
TelemetryConstants.ErrorId.failure,
|
|
e.message,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
private static async loadVCs(completeBackupData: {}, encryptionKey: any) {
|
|
try {
|
|
const allVCs = completeBackupData['VC_Records'];
|
|
const allVCKeys = Object.keys(allVCs);
|
|
const dataFromDB = completeBackupData['dataFromDB'];
|
|
// 0. Check for VC presense in the store
|
|
// 1. store the VCs and the HMAC
|
|
allVCKeys.forEach(async key => {
|
|
let vc = allVCs[key];
|
|
const ts = Date.now();
|
|
const prevUnixTimeStamp = vc.vcMetadata.timestamp;
|
|
vc.vcMetadata.timestamp = ts;
|
|
dataFromDB.myVCs.forEach(myVcMetadata => {
|
|
if (
|
|
myVcMetadata.requestId === vc.vcMetadata.requestId &&
|
|
myVcMetadata.timestamp === prevUnixTimeStamp
|
|
) {
|
|
myVcMetadata.timestamp = ts;
|
|
}
|
|
});
|
|
const updatedVcKey = new VCMetadata(vc.vcMetadata).getVcKey();
|
|
const encryptedVC = await encryptJson(
|
|
encryptionKey,
|
|
JSON.stringify(vc),
|
|
);
|
|
const tmp = VCMetadata.fromVC(key);
|
|
// Save the VC to disk
|
|
await this.setItem(updatedVcKey, encryptedVC, encryptionKey);
|
|
});
|
|
// 2. Update myVCsKey
|
|
const dataFromMyVCKey = dataFromDB[MY_VCS_STORE_KEY];
|
|
const encryptedMyVCKeyFromMMKV = await MMKV.getItem(MY_VCS_STORE_KEY);
|
|
let newDataForMyVCKey: VCMetadata[] = [];
|
|
if (encryptedMyVCKeyFromMMKV != null) {
|
|
const myVCKeyFromMMKV = await decryptJson(
|
|
encryptionKey,
|
|
encryptedMyVCKeyFromMMKV,
|
|
);
|
|
newDataForMyVCKey = [
|
|
...JSON.parse(myVCKeyFromMMKV),
|
|
...dataFromMyVCKey,
|
|
];
|
|
} else {
|
|
newDataForMyVCKey = dataFromMyVCKey;
|
|
}
|
|
const encryptedDataForMyVCKey = await encryptJson(
|
|
encryptionKey,
|
|
JSON.stringify(newDataForMyVCKey),
|
|
);
|
|
await this.setItem(
|
|
MY_VCS_STORE_KEY,
|
|
encryptedDataForMyVCKey,
|
|
encryptionKey,
|
|
);
|
|
await fileStorage.removeItemIfExist(`${DocumentDirectoryPath}/.prev`);
|
|
return dataFromDB;
|
|
} catch (e) {
|
|
console.error('error while loading Vcs:', e);
|
|
sendErrorEvent(
|
|
getErrorEventData(
|
|
TelemetryConstants.FlowType.dataBackup,
|
|
TelemetryConstants.ErrorId.failure,
|
|
e.message,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
private static async isVCAlreadyDownloaded(
|
|
key: string,
|
|
encryptionKey: string,
|
|
) {
|
|
try {
|
|
const storedHMACofCurrentVC = await this.readHmacForVC(
|
|
key,
|
|
encryptionKey,
|
|
);
|
|
return storedHMACofCurrentVC !== null;
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private static async isCorruptedVC(
|
|
key: string,
|
|
encryptionKey: string,
|
|
data: string,
|
|
) {
|
|
// TODO: INJI-612 refactor
|
|
try {
|
|
const storedHMACofCurrentVC = await this.readHmacForDataCorruptionCheck(
|
|
key,
|
|
encryptionKey,
|
|
);
|
|
const HMACofVC = await generateHmac(encryptionKey, data);
|
|
return HMACofVC !== storedHMACofCurrentVC;
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private static async readHmacForVC(key: string, encryptionKey: string) {
|
|
try {
|
|
const encryptedHMACofCurrentVC = await MMKV.getItem(key);
|
|
if (encryptedHMACofCurrentVC) {
|
|
return decryptJson(encryptionKey, encryptedHMACofCurrentVC);
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
console.error('error while reading Hmac for VC ', e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private static async readHmacForDataCorruptionCheck(
|
|
key: string,
|
|
encryptionKey: string,
|
|
) {
|
|
try {
|
|
const encryptedHMACofCurrentVC = await MMKV.getItem(key);
|
|
if (encryptedHMACofCurrentVC) {
|
|
return decryptJson(encryptionKey, encryptedHMACofCurrentVC);
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
private static async readVCFromFile(key: string) {
|
|
return await FileStorage.readFile(getFilePath(key));
|
|
}
|
|
|
|
private static async storeVC(key: string, data: string) {
|
|
await FileStorage.createDirectory(vcDirectoryPath);
|
|
const path = getFilePath(key);
|
|
return await FileStorage.writeFile(path, data);
|
|
}
|
|
|
|
// TODO: INJI-612 refactor
|
|
private static async storeVcHmac(
|
|
encryptionKey: string,
|
|
data: string,
|
|
key: string,
|
|
) {
|
|
const HMACofVC = await generateHmac(encryptionKey, data);
|
|
const encryptedHMACofVC = await encryptJson(encryptionKey, HMACofVC);
|
|
await MMKV.setItem(key, encryptedHMACofVC);
|
|
}
|
|
|
|
static removeItem = async (key: string) => {
|
|
if (VCMetadata.isVCKey(key)) {
|
|
const path = getFilePath(key);
|
|
const isFileExists = await FileStorage.exists(path);
|
|
if (isFileExists) {
|
|
return await FileStorage.removeItem(path);
|
|
} else {
|
|
console.warn('file not exist`s');
|
|
}
|
|
}
|
|
MMKV.removeItem(key);
|
|
};
|
|
|
|
static clear = async () => {
|
|
try {
|
|
(await FileStorage.exists(`${vcDirectoryPath}`)) &&
|
|
(await FileStorage.removeItem(`${vcDirectoryPath}`));
|
|
const settings = await MMKV.getItem(SETTINGS_STORE_KEY);
|
|
const appId = JSON.parse(settings).appId;
|
|
__AppId.setValue(appId);
|
|
MMKV.clearStore();
|
|
await MMKV.setItem(SETTINGS_STORE_KEY, JSON.stringify({appId: appId}));
|
|
} catch (e) {
|
|
console.error('Error Occurred while Clearing Storage.', e);
|
|
}
|
|
};
|
|
|
|
static isMinimumLimitReached = async (limitInMB: string) => {
|
|
const configurations = await getAllConfigurations();
|
|
if (!configurations[limitInMB]) return false;
|
|
|
|
const minimumStorageLimitInBytes =
|
|
configurations[limitInMB] * BYTES_IN_MEGABYTE;
|
|
|
|
const freeDiskStorageInBytes =
|
|
isAndroid() && androidVersion < 29
|
|
? getFreeDiskStorageOldSync()
|
|
: getFreeDiskStorageSync();
|
|
|
|
|
|
return freeDiskStorageInBytes <= minimumStorageLimitInBytes;
|
|
};
|
|
}
|
|
|
|
export default Storage;
|
|
|
|
function removeWalletBindingDataBeforeBackup(data: string) {
|
|
const vcData = JSON.parse(data);
|
|
vcData.walletBindingResponse = null;
|
|
vcData.publicKey = null;
|
|
vcData.privateKey = null;
|
|
return vcData;
|
|
}
|
|
|
|
export async function isMinimumLimitForBackupReached() {
|
|
try {
|
|
const directorySize = await getDirectorySize(vcDirectoryPath);
|
|
const freeDiskStorageInBytes =
|
|
isAndroid() && androidVersion < 29
|
|
? getFreeDiskStorageOldSync()
|
|
: getFreeDiskStorageSync();
|
|
|
|
return freeDiskStorageInBytes <= 2 * directorySize;
|
|
} catch (error) {
|
|
console.error('Error in isMinimumLimitForBackupReached:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function isMinimumLimitForBackupRestorationReached() {
|
|
// TODO: Have two checks, one for downloading the ZIP file from the cloud &
|
|
// then by looking at it's metadata to check it's expanded size
|
|
// APIs:
|
|
// 1. CloudStorage.stat(file, context)
|
|
// 2. getUncompressedSize()
|
|
return await Storage.isMinimumLimitReached('minStorageRequired');
|
|
}
|