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) {
|
} catch (error) {
|
||||||
// in case of exception reading from keystore, lets retry instead of assuming there is no storage and
|
// in case of exception reading from keystore, lets retry instead of assuming there is no storage and
|
||||||
// proceeding with no wallets
|
// proceeding with no wallets
|
||||||
console.warn(error);
|
console.warn('exception loading from disk:', error);
|
||||||
wasException = true;
|
wasException = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +38,9 @@ const startAndDecrypt = async retry => {
|
|||||||
try {
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 3000)); // sleep
|
await new Promise(resolve => setTimeout(resolve, 3000)); // sleep
|
||||||
success = await BlueApp.loadFromDisk(password);
|
success = await BlueApp.loadFromDisk(password);
|
||||||
} catch (_) {}
|
} catch (error) {
|
||||||
|
console.warn('second exception loading from disk:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* global alert */
|
/* global alert */
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
|
import RNSecureKeyStore, { ACCESSIBLE } from 'react-native-secure-key-store';
|
||||||
|
import * as Keychain from 'react-native-keychain';
|
||||||
import {
|
import {
|
||||||
HDLegacyBreadwalletWallet,
|
HDLegacyBreadwalletWallet,
|
||||||
HDSegwitP2SHWallet,
|
HDSegwitP2SHWallet,
|
||||||
@ -20,10 +21,12 @@ import {
|
|||||||
SLIP39LegacyP2PKHWallet,
|
SLIP39LegacyP2PKHWallet,
|
||||||
SLIP39SegwitBech32Wallet,
|
SLIP39SegwitBech32Wallet,
|
||||||
} from './';
|
} from './';
|
||||||
|
import { randomBytes } from './rng';
|
||||||
const encryption = require('../blue_modules/encryption');
|
const encryption = require('../blue_modules/encryption');
|
||||||
const Realm = require('realm');
|
const Realm = require('realm');
|
||||||
const createHash = require('create-hash');
|
const createHash = require('create-hash');
|
||||||
let usedBucketNum = false;
|
let usedBucketNum = false;
|
||||||
|
let savingInProgress = 0; // its both a flag and a counter of attempts to write to disk
|
||||||
|
|
||||||
export class AppStorage {
|
export class AppStorage {
|
||||||
static FLAG_ENCRYPTED = 'data_encrypted';
|
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 () => {
|
storageIsEncrypted = async () => {
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await this.getItem(AppStorage.FLAG_ENCRYPTED);
|
data = await this.getItemWithFallbackToRealm(AppStorage.FLAG_ENCRYPTED);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.warn('error reading `' + AppStorage.FLAG_ENCRYPTED + '` key:', error.message);
|
||||||
return false;
|
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
|
* Loads from storage all wallets and
|
||||||
* maps them to `this.wallets`
|
* maps them to `this.wallets`
|
||||||
@ -228,7 +308,7 @@ export class AppStorage {
|
|||||||
* @returns {Promise.<boolean>}
|
* @returns {Promise.<boolean>}
|
||||||
*/
|
*/
|
||||||
async loadFromDisk(password) {
|
async loadFromDisk(password) {
|
||||||
let data = await this.getItem('data');
|
let data = await this.getItemWithFallbackToRealm('data');
|
||||||
if (password) {
|
if (password) {
|
||||||
data = this.decryptData(data, password);
|
data = this.decryptData(data, password);
|
||||||
if (data) {
|
if (data) {
|
||||||
@ -414,71 +494,87 @@ export class AppStorage {
|
|||||||
* @returns {Promise} Result of storage save
|
* @returns {Promise} Result of storage save
|
||||||
*/
|
*/
|
||||||
async saveToDisk() {
|
async saveToDisk() {
|
||||||
const walletsToSave = [];
|
if (savingInProgress) {
|
||||||
const realm = await this.getRealm();
|
console.warn('saveToDisk is in progress');
|
||||||
for (const key of this.wallets) {
|
if (++savingInProgress > 10) alert('Critical error. Last actions were not saved'); // should never happen
|
||||||
if (typeof key === 'boolean' || key.type === PlaceholderWallet.type) continue;
|
await new Promise(resolve => setTimeout(resolve, 1000 * savingInProgress)); // sleep
|
||||||
key.prepareForSerialization();
|
return this.saveToDisk();
|
||||||
delete key.current;
|
|
||||||
const keyCloned = Object.assign({}, key); // stripped-down version of a wallet to save to secure keystore
|
|
||||||
if (key._hdWalletInstance) keyCloned._hdWalletInstance = Object.assign({}, key._hdWalletInstance);
|
|
||||||
this.offloadWalletToRealm(realm, key);
|
|
||||||
// stripping down:
|
|
||||||
if (key._txs_by_external_index) {
|
|
||||||
keyCloned._txs_by_external_index = {};
|
|
||||||
keyCloned._txs_by_internal_index = {};
|
|
||||||
}
|
|
||||||
if (key._hdWalletInstance) {
|
|
||||||
keyCloned._hdWalletInstance._txs_by_external_index = {};
|
|
||||||
keyCloned._hdWalletInstance._txs_by_internal_index = {};
|
|
||||||
}
|
|
||||||
walletsToSave.push(JSON.stringify({ ...keyCloned, type: keyCloned.type }));
|
|
||||||
}
|
}
|
||||||
realm.close();
|
savingInProgress = 1;
|
||||||
let data = {
|
|
||||||
wallets: walletsToSave,
|
|
||||||
tx_metadata: this.tx_metadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.cachedPassword) {
|
|
||||||
// should find the correct bucket, encrypt and then save
|
|
||||||
let buckets = await this.getItem('data');
|
|
||||||
buckets = JSON.parse(buckets);
|
|
||||||
const newData = [];
|
|
||||||
let num = 0;
|
|
||||||
for (const bucket of buckets) {
|
|
||||||
let decrypted;
|
|
||||||
// if we had `usedBucketNum` during loadFromDisk(), no point to try to decode each bucket to find the one we
|
|
||||||
// need, we just to find bucket with the same index
|
|
||||||
if (usedBucketNum !== false) {
|
|
||||||
if (num === usedBucketNum) {
|
|
||||||
decrypted = true;
|
|
||||||
}
|
|
||||||
num++;
|
|
||||||
} else {
|
|
||||||
// we dont have `usedBucketNum` for whatever reason, so lets try to decrypt each bucket after bucket
|
|
||||||
// till we find the right one
|
|
||||||
decrypted = encryption.decrypt(bucket, this.cachedPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!decrypted) {
|
|
||||||
// no luck decrypting, its not our bucket
|
|
||||||
newData.push(bucket);
|
|
||||||
} else {
|
|
||||||
// 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 {
|
try {
|
||||||
return await this.setItem('data', JSON.stringify(data));
|
const walletsToSave = [];
|
||||||
|
const realm = await this.getRealm();
|
||||||
|
for (const key of this.wallets) {
|
||||||
|
if (typeof key === 'boolean' || key.type === PlaceholderWallet.type) continue;
|
||||||
|
key.prepareForSerialization();
|
||||||
|
delete key.current;
|
||||||
|
const keyCloned = Object.assign({}, key); // stripped-down version of a wallet to save to secure keystore
|
||||||
|
if (key._hdWalletInstance) keyCloned._hdWalletInstance = Object.assign({}, key._hdWalletInstance);
|
||||||
|
this.offloadWalletToRealm(realm, key);
|
||||||
|
// stripping down:
|
||||||
|
if (key._txs_by_external_index) {
|
||||||
|
keyCloned._txs_by_external_index = {};
|
||||||
|
keyCloned._txs_by_internal_index = {};
|
||||||
|
}
|
||||||
|
if (key._hdWalletInstance) {
|
||||||
|
keyCloned._hdWalletInstance._txs_by_external_index = {};
|
||||||
|
keyCloned._hdWalletInstance._txs_by_internal_index = {};
|
||||||
|
}
|
||||||
|
walletsToSave.push(JSON.stringify({ ...keyCloned, type: keyCloned.type }));
|
||||||
|
}
|
||||||
|
realm.close();
|
||||||
|
let data = {
|
||||||
|
wallets: walletsToSave,
|
||||||
|
tx_metadata: this.tx_metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.cachedPassword) {
|
||||||
|
// should find the correct bucket, encrypt and then save
|
||||||
|
let buckets = await this.getItemWithFallbackToRealm('data');
|
||||||
|
buckets = JSON.parse(buckets);
|
||||||
|
const newData = [];
|
||||||
|
let num = 0;
|
||||||
|
for (const bucket of buckets) {
|
||||||
|
let decrypted;
|
||||||
|
// if we had `usedBucketNum` during loadFromDisk(), no point to try to decode each bucket to find the one we
|
||||||
|
// need, we just to find bucket with the same index
|
||||||
|
if (usedBucketNum !== false) {
|
||||||
|
if (num === usedBucketNum) {
|
||||||
|
decrypted = true;
|
||||||
|
}
|
||||||
|
num++;
|
||||||
|
} else {
|
||||||
|
// we dont have `usedBucketNum` for whatever reason, so lets try to decrypt each bucket after bucket
|
||||||
|
// till we find the right one
|
||||||
|
decrypted = encryption.decrypt(bucket, this.cachedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decrypted) {
|
||||||
|
// no luck decrypting, its not our bucket
|
||||||
|
newData.push(bucket);
|
||||||
|
} else {
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
} 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",
|
"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"
|
"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": {
|
"react-native-level-fs": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-level-fs/-/react-native-level-fs-3.0.1.tgz",
|
"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-image-picker": "3.5.0",
|
||||||
"react-native-inappbrowser-reborn": "https://github.com/BlueWallet/react-native-inappbrowser#fa2d8e1763e46dd12a7e53081e97a0f908049103",
|
"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-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-level-fs": "3.0.1",
|
||||||
"react-native-linear-gradient": "2.5.6",
|
"react-native-linear-gradient": "2.5.6",
|
||||||
"react-native-localize": "2.0.3",
|
"react-native-localize": "2.0.3",
|
||||||
|
@ -22,6 +22,7 @@ jest.mock('@sentry/react-native', () => {
|
|||||||
|
|
||||||
jest.mock('react-native-device-info', () => {
|
jest.mock('react-native-device-info', () => {
|
||||||
return {
|
return {
|
||||||
|
getUniqueId: jest.fn().mockReturnValue('uniqueId'),
|
||||||
getSystemName: jest.fn(),
|
getSystemName: jest.fn(),
|
||||||
hasGmsSync: jest.fn().mockReturnValue(true),
|
hasGmsSync: jest.fn().mockReturnValue(true),
|
||||||
hasHmsSync: jest.fn().mockReturnValue(false),
|
hasHmsSync: jest.fn().mockReturnValue(false),
|
||||||
|
Loading…
Reference in New Issue
Block a user