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.
@@ -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;