libsignal-skyzopedia 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +32 -0
- package/package.json +27 -0
- package/src/.eslintrc.json +31 -0
- package/src/WhisperTextProtocol.js +948 -0
- package/src/base_key_type.js +7 -0
- package/src/chain_type.js +6 -0
- package/src/crypto.js +98 -0
- package/src/curve.js +120 -0
- package/src/errors.js +33 -0
- package/src/keyhelper.js +45 -0
- package/src/numeric_fingerprint.js +72 -0
- package/src/protobufs.js +10 -0
- package/src/protocol_address.js +40 -0
- package/src/queue_job.js +69 -0
- package/src/session_builder.js +164 -0
- package/src/session_cipher.js +336 -0
- package/src/session_record.js +316 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// vim: ts=4:sw=4:expandtab
|
|
2
|
+
|
|
3
|
+
const ChainType = require('./chain_type');
|
|
4
|
+
const ProtocolAddress = require('./protocol_address');
|
|
5
|
+
const SessionBuilder = require('./session_builder');
|
|
6
|
+
const SessionRecord = require('./session_record');
|
|
7
|
+
const crypto = require('./crypto');
|
|
8
|
+
const curve = require('./curve');
|
|
9
|
+
const errors = require('./errors');
|
|
10
|
+
const protobufs = require('./protobufs');
|
|
11
|
+
const queueJob = require('./queue_job');
|
|
12
|
+
|
|
13
|
+
const VERSION = 3;
|
|
14
|
+
|
|
15
|
+
function assertBuffer(value) {
|
|
16
|
+
if (!(value instanceof Buffer)) {
|
|
17
|
+
throw TypeError(`Expected Buffer instead of: ${value.constructor.name}`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SessionCipher {
|
|
24
|
+
|
|
25
|
+
constructor(storage, protocolAddress) {
|
|
26
|
+
if (!(protocolAddress instanceof ProtocolAddress)) {
|
|
27
|
+
throw new TypeError("protocolAddress must be a ProtocolAddress");
|
|
28
|
+
}
|
|
29
|
+
this.addr = protocolAddress;
|
|
30
|
+
this.storage = storage;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_encodeTupleByte(number1, number2) {
|
|
34
|
+
if (number1 > 15 || number2 > 15) {
|
|
35
|
+
throw TypeError("Numbers must be 4 bits or less");
|
|
36
|
+
}
|
|
37
|
+
return (number1 << 4) | number2;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_decodeTupleByte(byte) {
|
|
41
|
+
return [byte >> 4, byte & 0xf];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
toString() {
|
|
45
|
+
return `<SessionCipher(${this.addr.toString()})>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getRecord() {
|
|
49
|
+
const record = await this.storage.loadSession(this.addr.toString());
|
|
50
|
+
if (record && !(record instanceof SessionRecord)) {
|
|
51
|
+
throw new TypeError('SessionRecord type expected from loadSession');
|
|
52
|
+
}
|
|
53
|
+
return record;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async storeRecord(record) {
|
|
57
|
+
record.removeOldSessions();
|
|
58
|
+
await this.storage.storeSession(this.addr.toString(), record);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async queueJob(awaitable) {
|
|
62
|
+
return await queueJob(this.addr.toString(), awaitable);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async encrypt(data) {
|
|
66
|
+
assertBuffer(data);
|
|
67
|
+
const ourIdentityKey = await this.storage.getOurIdentity();
|
|
68
|
+
return await this.queueJob(async () => {
|
|
69
|
+
const record = await this.getRecord();
|
|
70
|
+
if (!record) {
|
|
71
|
+
throw new errors.SessionError("No sessions");
|
|
72
|
+
}
|
|
73
|
+
const session = record.getOpenSession();
|
|
74
|
+
if (!session) {
|
|
75
|
+
throw new errors.SessionError("No open session");
|
|
76
|
+
}
|
|
77
|
+
const remoteIdentityKey = session.indexInfo.remoteIdentityKey;
|
|
78
|
+
if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) {
|
|
79
|
+
throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey);
|
|
80
|
+
}
|
|
81
|
+
const chain = session.getChain(session.currentRatchet.ephemeralKeyPair.pubKey);
|
|
82
|
+
if (chain.chainType === ChainType.RECEIVING) {
|
|
83
|
+
throw new Error("Tried to encrypt on a receiving chain");
|
|
84
|
+
}
|
|
85
|
+
this.fillMessageKeys(chain, chain.chainKey.counter + 1);
|
|
86
|
+
const keys = crypto.deriveSecrets(chain.messageKeys[chain.chainKey.counter],
|
|
87
|
+
Buffer.alloc(32), Buffer.from("WhisperMessageKeys"));
|
|
88
|
+
delete chain.messageKeys[chain.chainKey.counter];
|
|
89
|
+
const msg = protobufs.WhisperMessage.create();
|
|
90
|
+
msg.ephemeralKey = session.currentRatchet.ephemeralKeyPair.pubKey;
|
|
91
|
+
msg.counter = chain.chainKey.counter;
|
|
92
|
+
msg.previousCounter = session.currentRatchet.previousCounter;
|
|
93
|
+
msg.ciphertext = crypto.encrypt(keys[0], data, keys[2].slice(0, 16));
|
|
94
|
+
const msgBuf = protobufs.WhisperMessage.encode(msg).finish();
|
|
95
|
+
const macInput = Buffer.alloc(msgBuf.byteLength + (33 * 2) + 1);
|
|
96
|
+
macInput.set(ourIdentityKey.pubKey);
|
|
97
|
+
macInput.set(session.indexInfo.remoteIdentityKey, 33);
|
|
98
|
+
macInput[33 * 2] = this._encodeTupleByte(VERSION, VERSION);
|
|
99
|
+
macInput.set(msgBuf, (33 * 2) + 1);
|
|
100
|
+
const mac = crypto.calculateMAC(keys[1], macInput);
|
|
101
|
+
const result = Buffer.alloc(msgBuf.byteLength + 9);
|
|
102
|
+
result[0] = this._encodeTupleByte(VERSION, VERSION);
|
|
103
|
+
result.set(msgBuf, 1);
|
|
104
|
+
result.set(mac.slice(0, 8), msgBuf.byteLength + 1);
|
|
105
|
+
await this.storeRecord(record);
|
|
106
|
+
let type, body;
|
|
107
|
+
if (session.pendingPreKey) {
|
|
108
|
+
type = 3; // prekey bundle
|
|
109
|
+
const preKeyMsg = protobufs.PreKeyWhisperMessage.create({
|
|
110
|
+
identityKey: ourIdentityKey.pubKey,
|
|
111
|
+
registrationId: await this.storage.getOurRegistrationId(),
|
|
112
|
+
baseKey: session.pendingPreKey.baseKey,
|
|
113
|
+
signedPreKeyId: session.pendingPreKey.signedKeyId,
|
|
114
|
+
message: result
|
|
115
|
+
});
|
|
116
|
+
if (session.pendingPreKey.preKeyId) {
|
|
117
|
+
preKeyMsg.preKeyId = session.pendingPreKey.preKeyId;
|
|
118
|
+
}
|
|
119
|
+
body = Buffer.concat([
|
|
120
|
+
Buffer.from([this._encodeTupleByte(VERSION, VERSION)]),
|
|
121
|
+
Buffer.from(
|
|
122
|
+
protobufs.PreKeyWhisperMessage.encode(preKeyMsg).finish()
|
|
123
|
+
)
|
|
124
|
+
]);
|
|
125
|
+
} else {
|
|
126
|
+
type = 1; // normal
|
|
127
|
+
body = result;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
type,
|
|
131
|
+
body,
|
|
132
|
+
registrationId: session.registrationId
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async decryptWithSessions(data, sessions) {
|
|
138
|
+
// Iterate through the sessions, attempting to decrypt using each one.
|
|
139
|
+
// Stop and return the result if we get a valid result.
|
|
140
|
+
if (!sessions.length) {
|
|
141
|
+
throw new errors.SessionError("No sessions available");
|
|
142
|
+
}
|
|
143
|
+
const errs = [];
|
|
144
|
+
for (const session of sessions) {
|
|
145
|
+
let plaintext;
|
|
146
|
+
try {
|
|
147
|
+
plaintext = await this.doDecryptWhisperMessage(data, session);
|
|
148
|
+
session.indexInfo.used = Date.now();
|
|
149
|
+
return {
|
|
150
|
+
session,
|
|
151
|
+
plaintext
|
|
152
|
+
};
|
|
153
|
+
} catch(e) {
|
|
154
|
+
errs.push(e);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//console.error("Failed to decrypt message with any known session, wait baron will fix it..");
|
|
158
|
+
// for (const e of errs) {
|
|
159
|
+
// console.error("Session error:" + e, e.stack);
|
|
160
|
+
// }
|
|
161
|
+
throw new errors.SessionError("No matching sessions found for message");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async decryptWhisperMessage(data) {
|
|
165
|
+
assertBuffer(data);
|
|
166
|
+
return await this.queueJob(async () => {
|
|
167
|
+
const record = await this.getRecord();
|
|
168
|
+
if (!record) {
|
|
169
|
+
throw new errors.SessionError("No session record");
|
|
170
|
+
}
|
|
171
|
+
const result = await this.decryptWithSessions(data, record.getSessions());
|
|
172
|
+
const remoteIdentityKey = result.session.indexInfo.remoteIdentityKey;
|
|
173
|
+
if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) {
|
|
174
|
+
throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey);
|
|
175
|
+
}
|
|
176
|
+
if (record.isClosed(result.session)) {
|
|
177
|
+
// It's possible for this to happen when processing a backlog of messages.
|
|
178
|
+
// The message was, hopefully, just sent back in a time when this session
|
|
179
|
+
// was the most current. Simply make a note of it and continue. If our
|
|
180
|
+
// actual open session is for reason invalid, that must be handled via
|
|
181
|
+
// a full SessionError response.
|
|
182
|
+
//console.warn("Decrypted message with closed session.");
|
|
183
|
+
}
|
|
184
|
+
await this.storeRecord(record);
|
|
185
|
+
return result.plaintext;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async decryptPreKeyWhisperMessage(data) {
|
|
190
|
+
assertBuffer(data);
|
|
191
|
+
const versions = this._decodeTupleByte(data[0]);
|
|
192
|
+
if (versions[1] > 3 || versions[0] < 3) { // min version > 3 or max version < 3
|
|
193
|
+
throw new Error("Incompatible version number on PreKeyWhisperMessage");
|
|
194
|
+
}
|
|
195
|
+
return await this.queueJob(async () => {
|
|
196
|
+
let record = await this.getRecord();
|
|
197
|
+
const preKeyProto = protobufs.PreKeyWhisperMessage.decode(data.slice(1));
|
|
198
|
+
if (!record) {
|
|
199
|
+
if (preKeyProto.registrationId == null) {
|
|
200
|
+
throw new Error("No registrationId");
|
|
201
|
+
}
|
|
202
|
+
record = new SessionRecord();
|
|
203
|
+
}
|
|
204
|
+
const builder = new SessionBuilder(this.storage, this.addr);
|
|
205
|
+
const preKeyId = await builder.initIncoming(record, preKeyProto);
|
|
206
|
+
const session = record.getSession(preKeyProto.baseKey);
|
|
207
|
+
const plaintext = await this.doDecryptWhisperMessage(preKeyProto.message, session);
|
|
208
|
+
await this.storeRecord(record);
|
|
209
|
+
if (preKeyId) {
|
|
210
|
+
await this.storage.removePreKey(preKeyId);
|
|
211
|
+
}
|
|
212
|
+
return plaintext;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async doDecryptWhisperMessage(messageBuffer, session) {
|
|
217
|
+
assertBuffer(messageBuffer);
|
|
218
|
+
if (!session) {
|
|
219
|
+
throw new TypeError("session required");
|
|
220
|
+
}
|
|
221
|
+
const versions = this._decodeTupleByte(messageBuffer[0]);
|
|
222
|
+
if (versions[1] > 3 || versions[0] < 3) { // min version > 3 or max version < 3
|
|
223
|
+
throw new Error("Incompatible version number on WhisperMessage");
|
|
224
|
+
}
|
|
225
|
+
const messageProto = messageBuffer.slice(1, -8);
|
|
226
|
+
const message = protobufs.WhisperMessage.decode(messageProto);
|
|
227
|
+
this.maybeStepRatchet(session, message.ephemeralKey, message.previousCounter);
|
|
228
|
+
const chain = session.getChain(message.ephemeralKey);
|
|
229
|
+
if (chain.chainType === ChainType.SENDING) {
|
|
230
|
+
throw new Error("Tried to decrypt on a sending chain");
|
|
231
|
+
}
|
|
232
|
+
this.fillMessageKeys(chain, message.counter);
|
|
233
|
+
if (!chain.messageKeys.hasOwnProperty(message.counter)) {
|
|
234
|
+
// Most likely the message was already decrypted and we are trying to process
|
|
235
|
+
// twice. This can happen if the user restarts before the server gets an ACK.
|
|
236
|
+
throw new errors.MessageCounterError('Key used already or never filled');
|
|
237
|
+
}
|
|
238
|
+
const messageKey = chain.messageKeys[message.counter];
|
|
239
|
+
delete chain.messageKeys[message.counter];
|
|
240
|
+
const keys = crypto.deriveSecrets(messageKey, Buffer.alloc(32),
|
|
241
|
+
Buffer.from("WhisperMessageKeys"));
|
|
242
|
+
const ourIdentityKey = await this.storage.getOurIdentity();
|
|
243
|
+
const macInput = Buffer.alloc(messageProto.byteLength + (33 * 2) + 1);
|
|
244
|
+
macInput.set(session.indexInfo.remoteIdentityKey);
|
|
245
|
+
macInput.set(ourIdentityKey.pubKey, 33);
|
|
246
|
+
macInput[33 * 2] = this._encodeTupleByte(VERSION, VERSION);
|
|
247
|
+
macInput.set(messageProto, (33 * 2) + 1);
|
|
248
|
+
// This is where we most likely fail if the session is not a match.
|
|
249
|
+
// Don't misinterpret this as corruption.
|
|
250
|
+
crypto.verifyMAC(macInput, keys[1], messageBuffer.slice(-8), 8);
|
|
251
|
+
const plaintext = crypto.decrypt(keys[0], message.ciphertext, keys[2].slice(0, 16));
|
|
252
|
+
delete session.pendingPreKey;
|
|
253
|
+
return plaintext;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fillMessageKeys(chain, counter) {
|
|
257
|
+
if (chain.chainKey.counter >= counter) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (counter - chain.chainKey.counter > 2000) {
|
|
261
|
+
throw new errors.SessionError('Over 2000 messages into the future!');
|
|
262
|
+
}
|
|
263
|
+
if (chain.chainKey.key === undefined) {
|
|
264
|
+
throw new errors.SessionError('Chain closed');
|
|
265
|
+
}
|
|
266
|
+
const key = chain.chainKey.key;
|
|
267
|
+
chain.messageKeys[chain.chainKey.counter + 1] = crypto.calculateMAC(key, Buffer.from([1]));
|
|
268
|
+
chain.chainKey.key = crypto.calculateMAC(key, Buffer.from([2]));
|
|
269
|
+
chain.chainKey.counter += 1;
|
|
270
|
+
return this.fillMessageKeys(chain, counter);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
maybeStepRatchet(session, remoteKey, previousCounter) {
|
|
274
|
+
if (session.getChain(remoteKey)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const ratchet = session.currentRatchet;
|
|
278
|
+
let previousRatchet = session.getChain(ratchet.lastRemoteEphemeralKey);
|
|
279
|
+
if (previousRatchet) {
|
|
280
|
+
this.fillMessageKeys(previousRatchet, previousCounter);
|
|
281
|
+
delete previousRatchet.chainKey.key; // Close
|
|
282
|
+
}
|
|
283
|
+
this.calculateRatchet(session, remoteKey, false);
|
|
284
|
+
// Now swap the ephemeral key and calculate the new sending chain
|
|
285
|
+
const prevCounter = session.getChain(ratchet.ephemeralKeyPair.pubKey);
|
|
286
|
+
if (prevCounter) {
|
|
287
|
+
ratchet.previousCounter = prevCounter.chainKey.counter;
|
|
288
|
+
session.deleteChain(ratchet.ephemeralKeyPair.pubKey);
|
|
289
|
+
}
|
|
290
|
+
ratchet.ephemeralKeyPair = curve.generateKeyPair();
|
|
291
|
+
this.calculateRatchet(session, remoteKey, true);
|
|
292
|
+
ratchet.lastRemoteEphemeralKey = remoteKey;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
calculateRatchet(session, remoteKey, sending) {
|
|
296
|
+
let ratchet = session.currentRatchet;
|
|
297
|
+
const sharedSecret = curve.calculateAgreement(remoteKey, ratchet.ephemeralKeyPair.privKey);
|
|
298
|
+
const masterKey = crypto.deriveSecrets(sharedSecret, ratchet.rootKey,
|
|
299
|
+
Buffer.from("WhisperRatchet"), /*chunks*/ 2);
|
|
300
|
+
const chainKey = sending ? ratchet.ephemeralKeyPair.pubKey : remoteKey;
|
|
301
|
+
session.addChain(chainKey, {
|
|
302
|
+
messageKeys: {},
|
|
303
|
+
chainKey: {
|
|
304
|
+
counter: -1,
|
|
305
|
+
key: masterKey[1]
|
|
306
|
+
},
|
|
307
|
+
chainType: sending ? ChainType.SENDING : ChainType.RECEIVING
|
|
308
|
+
});
|
|
309
|
+
ratchet.rootKey = masterKey[0];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async hasOpenSession() {
|
|
313
|
+
return await this.queueJob(async () => {
|
|
314
|
+
const record = await this.getRecord();
|
|
315
|
+
if (!record) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
return record.haveOpenSession();
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async closeOpenSession() {
|
|
323
|
+
return await this.queueJob(async () => {
|
|
324
|
+
const record = await this.getRecord();
|
|
325
|
+
if (record) {
|
|
326
|
+
const openSession = record.getOpenSession();
|
|
327
|
+
if (openSession) {
|
|
328
|
+
record.closeSession(openSession);
|
|
329
|
+
await this.storeRecord(record);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = SessionCipher;
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// vim: ts=4:sw=4
|
|
2
|
+
|
|
3
|
+
const BaseKeyType = require('./base_key_type');
|
|
4
|
+
|
|
5
|
+
const CLOSED_SESSIONS_MAX = 40;
|
|
6
|
+
const SESSION_RECORD_VERSION = 'v1';
|
|
7
|
+
|
|
8
|
+
function assertBuffer(value) {
|
|
9
|
+
if (!Buffer.isBuffer(value)) {
|
|
10
|
+
throw new TypeError("Buffer required");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SessionEntry {
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
this._chains = {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toString() {
|
|
22
|
+
const baseKey = this.indexInfo && this.indexInfo.baseKey &&
|
|
23
|
+
this.indexInfo.baseKey.toString('base64');
|
|
24
|
+
return `<SessionEntry [baseKey=${baseKey}]>`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
inspect() {
|
|
28
|
+
return this.toString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
addChain(key, value) {
|
|
32
|
+
assertBuffer(key);
|
|
33
|
+
const id = key.toString('base64');
|
|
34
|
+
if (this._chains.hasOwnProperty(id)) {
|
|
35
|
+
throw new Error("Overwrite attempt");
|
|
36
|
+
}
|
|
37
|
+
this._chains[id] = value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getChain(key) {
|
|
41
|
+
assertBuffer(key);
|
|
42
|
+
return this._chains[key.toString('base64')];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
deleteChain(key) {
|
|
46
|
+
assertBuffer(key);
|
|
47
|
+
const id = key.toString('base64');
|
|
48
|
+
if (!this._chains.hasOwnProperty(id)) {
|
|
49
|
+
throw new ReferenceError("Not Found");
|
|
50
|
+
}
|
|
51
|
+
delete this._chains[id];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
*chains() {
|
|
55
|
+
for (const [k, v] of Object.entries(this._chains)) {
|
|
56
|
+
yield [Buffer.from(k, 'base64'), v];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
serialize() {
|
|
61
|
+
const data = {
|
|
62
|
+
registrationId: this.registrationId,
|
|
63
|
+
currentRatchet: {
|
|
64
|
+
ephemeralKeyPair: {
|
|
65
|
+
pubKey: this.currentRatchet.ephemeralKeyPair.pubKey.toString('base64'),
|
|
66
|
+
privKey: this.currentRatchet.ephemeralKeyPair.privKey.toString('base64')
|
|
67
|
+
},
|
|
68
|
+
lastRemoteEphemeralKey: this.currentRatchet.lastRemoteEphemeralKey.toString('base64'),
|
|
69
|
+
previousCounter: this.currentRatchet.previousCounter,
|
|
70
|
+
rootKey: this.currentRatchet.rootKey.toString('base64')
|
|
71
|
+
},
|
|
72
|
+
indexInfo: {
|
|
73
|
+
baseKey: this.indexInfo.baseKey.toString('base64'),
|
|
74
|
+
baseKeyType: this.indexInfo.baseKeyType,
|
|
75
|
+
closed: this.indexInfo.closed,
|
|
76
|
+
used: this.indexInfo.used,
|
|
77
|
+
created: this.indexInfo.created,
|
|
78
|
+
remoteIdentityKey: this.indexInfo.remoteIdentityKey.toString('base64')
|
|
79
|
+
},
|
|
80
|
+
_chains: this._serialize_chains(this._chains)
|
|
81
|
+
};
|
|
82
|
+
if (this.pendingPreKey) {
|
|
83
|
+
data.pendingPreKey = Object.assign({}, this.pendingPreKey);
|
|
84
|
+
data.pendingPreKey.baseKey = this.pendingPreKey.baseKey.toString('base64');
|
|
85
|
+
}
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static deserialize(data) {
|
|
90
|
+
const obj = new this();
|
|
91
|
+
obj.registrationId = data.registrationId;
|
|
92
|
+
obj.currentRatchet = {
|
|
93
|
+
ephemeralKeyPair: {
|
|
94
|
+
pubKey: Buffer.from(data.currentRatchet.ephemeralKeyPair.pubKey, 'base64'),
|
|
95
|
+
privKey: Buffer.from(data.currentRatchet.ephemeralKeyPair.privKey, 'base64')
|
|
96
|
+
},
|
|
97
|
+
lastRemoteEphemeralKey: Buffer.from(data.currentRatchet.lastRemoteEphemeralKey, 'base64'),
|
|
98
|
+
previousCounter: data.currentRatchet.previousCounter,
|
|
99
|
+
rootKey: Buffer.from(data.currentRatchet.rootKey, 'base64')
|
|
100
|
+
};
|
|
101
|
+
obj.indexInfo = {
|
|
102
|
+
baseKey: Buffer.from(data.indexInfo.baseKey, 'base64'),
|
|
103
|
+
baseKeyType: data.indexInfo.baseKeyType,
|
|
104
|
+
closed: data.indexInfo.closed,
|
|
105
|
+
used: data.indexInfo.used,
|
|
106
|
+
created: data.indexInfo.created,
|
|
107
|
+
remoteIdentityKey: Buffer.from(data.indexInfo.remoteIdentityKey, 'base64')
|
|
108
|
+
};
|
|
109
|
+
obj._chains = this._deserialize_chains(data._chains);
|
|
110
|
+
if (data.pendingPreKey) {
|
|
111
|
+
obj.pendingPreKey = Object.assign({}, data.pendingPreKey);
|
|
112
|
+
obj.pendingPreKey.baseKey = Buffer.from(data.pendingPreKey.baseKey, 'base64');
|
|
113
|
+
}
|
|
114
|
+
return obj;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_serialize_chains(chains) {
|
|
118
|
+
const r = {};
|
|
119
|
+
for (const key of Object.keys(chains)) {
|
|
120
|
+
const c = chains[key];
|
|
121
|
+
const messageKeys = {};
|
|
122
|
+
for (const [idx, key] of Object.entries(c.messageKeys)) {
|
|
123
|
+
messageKeys[idx] = key.toString('base64');
|
|
124
|
+
}
|
|
125
|
+
r[key] = {
|
|
126
|
+
chainKey: {
|
|
127
|
+
counter: c.chainKey.counter,
|
|
128
|
+
key: c.chainKey.key && c.chainKey.key.toString('base64')
|
|
129
|
+
},
|
|
130
|
+
chainType: c.chainType,
|
|
131
|
+
messageKeys: messageKeys
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return r;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static _deserialize_chains(chains_data) {
|
|
138
|
+
const r = {};
|
|
139
|
+
for (const key of Object.keys(chains_data)) {
|
|
140
|
+
const c = chains_data[key];
|
|
141
|
+
const messageKeys = {};
|
|
142
|
+
for (const [idx, key] of Object.entries(c.messageKeys)) {
|
|
143
|
+
messageKeys[idx] = Buffer.from(key, 'base64');
|
|
144
|
+
}
|
|
145
|
+
r[key] = {
|
|
146
|
+
chainKey: {
|
|
147
|
+
counter: c.chainKey.counter,
|
|
148
|
+
key: c.chainKey.key && Buffer.from(c.chainKey.key, 'base64')
|
|
149
|
+
},
|
|
150
|
+
chainType: c.chainType,
|
|
151
|
+
messageKeys: messageKeys
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return r;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
const migrations = [{
|
|
161
|
+
version: 'v1',
|
|
162
|
+
migrate: function migrateV1(data) {
|
|
163
|
+
const sessions = data._sessions;
|
|
164
|
+
if (data.registrationId) {
|
|
165
|
+
for (const key in sessions) {
|
|
166
|
+
if (!sessions[key].registrationId) {
|
|
167
|
+
sessions[key].registrationId = data.registrationId;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
for (const key in sessions) {
|
|
172
|
+
if (sessions[key].indexInfo.closed === -1) {
|
|
173
|
+
//console.error('V1 session storage migration error: registrationId',
|
|
174
|
+
//data.registrationId, 'for open session version',
|
|
175
|
+
//data.version);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}];
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class SessionRecord {
|
|
184
|
+
|
|
185
|
+
static createEntry() {
|
|
186
|
+
return new SessionEntry();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static migrate(data) {
|
|
190
|
+
let run = (data.version === undefined);
|
|
191
|
+
for (let i = 0; i < migrations.length; ++i) {
|
|
192
|
+
if (run) {
|
|
193
|
+
//console.info("Migrating session to:", migrations[i].version);
|
|
194
|
+
migrations[i].migrate(data);
|
|
195
|
+
} else if (migrations[i].version === data.version) {
|
|
196
|
+
run = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!run) {
|
|
200
|
+
throw new Error("Error migrating SessionRecord");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
static deserialize(data) {
|
|
205
|
+
if (data.version !== SESSION_RECORD_VERSION) {
|
|
206
|
+
this.migrate(data);
|
|
207
|
+
}
|
|
208
|
+
const obj = new this();
|
|
209
|
+
if (data._sessions) {
|
|
210
|
+
for (const [key, entry] of Object.entries(data._sessions)) {
|
|
211
|
+
obj.sessions[key] = SessionEntry.deserialize(entry);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return obj;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
constructor() {
|
|
218
|
+
this.sessions = {};
|
|
219
|
+
this.version = SESSION_RECORD_VERSION;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
serialize() {
|
|
223
|
+
const _sessions = {};
|
|
224
|
+
for (const [key, entry] of Object.entries(this.sessions)) {
|
|
225
|
+
_sessions[key] = entry.serialize();
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
_sessions,
|
|
229
|
+
version: this.version
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
haveOpenSession() {
|
|
234
|
+
const openSession = this.getOpenSession();
|
|
235
|
+
return (!!openSession && typeof openSession.registrationId === 'number');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getSession(key) {
|
|
239
|
+
assertBuffer(key);
|
|
240
|
+
const session = this.sessions[key.toString('base64')];
|
|
241
|
+
if (session && session.indexInfo.baseKeyType === BaseKeyType.OURS) {
|
|
242
|
+
throw new Error("Tried to lookup a session using our basekey");
|
|
243
|
+
}
|
|
244
|
+
return session;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
getOpenSession() {
|
|
248
|
+
for (const session of Object.values(this.sessions)) {
|
|
249
|
+
if (!this.isClosed(session)) {
|
|
250
|
+
return session;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
setSession(session) {
|
|
256
|
+
this.sessions[session.indexInfo.baseKey.toString('base64')] = session;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
getSessions() {
|
|
260
|
+
// Return sessions ordered with most recently used first.
|
|
261
|
+
return Array.from(Object.values(this.sessions)).sort((a, b) => {
|
|
262
|
+
const aUsed = a.indexInfo.used || 0;
|
|
263
|
+
const bUsed = b.indexInfo.used || 0;
|
|
264
|
+
return aUsed === bUsed ? 0 : aUsed < bUsed ? 1 : -1;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
closeSession(session) {
|
|
269
|
+
if (this.isClosed(session)) {
|
|
270
|
+
//console.warn("Session already closed", session);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
//console.info("Closing session:", session);
|
|
274
|
+
session.indexInfo.closed = Date.now();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
openSession(session) {
|
|
278
|
+
if (!this.isClosed(session)) {
|
|
279
|
+
//console.warn("Session already open");
|
|
280
|
+
}
|
|
281
|
+
//console.info("Opening session:", session);
|
|
282
|
+
session.indexInfo.closed = -1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
isClosed(session) {
|
|
286
|
+
return session.indexInfo.closed !== -1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
removeOldSessions() {
|
|
290
|
+
while (Object.keys(this.sessions).length > CLOSED_SESSIONS_MAX) {
|
|
291
|
+
let oldestKey;
|
|
292
|
+
let oldestSession;
|
|
293
|
+
for (const [key, session] of Object.entries(this.sessions)) {
|
|
294
|
+
if (session.indexInfo.closed !== -1 &&
|
|
295
|
+
(!oldestSession || session.indexInfo.closed < oldestSession.indexInfo.closed)) {
|
|
296
|
+
oldestKey = key;
|
|
297
|
+
oldestSession = session;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (oldestKey) {
|
|
301
|
+
//console.info("Removing old closed session:", oldestSession);
|
|
302
|
+
delete this.sessions[oldestKey];
|
|
303
|
+
} else {
|
|
304
|
+
throw new Error('Corrupt sessions object');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
deleteAllSessions() {
|
|
310
|
+
for (const key of Object.keys(this.sessions)) {
|
|
311
|
+
delete this.sessions[key];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = SessionRecord;
|