forked from BlueWallet/BlueWallet
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsynced-async-storage.ts
160 lines (135 loc) · 5.35 KB
/
synced-async-storage.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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');
}
}
}