This commit is contained in:
overtorment 2024-04-15 21:53:44 +01:00
parent bcaec353cb
commit ca6725675d
10 changed files with 84 additions and 70 deletions

View file

@ -1,6 +1,8 @@
import { getUniqueId } from 'react-native-device-info';
import Bugsnag from '@bugsnag/react-native';
import BlueApp from '../BlueApp';
import { BlueApp as BlueAppClass } from '../class';
const BlueApp = BlueAppClass.getInstance();
/**
* in case Bugsnag was started, but user decided to opt out while using the app, we have this

View file

@ -1,11 +1,11 @@
import { Platform } from 'react-native';
import { initCurrencyDaemon } from './blue_modules/currency';
import Biometric from './class/biometrics';
import prompt from './helpers/prompt';
import loc from './loc';
import { AppStorage } from './class/';
import { initCurrencyDaemon } from '../blue_modules/currency';
import Biometric from '../class/biometrics';
import prompt from '../helpers/prompt';
import loc from '../loc';
import { BlueApp as BlueAppClass } from '../class/';
const BlueApp = new AppStorage();
const BlueApp = BlueAppClass.getInstance();
// If attempt reaches 10, a wipe keychain option will be provided to the user.
let unlockAttempt = 0;

View file

@ -1,9 +1,9 @@
import React, { createContext, useEffect, useState } from 'react';
import { useAsyncStorage } from '@react-native-async-storage/async-storage';
import BlueApp, { startAndDecrypt } from '../BlueApp';
import { startAndDecrypt } from './start-and-decrypt';
import Notifications from '../blue_modules/notifications';
import { LegacyWallet, TTXMetadata, WatchOnlyWallet } from '../class';
import { LegacyWallet, TTXMetadata, WatchOnlyWallet, BlueApp as BlueAppClass } from '../class';
import type { TWallet } from '../class/wallets/types';
import presentAlert from '../components/Alert';
import loc, { STORAGE_KEY as LOC_STORAGE_KEY } from '../loc';
@ -13,6 +13,8 @@ import { PREFERRED_CURRENCY_STORAGE_KEY } from './currency';
import triggerHapticFeedback, { HapticFeedbackTypes } from './hapticFeedback';
import A from '../blue_modules/analytics';
const BlueApp = BlueAppClass.getInstance();
// hashmap of timestamps we _started_ refetching some wallet
const _lastTimeTriedToRefetchWallet: { [walletID: string]: number } = {};

View file

@ -43,14 +43,16 @@ type TRealmTransaction = {
const isReactNative = typeof navigator !== 'undefined' && navigator?.product === 'ReactNative';
export class AppStorage {
export class BlueApp {
static FLAG_ENCRYPTED = 'data_encrypted';
static LNDHUB = 'lndhub';
static ADVANCED_MODE_ENABLED = 'advancedmodeenabled';
static DO_NOT_TRACK = 'donottrack';
static HANDOFF_STORAGE_KEY = 'HandOff';
static keys2migrate = [AppStorage.HANDOFF_STORAGE_KEY, AppStorage.DO_NOT_TRACK, AppStorage.ADVANCED_MODE_ENABLED];
private static _instance: BlueApp | null = null;
static keys2migrate = [BlueApp.HANDOFF_STORAGE_KEY, BlueApp.DO_NOT_TRACK, BlueApp.ADVANCED_MODE_ENABLED];
public cachedPassword?: false | string;
public tx_metadata: TTXMetadata;
@ -62,13 +64,21 @@ export class AppStorage {
this.cachedPassword = false;
}
static getInstance(): BlueApp {
if (!BlueApp._instance) {
BlueApp._instance = new BlueApp();
}
return BlueApp._instance;
}
async migrateKeys() {
// do not migrate keys if we are not in RN env
if (!isReactNative) {
return;
}
for (const key of AppStorage.keys2migrate) {
for (const key of BlueApp.keys2migrate) {
try {
const value = await RNSecureKeyStore.get(key);
if (value) {
@ -126,9 +136,9 @@ export class AppStorage {
storageIsEncrypted = async (): Promise<boolean> => {
let data;
try {
data = await this.getItemWithFallbackToRealm(AppStorage.FLAG_ENCRYPTED);
data = await this.getItemWithFallbackToRealm(BlueApp.FLAG_ENCRYPTED);
} catch (error: any) {
console.warn('error reading `' + AppStorage.FLAG_ENCRYPTED + '` key:', error.message);
console.warn('error reading `' + BlueApp.FLAG_ENCRYPTED + '` key:', error.message);
return false;
}
@ -190,7 +200,7 @@ export class AppStorage {
data = JSON.stringify(data);
this.cachedPassword = password;
await this.setItem('data', data);
await this.setItem(AppStorage.FLAG_ENCRYPTED, '1');
await this.setItem(BlueApp.FLAG_ENCRYPTED, '1');
};
/**
@ -405,7 +415,7 @@ export class AppStorage {
unserializedWallet = LightningCustodianWallet.fromJson(key) as unknown as LightningCustodianWallet;
let lndhub: false | any = false;
try {
lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
lndhub = await AsyncStorage.getItem(BlueApp.LNDHUB);
} catch (error) {
console.warn(error);
}
@ -674,12 +684,12 @@ export class AppStorage {
}
await this.setItem('data', JSON.stringify(data));
await this.setItem(AppStorage.FLAG_ENCRYPTED, this.cachedPassword ? '1' : '');
await this.setItem(BlueApp.FLAG_ENCRYPTED, this.cachedPassword ? '1' : '');
// now, backing up same data in realm:
const realmkeyValue = await this.openRealmKeyValue();
this.saveToRealmKeyValue(realmkeyValue, 'data', JSON.stringify(data));
this.saveToRealmKeyValue(realmkeyValue, AppStorage.FLAG_ENCRYPTED, this.cachedPassword ? '1' : '');
this.saveToRealmKeyValue(realmkeyValue, BlueApp.FLAG_ENCRYPTED, this.cachedPassword ? '1' : '');
realmkeyValue.close();
} catch (error: any) {
console.error('save to disk exception:', error.message);
@ -834,50 +844,50 @@ export class AppStorage {
isAdvancedModeEnabled = async (): Promise<boolean> => {
try {
return !!(await AsyncStorage.getItem(AppStorage.ADVANCED_MODE_ENABLED));
return !!(await AsyncStorage.getItem(BlueApp.ADVANCED_MODE_ENABLED));
} catch (_) {}
return false;
};
setIsAdvancedModeEnabled = async (value: boolean) => {
await AsyncStorage.setItem(AppStorage.ADVANCED_MODE_ENABLED, value ? '1' : '');
await AsyncStorage.setItem(BlueApp.ADVANCED_MODE_ENABLED, value ? '1' : '');
};
isHandoffEnabled = async (): Promise<boolean> => {
try {
return !!(await AsyncStorage.getItem(AppStorage.HANDOFF_STORAGE_KEY));
return !!(await AsyncStorage.getItem(BlueApp.HANDOFF_STORAGE_KEY));
} catch (_) {}
return false;
};
setIsHandoffEnabled = async (value: boolean): Promise<void> => {
await AsyncStorage.setItem(AppStorage.HANDOFF_STORAGE_KEY, value ? '1' : '');
await AsyncStorage.setItem(BlueApp.HANDOFF_STORAGE_KEY, value ? '1' : '');
};
isDoNotTrackEnabled = async (): Promise<boolean> => {
try {
const keyExists = await AsyncStorage.getItem(AppStorage.DO_NOT_TRACK);
const keyExists = await AsyncStorage.getItem(BlueApp.DO_NOT_TRACK);
if (keyExists !== null) {
const doNotTrackValue = !!keyExists;
if (doNotTrackValue) {
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
await DefaultPreference.set(AppStorage.DO_NOT_TRACK, '1');
AsyncStorage.removeItem(AppStorage.DO_NOT_TRACK);
await DefaultPreference.set(BlueApp.DO_NOT_TRACK, '1');
AsyncStorage.removeItem(BlueApp.DO_NOT_TRACK);
} else {
return Boolean(await DefaultPreference.get(AppStorage.DO_NOT_TRACK));
return Boolean(await DefaultPreference.get(BlueApp.DO_NOT_TRACK));
}
}
} catch (_) {}
const doNotTrackValue = await DefaultPreference.get(AppStorage.DO_NOT_TRACK);
const doNotTrackValue = await DefaultPreference.get(BlueApp.DO_NOT_TRACK);
return doNotTrackValue === '1' || false;
};
setDoNotTrack = async (value: boolean) => {
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
if (value) {
await DefaultPreference.set(AppStorage.DO_NOT_TRACK, '1');
await DefaultPreference.set(BlueApp.DO_NOT_TRACK, '1');
} else {
await DefaultPreference.clear(AppStorage.DO_NOT_TRACK);
await DefaultPreference.clear(BlueApp.DO_NOT_TRACK);
}
};

View file

@ -5,7 +5,7 @@ import URL from 'url';
import { readFileOutsideSandbox } from '../blue_modules/fs';
import { Chain } from '../models/bitcoinUnits';
import { AppStorage, LightningCustodianWallet, WatchOnlyWallet } from './';
import { BlueApp, LightningCustodianWallet, WatchOnlyWallet } from './';
import Azteco from './azteco';
import Lnurl from './lnurl';
import type { TWallet } from './wallets/types';
@ -231,7 +231,7 @@ class DeeplinkSchemaMatch {
w.setLabel(w.typeReadable);
try {
const lndhub = await AsyncStorage.getItem(AppStorage.LNDHUB);
const lndhub = await AsyncStorage.getItem(BlueApp.LNDHUB);
if (lndhub) {
w.setBaseURI(lndhub);
w.init();

View file

@ -18,4 +18,4 @@ export * from './wallets/multisig-hd-wallet';
export * from './wallets/slip39-wallets';
export * from './hd-segwit-bech32-transaction';
export * from './multisig-cosigner';
export * from './app-storage';
export * from './blue-app';

View file

@ -12,7 +12,7 @@ import DeeplinkSchemaMatch from '../../class/deeplink-schema-match';
import presentAlert from '../../components/Alert';
import { requestCameraAuthorization } from '../../helpers/scan-qr';
import { Button } from '../../components/Button';
import { AppStorage } from '../../class';
import { BlueApp } from '../../class';
const styles = StyleSheet.create({
uri: {
@ -62,7 +62,7 @@ const LightningSettings: React.FC & { navigationOptions: NavigationOptionsGetter
});
useEffect(() => {
AsyncStorage.getItem(AppStorage.LNDHUB)
AsyncStorage.getItem(BlueApp.LNDHUB)
.then(value => setURI(value ?? undefined))
.then(() => setIsLoading(false))
.catch(() => setIsLoading(false));
@ -101,9 +101,9 @@ const LightningSettings: React.FC & { navigationOptions: NavigationOptionsGetter
// validating only if its not empty. empty means use default
}
if (URI) {
await AsyncStorage.setItem(AppStorage.LNDHUB, URI);
await AsyncStorage.setItem(BlueApp.LNDHUB, URI);
} else {
await AsyncStorage.removeItem(AppStorage.LNDHUB);
await AsyncStorage.removeItem(BlueApp.LNDHUB);
}
presentAlert({ message: loc.settings.lightning_saved });
} catch (error) {

View file

@ -19,7 +19,7 @@ import { BlueButtonLink, BlueFormLabel, BlueSpacing20, BlueSpacing40, BlueText }
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { BlueStorageContext } from '../../blue_modules/storage-context';
import {
AppStorage,
BlueApp,
HDSegwitBech32Wallet,
HDSegwitP2SHWallet,
LightningCustodianWallet,
@ -149,7 +149,7 @@ const WalletsAdd: React.FC = () => {
};
useEffect(() => {
AsyncStorage.getItem(AppStorage.LNDHUB)
AsyncStorage.getItem(BlueApp.LNDHUB)
.then(url => (url ? setWalletBaseURI(url) : setWalletBaseURI('')))
.catch(() => setWalletBaseURI(''))
.finally(() => setIsLoading(false));

View file

@ -3,7 +3,7 @@ const path = require('path');
const mainLocFile = './loc/en.json';
const dirsToInterate = ['components', 'screen', 'blue_modules', 'class', 'hooks', 'helpers'];
const addFiles = ['BlueComponents.js', 'App.js', 'BlueApp.ts', 'Navigation.tsx'];
const addFiles = ['BlueComponents.js', 'App.js', 'Navigation.tsx'];
const allowedLocPrefixes = ['loc.lnurl_auth', 'loc.units'];
const allLocKeysHashmap = {}; // loc key -> used or not

View file

@ -1,6 +1,6 @@
import assert from 'assert';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { SegwitP2SHWallet, AppStorage } from '../../class';
import { SegwitP2SHWallet, BlueApp } from '../../class';
jest.mock('../../blue_modules/BlueElectrum', () => {
return {
@ -9,8 +9,8 @@ jest.mock('../../blue_modules/BlueElectrum', () => {
});
it('Appstorage - loadFromDisk works', async () => {
/** @type {AppStorage} */
const Storage = new AppStorage();
/** @type {BlueApp} */
const Storage = new BlueApp();
const w = new SegwitP2SHWallet();
w.setLabel('testlabel');
await w.generate();
@ -19,7 +19,7 @@ it('Appstorage - loadFromDisk works', async () => {
// saved, now trying to load
const Storage2 = new AppStorage();
const Storage2 = new BlueApp();
await Storage2.loadFromDisk();
assert.strictEqual(Storage2.wallets.length, 1);
assert.strictEqual(Storage2.wallets[0].getLabel(), 'testlabel');
@ -29,15 +29,15 @@ it('Appstorage - loadFromDisk works', async () => {
// emulating encrypted storage (and testing flag)
await AsyncStorage.setItem('data', false);
await AsyncStorage.setItem(AppStorage.FLAG_ENCRYPTED, '1');
const Storage3 = new AppStorage();
await AsyncStorage.setItem(BlueApp.FLAG_ENCRYPTED, '1');
const Storage3 = new BlueApp();
isEncrypted = await Storage3.storageIsEncrypted();
assert.ok(isEncrypted);
});
it('Appstorage - encryptStorage & load encrypted storage works', async () => {
/** @type {AppStorage} */
const Storage = new AppStorage();
/** @type {BlueApp} */
const Storage = new BlueApp();
let w = new SegwitP2SHWallet();
w.setLabel('testlabel');
await w.generate();
@ -52,7 +52,7 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
// saved, now trying to load, using good password
let Storage2 = new AppStorage();
let Storage2 = new BlueApp();
isEncrypted = await Storage2.storageIsEncrypted();
assert.ok(isEncrypted);
let loadResult = await Storage2.loadFromDisk('password');
@ -62,7 +62,7 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
// now trying to load, using bad password
Storage2 = new AppStorage();
Storage2 = new BlueApp();
isEncrypted = await Storage2.storageIsEncrypted();
assert.ok(isEncrypted);
loadResult = await Storage2.loadFromDisk('passwordBAD');
@ -72,7 +72,7 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
// now, trying case with adding data after decrypt.
// saveToDisk should be handled correctly
Storage2 = new AppStorage();
Storage2 = new BlueApp();
isEncrypted = await Storage2.storageIsEncrypted();
assert.ok(isEncrypted);
loadResult = await Storage2.loadFromDisk('password');
@ -87,7 +87,7 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
assert.strictEqual(Storage2.wallets[1].getLabel(), 'testlabel2');
await Storage2.saveToDisk();
// saved to encrypted storage after load. next load should be successfull
Storage2 = new AppStorage();
Storage2 = new BlueApp();
isEncrypted = await Storage2.storageIsEncrypted();
assert.ok(isEncrypted);
loadResult = await Storage2.loadFromDisk('password');
@ -108,13 +108,13 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
await Storage2.saveToDisk();
// now, will try to load & decrypt with real password and with fake password
// real:
let Storage3 = new AppStorage();
let Storage3 = new BlueApp();
loadResult = await Storage3.loadFromDisk('password');
assert.ok(loadResult);
assert.strictEqual(Storage3.wallets.length, 2);
assert.strictEqual(Storage3.wallets[0].getLabel(), 'testlabel');
// fake:
Storage3 = new AppStorage();
Storage3 = new BlueApp();
loadResult = await Storage3.loadFromDisk('fakePassword');
assert.ok(loadResult);
assert.strictEqual(Storage3.wallets.length, 1);
@ -122,8 +122,8 @@ it('Appstorage - encryptStorage & load encrypted storage works', async () => {
});
it('Appstorage - encryptStorage & load encrypted, then decryptStorage and load storage works', async () => {
/** @type {AppStorage} */
const Storage = new AppStorage();
/** @type {BlueApp} */
const Storage = new BlueApp();
let w = new SegwitP2SHWallet();
w.setLabel('testlabel');
await w.generate();
@ -138,7 +138,7 @@ it('Appstorage - encryptStorage & load encrypted, then decryptStorage and load s
// saved, now trying to load, using good password
let Storage2 = new AppStorage();
let Storage2 = new BlueApp();
isEncrypted = await Storage2.storageIsEncrypted();
assert.ok(isEncrypted);
let loadResult = await Storage2.loadFromDisk('password');
@ -148,7 +148,7 @@ it('Appstorage - encryptStorage & load encrypted, then decryptStorage and load s
// now trying to load, using bad password
Storage2 = new AppStorage();
Storage2 = new BlueApp();
isEncrypted = await Storage2.storageIsEncrypted();
assert.ok(isEncrypted);
loadResult = await Storage2.loadFromDisk('passwordBAD');
@ -158,7 +158,7 @@ it('Appstorage - encryptStorage & load encrypted, then decryptStorage and load s
// now, trying case with adding data after decrypt.
// saveToDisk should be handled correctly
Storage2 = new AppStorage();
Storage2 = new BlueApp();
isEncrypted = await Storage2.storageIsEncrypted();
assert.ok(isEncrypted);
loadResult = await Storage2.loadFromDisk('password');
@ -173,7 +173,7 @@ it('Appstorage - encryptStorage & load encrypted, then decryptStorage and load s
assert.strictEqual(Storage2.wallets[1].getLabel(), 'testlabel2');
await Storage2.saveToDisk();
// saved to encrypted storage after load. next load should be successfull
Storage2 = new AppStorage();
Storage2 = new BlueApp();
isEncrypted = await Storage2.storageIsEncrypted();
assert.ok(isEncrypted);
loadResult = await Storage2.loadFromDisk('password');
@ -194,13 +194,13 @@ it('Appstorage - encryptStorage & load encrypted, then decryptStorage and load s
await Storage2.saveToDisk();
// now, will try to load & decrypt with real password and with fake password
// real:
let Storage3 = new AppStorage();
let Storage3 = new BlueApp();
loadResult = await Storage3.loadFromDisk('password');
assert.ok(loadResult);
assert.strictEqual(Storage3.wallets.length, 2);
assert.strictEqual(Storage3.wallets[0].getLabel(), 'testlabel');
// fake:
Storage3 = new AppStorage();
Storage3 = new BlueApp();
loadResult = await Storage3.loadFromDisk('fakePassword');
assert.ok(loadResult);
assert.strictEqual(Storage3.wallets.length, 1);
@ -208,7 +208,7 @@ it('Appstorage - encryptStorage & load encrypted, then decryptStorage and load s
// now will decrypt storage. label of wallet should be testlabel
const Storage4 = new AppStorage();
const Storage4 = new BlueApp();
isEncrypted = await Storage4.storageIsEncrypted();
assert.ok(isEncrypted);
loadResult = await Storage4.loadFromDisk('password');
@ -216,7 +216,7 @@ it('Appstorage - encryptStorage & load encrypted, then decryptStorage and load s
const decryptStorageResult = await Storage4.decryptStorage('password');
assert.ok(decryptStorageResult);
const Storage5 = new AppStorage();
const Storage5 = new BlueApp();
isEncrypted = await Storage5.storageIsEncrypted();
assert.strictEqual(isEncrypted, false);
const storage5loadResult = await Storage5.loadFromDisk();
@ -227,8 +227,8 @@ it('Appstorage - encryptStorage & load encrypted, then decryptStorage and load s
});
it('can decrypt storage that is second in a list of buckets; and isPasswordInUse() works', async () => {
/** @type {AppStorage} */
const Storage = new AppStorage();
/** @type {BlueApp} */
const Storage = new BlueApp();
let w = new SegwitP2SHWallet();
w.setLabel('testlabel');
await w.generate();
@ -255,7 +255,7 @@ it('can decrypt storage that is second in a list of buckets; and isPasswordInUse
// now will decrypt storage. will try to decrypt FAKE storage (second in the list) while
// currently decrypted is the MAIN (non-fake) storage. this should throw an exception
const Storage4 = new AppStorage();
const Storage4 = new BlueApp();
isEncrypted = await Storage4.storageIsEncrypted();
assert.ok(isEncrypted);
let loadResult = await Storage4.loadFromDisk('password');
@ -274,7 +274,7 @@ it('can decrypt storage that is second in a list of buckets; and isPasswordInUse
// storage, purging other buckets. this should be possible since if user wants to shoot himsel in the foot
// he should be able to do it.
const Storage5 = new AppStorage();
const Storage5 = new BlueApp();
isEncrypted = await Storage5.storageIsEncrypted();
assert.ok(isEncrypted);
loadResult = await Storage5.loadFromDisk('fakePassword');
@ -287,7 +287,7 @@ it('can decrypt storage that is second in a list of buckets; and isPasswordInUse
// now we will decrypt storage. label of wallet should be testlabel
const Storage6 = new AppStorage();
const Storage6 = new BlueApp();
isEncrypted = await Storage6.storageIsEncrypted();
assert.ok(isEncrypted);
loadResult = await Storage6.loadFromDisk('fakePassword');
@ -295,7 +295,7 @@ it('can decrypt storage that is second in a list of buckets; and isPasswordInUse
const decryptStorageResult = await Storage6.decryptStorage('fakePassword');
assert.ok(decryptStorageResult);
const Storage7 = new AppStorage();
const Storage7 = new BlueApp();
isEncrypted = await Storage7.storageIsEncrypted();
assert.strictEqual(isEncrypted, false);
const storage5loadResult = await Storage7.loadFromDisk();
@ -304,6 +304,6 @@ it('can decrypt storage that is second in a list of buckets; and isPasswordInUse
});
it('Appstorage - hashIt() works', async () => {
const storage = new AppStorage();
const storage = new BlueApp();
assert.strictEqual(storage.hashIt('hello'), '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
});