mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 09:50:15 +01:00
REF: storage improvements
This commit is contained in:
parent
ba098e822f
commit
f00c5b4488
@ -29,7 +29,7 @@ const startAndDecrypt = async retry => {
|
||||
} catch (error) {
|
||||
// in case of exception reading from keystore, lets retry instead of assuming there is no storage and
|
||||
// proceeding with no wallets
|
||||
console.warn(error);
|
||||
console.warn('exception loading from disk:', error);
|
||||
wasException = true;
|
||||
}
|
||||
|
||||
@ -38,7 +38,9 @@ const startAndDecrypt = async retry => {
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 3000)); // sleep
|
||||
success = await BlueApp.loadFromDisk(password);
|
||||
} catch (_) {}
|
||||
} catch (error) {
|
||||
console.warn('second exception loading from disk:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* global alert */
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
|
||||
import * as Keychain from 'react-native-keychain';
|
||||
import {
|
||||
HDLegacyBreadwalletWallet,
|
||||
HDSegwitP2SHWallet,
|
||||
@ -20,10 +21,12 @@ import {
|
||||
SLIP39LegacyP2PKHWallet,
|
||||
SLIP39SegwitBech32Wallet,
|
||||
} from './';
|
||||
import { randomBytes } from './rng';
|
||||
const encryption = require('../blue_modules/encryption');
|
||||
const Realm = require('realm');
|
||||
const createHash = require('create-hash');
|
||||
let usedBucketNum = false;
|
||||
let savingInProgress = 0; // its both a flag and a counter of attempts to write to disk
|
||||
|
||||
export class AppStorage {
|
||||
static FLAG_ENCRYPTED = 'data_encrypted';
|
||||
@ -88,11 +91,36 @@ export class AppStorage {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @param key {string}
|
||||
* @returns {Promise<*>|null}
|
||||
*/
|
||||
getItemWithFallbackToRealm = async key => {
|
||||
let value;
|
||||
try {
|
||||
return await this.getItem(key);
|
||||
} catch (error) {
|
||||
console.warn('error reading', key, error.message);
|
||||
console.warn('fallback to realm');
|
||||
const realmKeyValue = await this.openRealmKeyValue();
|
||||
const obj = realmKeyValue.objectForPrimaryKey('KeyValue', key); // search for a realm object with a primary key
|
||||
value = obj?.value;
|
||||
realmKeyValue.close();
|
||||
if (value) {
|
||||
console.warn('successfully recovered', value.length, 'bytes from realm for key', key);
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
storageIsEncrypted = async () => {
|
||||
let data;
|
||||
try {
|
||||
data = await this.getItem(AppStorage.FLAG_ENCRYPTED);
|
||||
data = await this.getItemWithFallbackToRealm(AppStorage.FLAG_ENCRYPTED);
|
||||
} catch (error) {
|
||||
console.warn('error reading `' + AppStorage.FLAG_ENCRYPTED + '` key:', error.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -220,6 +248,58 @@ export class AppStorage {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns instace of the Realm database, which is encrypted by device unique id
|
||||
* Database file is static.
|
||||
*
|
||||
* @returns {Promise<Realm>}
|
||||
*/
|
||||
async openRealmKeyValue() {
|
||||
const service = 'realm_encryption_key';
|
||||
let password;
|
||||
const credentials = await Keychain.getGenericPassword({ service });
|
||||
if (credentials) {
|
||||
password = credentials.password;
|
||||
} else {
|
||||
const buf = await randomBytes(64);
|
||||
password = buf.toString('hex');
|
||||
await Keychain.setGenericPassword(service, password, { service });
|
||||
}
|
||||
|
||||
const buf = Buffer.from(password, 'hex');
|
||||
const encryptionKey = Int8Array.from(buf);
|
||||
const path = 'keyvalue.realm';
|
||||
|
||||
const schema = [
|
||||
{
|
||||
name: 'KeyValue',
|
||||
primaryKey: 'key',
|
||||
properties: {
|
||||
key: { type: 'string', indexed: true },
|
||||
value: 'string', // stringified json, or whatever
|
||||
},
|
||||
},
|
||||
];
|
||||
return Realm.open({
|
||||
schema,
|
||||
path,
|
||||
encryptionKey,
|
||||
});
|
||||
}
|
||||
|
||||
saveToRealmKeyValue(realmkeyValue, key, value) {
|
||||
realmkeyValue.write(() => {
|
||||
realmkeyValue.create(
|
||||
'KeyValue',
|
||||
{
|
||||
key: key,
|
||||
value: value,
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads from storage all wallets and
|
||||
* maps them to `this.wallets`
|
||||
@ -228,7 +308,7 @@ export class AppStorage {
|
||||
* @returns {Promise.<boolean>}
|
||||
*/
|
||||
async loadFromDisk(password) {
|
||||
let data = await this.getItem('data');
|
||||
let data = await this.getItemWithFallbackToRealm('data');
|
||||
if (password) {
|
||||
data = this.decryptData(data, password);
|
||||
if (data) {
|
||||
@ -414,6 +494,15 @@ export class AppStorage {
|
||||
* @returns {Promise} Result of storage save
|
||||
*/
|
||||
async saveToDisk() {
|
||||
if (savingInProgress) {
|
||||
console.warn('saveToDisk is in progress');
|
||||
if (++savingInProgress > 10) alert('Critical error. Last actions were not saved'); // should never happen
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * savingInProgress)); // sleep
|
||||
return this.saveToDisk();
|
||||
}
|
||||
savingInProgress = 1;
|
||||
|
||||
try {
|
||||
const walletsToSave = [];
|
||||
const realm = await this.getRealm();
|
||||
for (const key of this.wallets) {
|
||||
@ -442,7 +531,7 @@ export class AppStorage {
|
||||
|
||||
if (this.cachedPassword) {
|
||||
// should find the correct bucket, encrypt and then save
|
||||
let buckets = await this.getItem('data');
|
||||
let buckets = await this.getItemWithFallbackToRealm('data');
|
||||
buckets = JSON.parse(buckets);
|
||||
const newData = [];
|
||||
let num = 0;
|
||||
@ -468,17 +557,24 @@ export class AppStorage {
|
||||
// decrypted ok, this is our bucket
|
||||
// we serialize our object's data, encrypt it, and add it to buckets
|
||||
newData.push(encryption.encrypt(JSON.stringify(data), this.cachedPassword));
|
||||
await this.setItem(AppStorage.FLAG_ENCRYPTED, '1');
|
||||
}
|
||||
}
|
||||
data = newData;
|
||||
} else {
|
||||
await this.setItem(AppStorage.FLAG_ENCRYPTED, ''); // drop the flag
|
||||
}
|
||||
try {
|
||||
return await this.setItem('data', JSON.stringify(data));
|
||||
|
||||
await this.setItem('data', JSON.stringify(data));
|
||||
await this.setItem(AppStorage.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' : '');
|
||||
realmkeyValue.close();
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
console.error('save to disk exception:', error.message);
|
||||
alert('save to disk exception: ' + error.message);
|
||||
} finally {
|
||||
savingInProgress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
5
package-lock.json
generated
5
package-lock.json
generated
@ -17669,6 +17669,11 @@
|
||||
"version": "git+https://github.com/BlueWallet/react-native-is-catalyst.git#a665155424ae5b865971f71ba2625b2c53983364",
|
||||
"from": "git+https://github.com/BlueWallet/react-native-is-catalyst.git#v1.0.0"
|
||||
},
|
||||
"react-native-keychain": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-7.0.0.tgz",
|
||||
"integrity": "sha512-tH26sgW4OxB/llXmhO+DajFISEUoF1Ip2+WSDMIgCt8SP1xRE81m2qFzgIOc/7StYsUERxHhDPkxvq2H0/Goig=="
|
||||
},
|
||||
"react-native-level-fs": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-level-fs/-/react-native-level-fs-3.0.1.tgz",
|
||||
|
@ -140,6 +140,7 @@
|
||||
"react-native-image-picker": "3.5.0",
|
||||
"react-native-inappbrowser-reborn": "https://github.com/BlueWallet/react-native-inappbrowser#fa2d8e1763e46dd12a7e53081e97a0f908049103",
|
||||
"react-native-is-catalyst": "https://github.com/BlueWallet/react-native-is-catalyst#v1.0.0",
|
||||
"react-native-keychain": "7.0.0",
|
||||
"react-native-level-fs": "3.0.1",
|
||||
"react-native-linear-gradient": "2.5.6",
|
||||
"react-native-localize": "2.0.3",
|
||||
|
@ -22,6 +22,7 @@ jest.mock('@sentry/react-native', () => {
|
||||
|
||||
jest.mock('react-native-device-info', () => {
|
||||
return {
|
||||
getUniqueId: jest.fn().mockReturnValue('uniqueId'),
|
||||
getSystemName: jest.fn(),
|
||||
hasGmsSync: jest.fn().mockReturnValue(true),
|
||||
hasHmsSync: jest.fn().mockReturnValue(false),
|
||||
|
Loading…
Reference in New Issue
Block a user