mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2024-11-19 01:40:12 +01:00
161 lines
5.4 KiB
TypeScript
161 lines
5.4 KiB
TypeScript
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import AES from 'crypto-js/aes';
|
|
import ENCHEX from 'crypto-js/enc-hex';
|
|
import ENCUTF8 from 'crypto-js/enc-utf8';
|
|
import SHA256 from 'crypto-js/sha256';
|
|
|
|
export default class SyncedAsyncStorage {
|
|
defaultBaseUrl = 'https://bytes-store.herokuapp.com';
|
|
encryptionMarker = 'encrypted://';
|
|
|
|
namespace: string = '';
|
|
encryptionKey: string = '';
|
|
|
|
constructor(entropy: string) {
|
|
if (!entropy) throw new Error('entropy not provided');
|
|
|
|
this.namespace = this.hashIt(this.hashIt('namespace' + entropy));
|
|
this.encryptionKey = this.hashIt(this.hashIt('encryption' + entropy));
|
|
}
|
|
|
|
hashIt(arg: string) {
|
|
return ENCHEX.stringify(SHA256(arg));
|
|
}
|
|
|
|
encrypt(clearData: string): string {
|
|
return this.encryptionMarker + AES.encrypt(clearData, this.encryptionKey).toString();
|
|
}
|
|
|
|
decrypt(encryptedData: string | null, encryptionKey: string | null = null): string {
|
|
if (encryptedData === null) return '';
|
|
if (!encryptedData.startsWith(this.encryptionMarker)) return encryptedData;
|
|
const bytes = AES.decrypt(encryptedData.replace(this.encryptionMarker, ''), encryptionKey || this.encryptionKey);
|
|
return bytes.toString(ENCUTF8);
|
|
}
|
|
|
|
static assertEquals(a: any, b: any) {
|
|
if (a !== b) throw new Error('Assertion failed that ' + a + ' equals ' + b);
|
|
}
|
|
|
|
static assertNotEquals(a: any, b: any) {
|
|
if (a === b) throw new Error('Assertion failed that ' + a + ' NOT equals ' + b);
|
|
}
|
|
|
|
async selftest(): Promise<boolean> {
|
|
const clear = 'text line to be encrypted';
|
|
const encrypted = this.encrypt(clear);
|
|
|
|
SyncedAsyncStorage.assertEquals(encrypted.startsWith(this.encryptionMarker), true);
|
|
SyncedAsyncStorage.assertNotEquals(clear, encrypted);
|
|
const decrypted = this.decrypt(encrypted);
|
|
SyncedAsyncStorage.assertEquals(clear, decrypted);
|
|
|
|
SyncedAsyncStorage.assertEquals(this.decrypt(clear), clear);
|
|
|
|
SyncedAsyncStorage.assertEquals(
|
|
this.decrypt(
|
|
'encrypted://U2FsdGVkX19XQWgwS8q5XjQSQ19OmBsNax4k6NZOAsKFhCgw9sJFwb+qVYfqy6X5',
|
|
'3a013f391e59daf2f5074fa66652784d17511ea072d7a8329ff9bddf371932ab',
|
|
),
|
|
'text line to be encrypted',
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param key {string}
|
|
* @param value {string}
|
|
*
|
|
* @return {string} New sequence number from remote
|
|
*/
|
|
async setItemRemote(key: string, value: string): Promise<string> {
|
|
const that = this;
|
|
return new Promise(function (resolve, reject) {
|
|
fetch(that.defaultBaseUrl + '/namespace/' + that.namespace + '/' + key, {
|
|
method: 'POST',
|
|
headers: {
|
|
Accept: 'text/plain',
|
|
'Content-Type': 'text/plain',
|
|
},
|
|
body: value,
|
|
})
|
|
.then(async response => {
|
|
const text = await response.text();
|
|
console.log('saved, seq num:', text);
|
|
resolve(text);
|
|
})
|
|
.catch((reason: Error) => reject(reason));
|
|
});
|
|
}
|
|
|
|
async setItem(key: string, value: string) {
|
|
value = this.encrypt(value);
|
|
await AsyncStorage.setItem(this.namespace + '_' + key, value);
|
|
const newSeqNum = await this.setItemRemote(key, value);
|
|
const localSeqNum = await this.getLocalSeqNum();
|
|
if (+localSeqNum > +newSeqNum) {
|
|
// some race condition during save happened..?
|
|
return;
|
|
}
|
|
await AsyncStorage.setItem(this.namespace + '_' + 'seqnum', newSeqNum);
|
|
}
|
|
|
|
async getItemRemote(key: string) {
|
|
const response = await fetch(this.defaultBaseUrl + '/namespace/' + this.namespace + '/' + key);
|
|
return await response.text();
|
|
}
|
|
|
|
async getItem(key: string) {
|
|
return this.decrypt(await AsyncStorage.getItem(this.namespace + '_' + key));
|
|
}
|
|
|
|
async getAllKeysRemote(): Promise<string[]> {
|
|
const response = await fetch(this.defaultBaseUrl + '/namespacekeys/' + this.namespace);
|
|
const text = await response.text();
|
|
return text.split(',');
|
|
}
|
|
|
|
async getAllKeys(): Promise<string[]> {
|
|
return (await AsyncStorage.getAllKeys())
|
|
.filter(key => key.startsWith(this.namespace + '_'))
|
|
.map(key => key.replace(this.namespace + '_', ''));
|
|
}
|
|
|
|
async getLocalSeqNum() {
|
|
return (await AsyncStorage.getItem(this.namespace + '_' + 'seqnum')) || '0';
|
|
}
|
|
|
|
async purgeLocalStorage() {
|
|
if (!this.namespace) throw new Error('No namespace');
|
|
const keys = (await AsyncStorage.getAllKeys()).filter(key => key.startsWith(this.namespace));
|
|
for (const key of keys) {
|
|
await AsyncStorage.removeItem(key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Should be called at init.
|
|
* Checks remote sequence number, and if remote is ahead - we sync all keys with local storage.
|
|
*/
|
|
async synchronize() {
|
|
const response = await fetch(this.defaultBaseUrl + '/namespaceseq/' + this.namespace);
|
|
const remoteSeqNum = (await response.text()) || '0';
|
|
const localSeqNum = await this.getLocalSeqNum();
|
|
if (+remoteSeqNum > +localSeqNum) {
|
|
console.log('remote storage is ahead, need to sync;', +remoteSeqNum, '>', +localSeqNum);
|
|
|
|
// sort to ensure channel_manager comes first
|
|
for (const key of (await this.getAllKeysRemote()).sort()) {
|
|
const value = await this.getItemRemote(key);
|
|
await AsyncStorage.setItem(this.namespace + '_' + key, value);
|
|
console.log('synced', key, 'to', value);
|
|
}
|
|
|
|
await AsyncStorage.setItem(this.namespace + '_' + 'seqnum', remoteSeqNum);
|
|
} else {
|
|
console.log('storage is up-to-date, no need for sync');
|
|
}
|
|
}
|
|
}
|