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 { 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 = {}; 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 => { 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'); }