Files
inji-wallet/shared/storage.ts
KiruthikaJeyashankar 003cc156c2 [INJIMOB-1192] onboarding of new issuer is affecting the existing issuers (#1476)
* [INJIMOB-1192] : use wellknown response instead of mimoto issuer config.
-- Remove hardcoding for sunbird issuer in vc activation and verification flow.
-- Render idType from wellknown response
-- Remove UIN/VID from default add-on fields

Signed-off-by: Swati Goel <meet2swati@gmail.com>

* [INJIMOB-1192] : fix propType and some refactoring

Signed-off-by: Swati Goel <meet2swati@gmail.com>

* [INJIMOB-1192] : add credentialType in VcMetadata

Signed-off-by: Swati Goel <meet2swati@gmail.com>

* [INJIMOB-1192] fix vc download via issuer flow due to credentialType mismatch

Co-authored-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com>
Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

* [INJIMOB-1192] rename supported list of credential type in issuers model

Co-authored-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com>
Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

* [INJIMOB-1192] display id type in history based on wellknown for issuers VC

Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

* [INJIMOB-1192] fix id type not shown for VC activation

Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

* [INJIMOB-1192] remove unused credentialType field from VCMetaData

Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

* [INJIMOB-1192] set default idType for logging activity

Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

* [INJIMOB-1192] move vc item machine events into model

Events should not be exported to other packages for direct use so that Xstate's createModel() can decorate the function appropriately

Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

* [INJIMOB-1192] show verify banner id type from wellknown

Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

* [INJIMOB-1192] refactor duplication and unused code

Co-authored-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com>
Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

* [INJIMOB-1192] remove unused displayId in metadata

Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

* [INJIMOB-1192] revert the dimensions of camera scanner to old values to support face liveness verification

Co-authored-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>
Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com>

* [INJIMOB-1192] remove unused code & debug logs

Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

* [INJIMOB-1192] fix failing test cases

Co-authored-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>
Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com>

* [INJIMOB-1192] remove unused translations

Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>

---------

Signed-off-by: Swati Goel <meet2swati@gmail.com>
Signed-off-by: KiruthikaJeyashankar <81218987+KiruthikaJeyashankar@users.noreply.github.com>
Signed-off-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com>
Co-authored-by: Swati Goel <meet2swati@gmail.com>
Co-authored-by: PuBHARGAVI <46226958+PuBHARGAVI@users.noreply.github.com>
2024-06-04 16:37:54 +05:30

540 lines
17 KiB
TypeScript

import {MMKVLoader} from 'react-native-mmkv-storage';
import getAllConfigurations from './api';
import {
getFreeDiskStorageOldSync,
getFreeDiskStorageSync,
} from 'react-native-device-info';
import {NativeModules} from 'react-native';
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,
API_CACHED_STORAGE_KEYS,
} 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();
const {RNSecureKeystoreModule} = NativeModules;
async function generateHmac(
encryptionKey: string,
data: string,
): Promise<string> {
if (!isHardwareKeystoreExists) {
return hmacSHA(encryptionKey, data);
}
return await RNSecureKeystoreModule.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) {
console.error('Error while loading backup data ', error);
return error;
}
};
static fetchAllWellknownConfig = async (encryptionKey: string) => {
let wellknownConfigData: Record<string, Object> = {};
const allKeysInDB = await MMKV.indexer.strings.getKeys();
const wellknownConfigCacheKey =
API_CACHED_STORAGE_KEYS.fetchIssuerWellknownConfig('');
const wellknownKeys = allKeysInDB.filter(key =>
key.includes(wellknownConfigCacheKey),
);
for (const wellknownKey of wellknownKeys) {
const configData = await this.getItem(wellknownKey, encryptionKey);
const decryptedConfigData = await decryptJson(encryptionKey, configData);
wellknownConfigData[
wellknownKey.substring(wellknownConfigCacheKey.length)
] = JSON.parse(JSON.stringify(decryptedConfigData));
}
return wellknownConfigData;
};
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),
);
// 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');
}