@unknownncat/swt-libsignal 1.0.4
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/dist/base_key_type.d.ts +5 -0
- package/dist/base_key_type.js +4 -0
- package/dist/chain_type.d.ts +5 -0
- package/dist/chain_type.js +4 -0
- package/dist/crypto.d.ts +2 -0
- package/dist/crypto.js +95 -0
- package/dist/curve.d.ts +5 -0
- package/dist/curve.js +67 -0
- package/dist/fingerprint.d.ts +5 -0
- package/dist/fingerprint.js +76 -0
- package/dist/generated/WhisperTextProtocol.d.ts +49 -0
- package/dist/generated/WhisperTextProtocol.js +199 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +13 -0
- package/dist/job_queue.d.ts +3 -0
- package/dist/job_queue.js +48 -0
- package/dist/key-helper.d.ts +14 -0
- package/dist/key-helper.js +42 -0
- package/dist/protobuf.d.ts +1 -0
- package/dist/protobuf.js +2 -0
- package/dist/protocol_address.d.ts +9 -0
- package/dist/protocol_address.js +50 -0
- package/dist/session/builder/index.d.ts +2 -0
- package/dist/session/builder/index.js +2 -0
- package/dist/session/builder/session-builder.d.ts +13 -0
- package/dist/session/builder/session-builder.js +148 -0
- package/dist/session/builder/types.d.ts +38 -0
- package/dist/session/builder/types.js +1 -0
- package/dist/session/cipher/encoding.d.ts +13 -0
- package/dist/session/cipher/encoding.js +135 -0
- package/dist/session/cipher/index.d.ts +3 -0
- package/dist/session/cipher/index.js +3 -0
- package/dist/session/cipher/session-cipher.d.ts +26 -0
- package/dist/session/cipher/session-cipher.js +300 -0
- package/dist/session/cipher/types.d.ts +47 -0
- package/dist/session/cipher/types.js +1 -0
- package/dist/session/constants.d.ts +3 -0
- package/dist/session/constants.js +3 -0
- package/dist/session/index.d.ts +3 -0
- package/dist/session/index.js +3 -0
- package/dist/session/record/index.d.ts +3 -0
- package/dist/session/record/index.js +3 -0
- package/dist/session/record/session-entry.d.ts +20 -0
- package/dist/session/record/session-entry.js +146 -0
- package/dist/session/record/session-record.d.ts +21 -0
- package/dist/session/record/session-record.js +95 -0
- package/dist/session/record/types.d.ts +71 -0
- package/dist/session/record/types.js +1 -0
- package/dist/session/utils.d.ts +7 -0
- package/dist/session/utils.js +18 -0
- package/dist/signal-errors.d.ts +18 -0
- package/dist/signal-errors.js +24 -0
- package/dist/teste.d.ts +1 -0
- package/dist/teste.js +18 -0
- package/package.json +40 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION } from '../constants.js';
|
|
2
|
+
import { WhisperMessageEncoder } from './encoding.js';
|
|
3
|
+
import { assertUint8 } from '../utils.js';
|
|
4
|
+
import { ChainType } from '../../chain_type.js';
|
|
5
|
+
import { ProtocolAddress } from '../../protocol_address.js';
|
|
6
|
+
import { SessionBuilder } from '../builder/session-builder.js';
|
|
7
|
+
import { SessionRecord } from '../record/index.js';
|
|
8
|
+
import { crypto } from '../../crypto.js';
|
|
9
|
+
import { signalCrypto } from '../../curve.js';
|
|
10
|
+
import { SessionError, UntrustedIdentityKeyError, MessageCounterError } from '../../signal-errors.js';
|
|
11
|
+
import { enqueue } from '../../job_queue.js';
|
|
12
|
+
export class SessionCipher {
|
|
13
|
+
addr;
|
|
14
|
+
addrStr;
|
|
15
|
+
storage;
|
|
16
|
+
constructor(storage, protocolAddress) {
|
|
17
|
+
if (!(protocolAddress instanceof ProtocolAddress)) {
|
|
18
|
+
throw new TypeError('protocolAddress must be a ProtocolAddress');
|
|
19
|
+
}
|
|
20
|
+
this.addr = protocolAddress;
|
|
21
|
+
this.addrStr = protocolAddress.toString();
|
|
22
|
+
this.storage = storage;
|
|
23
|
+
}
|
|
24
|
+
toString() {
|
|
25
|
+
return `<SessionCipher(${this.addrStr})>`;
|
|
26
|
+
}
|
|
27
|
+
_encodeTupleByte(n1, n2) {
|
|
28
|
+
if (n1 > 15 || n2 > 15)
|
|
29
|
+
throw new TypeError('Numbers must be 4 bits or less');
|
|
30
|
+
return (n1 << 4) | n2;
|
|
31
|
+
}
|
|
32
|
+
_decodeTupleByte(byte) {
|
|
33
|
+
return [byte >> 4, byte & 0xf];
|
|
34
|
+
}
|
|
35
|
+
async getRecord() {
|
|
36
|
+
const record = await this.storage.loadSession(this.addrStr);
|
|
37
|
+
if (record && !(record instanceof SessionRecord)) {
|
|
38
|
+
throw new TypeError('SessionRecord type expected from loadSession');
|
|
39
|
+
}
|
|
40
|
+
return record;
|
|
41
|
+
}
|
|
42
|
+
async storeRecord(record) {
|
|
43
|
+
record.removeOldSessions();
|
|
44
|
+
await this.storage.storeSession(this.addrStr, record);
|
|
45
|
+
}
|
|
46
|
+
async queueJob(fn) {
|
|
47
|
+
return enqueue(this.addrStr, fn);
|
|
48
|
+
}
|
|
49
|
+
async encrypt(data) {
|
|
50
|
+
assertUint8(data);
|
|
51
|
+
const ourIdentity = await this.storage.getOurIdentity();
|
|
52
|
+
return this.queueJob(async () => {
|
|
53
|
+
const record = await this.getRecord();
|
|
54
|
+
if (!record)
|
|
55
|
+
throw new SessionError('No sessions');
|
|
56
|
+
const session = record.getOpenSession();
|
|
57
|
+
if (!session)
|
|
58
|
+
throw new SessionError('No open session');
|
|
59
|
+
const remoteIdentityKey = session.indexInfo.remoteIdentityKey;
|
|
60
|
+
if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) {
|
|
61
|
+
throw new UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey);
|
|
62
|
+
}
|
|
63
|
+
const chain = session.getChain(session.currentRatchet.ephemeralKeyPair.pubKey);
|
|
64
|
+
if (!chain || chain.chainType === ChainType.RECEIVING) {
|
|
65
|
+
throw new Error('Tried to encrypt on a receiving chain');
|
|
66
|
+
}
|
|
67
|
+
this.fillMessageKeys(chain, chain.chainKey.counter + 1);
|
|
68
|
+
const messageKey = chain.messageKeys[chain.chainKey.counter];
|
|
69
|
+
if (!messageKey)
|
|
70
|
+
throw new Error('Message key not generated');
|
|
71
|
+
const keys = this.deriveSecrets(messageKey, new Uint8Array(32), new TextEncoder().encode('WhisperMessageKeys'));
|
|
72
|
+
delete chain.messageKeys[chain.chainKey.counter];
|
|
73
|
+
const [cipherKey, macKey, aadKey] = keys;
|
|
74
|
+
if (!cipherKey || !macKey || !aadKey)
|
|
75
|
+
throw new Error('Keys not derived');
|
|
76
|
+
const encrypted = await crypto.encrypt(cipherKey, data, { aad: aadKey.subarray(0, 16) });
|
|
77
|
+
const msg = {
|
|
78
|
+
ephemeralKey: session.currentRatchet.ephemeralKeyPair.pubKey,
|
|
79
|
+
counter: chain.chainKey.counter,
|
|
80
|
+
previousCounter: session.currentRatchet.previousCounter,
|
|
81
|
+
ciphertext: encrypted.ciphertext
|
|
82
|
+
};
|
|
83
|
+
const msgBuf = WhisperMessageEncoder.encodeWhisperMessage(msg);
|
|
84
|
+
const macInput = new Uint8Array(msgBuf.byteLength + 67);
|
|
85
|
+
macInput.set(ourIdentity.pubKey);
|
|
86
|
+
macInput.set(remoteIdentityKey, 33);
|
|
87
|
+
macInput[66] = this._encodeTupleByte(PROTOCOL_VERSION, PROTOCOL_VERSION);
|
|
88
|
+
macInput.set(msgBuf, 67);
|
|
89
|
+
const mac = crypto.hmacSha256(macKey, macInput);
|
|
90
|
+
const result = new Uint8Array(msgBuf.byteLength + 9);
|
|
91
|
+
result[0] = this._encodeTupleByte(PROTOCOL_VERSION, PROTOCOL_VERSION);
|
|
92
|
+
result.set(msgBuf, 1);
|
|
93
|
+
result.set(mac.subarray(0, 8), msgBuf.byteLength + 1);
|
|
94
|
+
await this.storeRecord(record);
|
|
95
|
+
if (session.pendingPreKey) {
|
|
96
|
+
const preKeyMsg = {
|
|
97
|
+
identityKey: ourIdentity.pubKey,
|
|
98
|
+
registrationId: await this.storage.getOurRegistrationId(),
|
|
99
|
+
baseKey: session.pendingPreKey.baseKey,
|
|
100
|
+
signedPreKeyId: session.pendingPreKey.signedKeyId,
|
|
101
|
+
preKeyId: session.pendingPreKey.preKeyId,
|
|
102
|
+
message: result
|
|
103
|
+
};
|
|
104
|
+
const preKeyBuf = WhisperMessageEncoder.encodePreKeyWhisperMessage(preKeyMsg);
|
|
105
|
+
const body = new Uint8Array(1 + preKeyBuf.byteLength);
|
|
106
|
+
body[0] = this._encodeTupleByte(PROTOCOL_VERSION, PROTOCOL_VERSION);
|
|
107
|
+
body.set(preKeyBuf, 1);
|
|
108
|
+
return { type: 3, body, registrationId: session.registrationId };
|
|
109
|
+
}
|
|
110
|
+
return { type: 1, body: result, registrationId: session.registrationId };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async decryptWhisperMessage(data) {
|
|
114
|
+
assertUint8(data);
|
|
115
|
+
return this.queueJob(async () => {
|
|
116
|
+
const record = await this.getRecord();
|
|
117
|
+
if (!record)
|
|
118
|
+
throw new SessionError('No session record');
|
|
119
|
+
const result = await this.decryptWithSessions(data, record.getSessions());
|
|
120
|
+
const remoteIdentityKey = result.session.indexInfo.remoteIdentityKey;
|
|
121
|
+
if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) {
|
|
122
|
+
throw new UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey);
|
|
123
|
+
}
|
|
124
|
+
await this.storeRecord(record);
|
|
125
|
+
return result.plaintext;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async decryptPreKeyWhisperMessage(data) {
|
|
129
|
+
assertUint8(data);
|
|
130
|
+
if (!data[0])
|
|
131
|
+
throw new Error('Invalid PreKeyWhisperMessage');
|
|
132
|
+
const versions = this._decodeTupleByte(data[0]);
|
|
133
|
+
if (versions[1] > PROTOCOL_VERSION || versions[0] < PROTOCOL_VERSION) {
|
|
134
|
+
throw new Error('Incompatible version number on PreKeyWhisperMessage');
|
|
135
|
+
}
|
|
136
|
+
return this.queueJob(async () => {
|
|
137
|
+
let record = await this.getRecord();
|
|
138
|
+
const preKeyProto = WhisperMessageEncoder.decodePreKeyWhisperMessage(data.subarray(1));
|
|
139
|
+
if (!record) {
|
|
140
|
+
if (preKeyProto.registrationId == null)
|
|
141
|
+
throw new Error('No registrationId');
|
|
142
|
+
record = new SessionRecord();
|
|
143
|
+
}
|
|
144
|
+
const builder = new SessionBuilder(this.storage, this.addr);
|
|
145
|
+
const preKeyId = await builder.initIncoming(record, preKeyProto);
|
|
146
|
+
const session = record.getSession(preKeyProto.baseKey);
|
|
147
|
+
if (!session)
|
|
148
|
+
throw new SessionError('Session not initialized');
|
|
149
|
+
const plaintext = await this.doDecryptWhisperMessage(preKeyProto.message, session);
|
|
150
|
+
await this.storeRecord(record);
|
|
151
|
+
if (preKeyId)
|
|
152
|
+
await this.storage.removePreKey(preKeyId);
|
|
153
|
+
return plaintext;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async hasOpenSession() {
|
|
157
|
+
return this.queueJob(async () => {
|
|
158
|
+
const record = await this.getRecord();
|
|
159
|
+
return !!record && record.haveOpenSession();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async closeOpenSession() {
|
|
163
|
+
return this.queueJob(async () => {
|
|
164
|
+
const record = await this.getRecord();
|
|
165
|
+
if (record) {
|
|
166
|
+
const open = record.getOpenSession();
|
|
167
|
+
if (open) {
|
|
168
|
+
record.closeSession(open);
|
|
169
|
+
await this.storeRecord(record);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async decryptWithSessions(data, sessions) {
|
|
175
|
+
if (!sessions.length)
|
|
176
|
+
throw new SessionError('No sessions available');
|
|
177
|
+
const errors = [];
|
|
178
|
+
for (const session of sessions) {
|
|
179
|
+
try {
|
|
180
|
+
const plaintext = await this.doDecryptWhisperMessage(data, session);
|
|
181
|
+
session.indexInfo.used = Date.now();
|
|
182
|
+
return { session, plaintext };
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
errors.push(e);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
console.error('Failed to decrypt with any session');
|
|
189
|
+
errors.forEach(e => console.error(e));
|
|
190
|
+
throw new SessionError('No matching sessions found for message');
|
|
191
|
+
}
|
|
192
|
+
async doDecryptWhisperMessage(messageBuffer, session) {
|
|
193
|
+
assertUint8(messageBuffer);
|
|
194
|
+
if (!messageBuffer[0])
|
|
195
|
+
throw new Error('Invalid WhisperMessage');
|
|
196
|
+
const versions = this._decodeTupleByte(messageBuffer[0]);
|
|
197
|
+
if (versions[1] > PROTOCOL_VERSION || versions[0] < PROTOCOL_VERSION) {
|
|
198
|
+
throw new Error('Incompatible version number on WhisperMessage');
|
|
199
|
+
}
|
|
200
|
+
const messageEnd = messageBuffer.byteLength - 8;
|
|
201
|
+
const messageProto = messageBuffer.subarray(1, messageEnd);
|
|
202
|
+
const message = WhisperMessageEncoder.decodeWhisperMessage(messageProto);
|
|
203
|
+
await this.maybeStepRatchet(session, message.ephemeralKey, message.previousCounter);
|
|
204
|
+
const chain = session.getChain(message.ephemeralKey);
|
|
205
|
+
if (!chain || chain.chainType === ChainType.SENDING) {
|
|
206
|
+
throw new Error('Tried to decrypt on a sending chain');
|
|
207
|
+
}
|
|
208
|
+
this.fillMessageKeys(chain, message.counter);
|
|
209
|
+
if (!Object.prototype.hasOwnProperty.call(chain.messageKeys, message.counter)) {
|
|
210
|
+
throw new MessageCounterError('Key used already or never filled');
|
|
211
|
+
}
|
|
212
|
+
const messageKey = chain.messageKeys[message.counter];
|
|
213
|
+
delete chain.messageKeys[message.counter];
|
|
214
|
+
const keys = this.deriveSecrets(messageKey, new Uint8Array(32), new TextEncoder().encode('WhisperMessageKeys'));
|
|
215
|
+
const ourIdentity = await this.storage.getOurIdentity();
|
|
216
|
+
const macInput = new Uint8Array(messageEnd + 67);
|
|
217
|
+
macInput.set(session.indexInfo.remoteIdentityKey);
|
|
218
|
+
macInput.set(ourIdentity.pubKey, 33);
|
|
219
|
+
macInput[66] = this._encodeTupleByte(PROTOCOL_VERSION, PROTOCOL_VERSION);
|
|
220
|
+
macInput.set(messageProto, 67);
|
|
221
|
+
const mac = messageBuffer.subarray(-8);
|
|
222
|
+
this.verifyMAC(macInput, keys[1], mac, 8);
|
|
223
|
+
const plaintext = await crypto.decrypt(keys[0], {
|
|
224
|
+
ciphertext: message.ciphertext,
|
|
225
|
+
iv: keys[2].subarray(0, 16),
|
|
226
|
+
tag: keys[2].subarray(16, 32)
|
|
227
|
+
});
|
|
228
|
+
delete session.pendingPreKey;
|
|
229
|
+
return plaintext;
|
|
230
|
+
}
|
|
231
|
+
fillMessageKeys(chain, targetCounter) {
|
|
232
|
+
if (chain.chainKey.counter >= targetCounter)
|
|
233
|
+
return;
|
|
234
|
+
if (targetCounter - chain.chainKey.counter > 2000) {
|
|
235
|
+
throw new SessionError('Over 2000 messages into the future!');
|
|
236
|
+
}
|
|
237
|
+
if (chain.chainKey.key === undefined) {
|
|
238
|
+
throw new SessionError('Chain closed');
|
|
239
|
+
}
|
|
240
|
+
let key = chain.chainKey.key;
|
|
241
|
+
let counter = chain.chainKey.counter;
|
|
242
|
+
while (counter < targetCounter) {
|
|
243
|
+
counter++;
|
|
244
|
+
chain.messageKeys[counter] = crypto.hmacSha256(key, new Uint8Array([1]));
|
|
245
|
+
key = crypto.hmacSha256(key, new Uint8Array([2]));
|
|
246
|
+
}
|
|
247
|
+
chain.chainKey.key = key;
|
|
248
|
+
chain.chainKey.counter = counter;
|
|
249
|
+
}
|
|
250
|
+
async maybeStepRatchet(session, remoteKey, previousCounter) {
|
|
251
|
+
if (session.getChain(remoteKey))
|
|
252
|
+
return;
|
|
253
|
+
const ratchet = session.currentRatchet;
|
|
254
|
+
const previousRatchet = session.getChain(ratchet.lastRemoteEphemeralKey);
|
|
255
|
+
if (previousRatchet) {
|
|
256
|
+
this.fillMessageKeys(previousRatchet, previousCounter);
|
|
257
|
+
previousRatchet.chainKey.key = undefined;
|
|
258
|
+
}
|
|
259
|
+
this.calculateRatchet(session, remoteKey, false);
|
|
260
|
+
const prevChain = session.getChain(ratchet.ephemeralKeyPair.pubKey);
|
|
261
|
+
if (prevChain) {
|
|
262
|
+
ratchet.previousCounter = prevChain.chainKey.counter;
|
|
263
|
+
session.deleteChain(ratchet.ephemeralKeyPair.pubKey);
|
|
264
|
+
}
|
|
265
|
+
const newKp = await signalCrypto.generateDHKeyPair();
|
|
266
|
+
ratchet.ephemeralKeyPair = {
|
|
267
|
+
pubKey: newKp.publicKey,
|
|
268
|
+
privKey: newKp.privateKey
|
|
269
|
+
};
|
|
270
|
+
this.calculateRatchet(session, remoteKey, true);
|
|
271
|
+
ratchet.lastRemoteEphemeralKey = remoteKey;
|
|
272
|
+
}
|
|
273
|
+
calculateRatchet(session, remoteKey, sending) {
|
|
274
|
+
const ratchet = session.currentRatchet;
|
|
275
|
+
const sharedSecret = signalCrypto.calculateAgreement(remoteKey, ratchet.ephemeralKeyPair.privKey);
|
|
276
|
+
const masterKeys = this.deriveSecrets(sharedSecret, ratchet.rootKey, new TextEncoder().encode('WhisperRatchet'), 2);
|
|
277
|
+
const chainKey = sending ? ratchet.ephemeralKeyPair.pubKey : remoteKey;
|
|
278
|
+
session.addChain(chainKey, {
|
|
279
|
+
messageKeys: {},
|
|
280
|
+
chainKey: { counter: -1, key: masterKeys[1] },
|
|
281
|
+
chainType: sending ? ChainType.SENDING : ChainType.RECEIVING
|
|
282
|
+
});
|
|
283
|
+
ratchet.rootKey = masterKeys[0];
|
|
284
|
+
}
|
|
285
|
+
deriveSecrets(input, salt, info, chunks = 3) {
|
|
286
|
+
const hkdf = crypto.hkdf(input, salt, info, { length: chunks * 32 });
|
|
287
|
+
const result = [];
|
|
288
|
+
for (let i = 0; i < chunks; i++) {
|
|
289
|
+
result.push(hkdf.subarray(i * 32, (i + 1) * 32));
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
verifyMAC(macInput, key, mac, length) {
|
|
294
|
+
const computed = crypto.hmacSha256(key, macInput);
|
|
295
|
+
for (let i = 0; i < length; i++) {
|
|
296
|
+
if (computed[i] !== mac[i])
|
|
297
|
+
throw new Error('MAC verification failed');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SessionEntry } from "../record/session-entry.js";
|
|
2
|
+
import type { SessionRecord } from "../record/session-record.js";
|
|
3
|
+
export interface EncryptResult {
|
|
4
|
+
type: number;
|
|
5
|
+
body: Uint8Array;
|
|
6
|
+
registrationId: number;
|
|
7
|
+
}
|
|
8
|
+
export interface DecryptResult {
|
|
9
|
+
plaintext: Uint8Array;
|
|
10
|
+
}
|
|
11
|
+
export interface DecryptWithSessionResult {
|
|
12
|
+
session: SessionEntry;
|
|
13
|
+
plaintext: Uint8Array;
|
|
14
|
+
}
|
|
15
|
+
export interface WhisperMessageProto {
|
|
16
|
+
ephemeralKey: Uint8Array;
|
|
17
|
+
counter: number;
|
|
18
|
+
previousCounter: number;
|
|
19
|
+
ciphertext: Uint8Array;
|
|
20
|
+
}
|
|
21
|
+
export interface PreKeyWhisperMessageProto {
|
|
22
|
+
identityKey: Uint8Array;
|
|
23
|
+
registrationId: number;
|
|
24
|
+
baseKey: Uint8Array;
|
|
25
|
+
signedPreKeyId: number;
|
|
26
|
+
preKeyId?: number;
|
|
27
|
+
message: Uint8Array;
|
|
28
|
+
}
|
|
29
|
+
export interface SessionCipherStorage {
|
|
30
|
+
loadSession(addressName: string): Promise<SessionRecord | undefined>;
|
|
31
|
+
storeSession(addressName: string, record: SessionRecord): Promise<void>;
|
|
32
|
+
getOurIdentity(): Promise<{
|
|
33
|
+
pubKey: Uint8Array;
|
|
34
|
+
privKey: Uint8Array;
|
|
35
|
+
}>;
|
|
36
|
+
isTrustedIdentity(addressName: string, identityKey: Uint8Array): Promise<boolean>;
|
|
37
|
+
getOurRegistrationId(): Promise<number>;
|
|
38
|
+
loadPreKey(preKeyId: number): Promise<{
|
|
39
|
+
pubKey: Uint8Array;
|
|
40
|
+
privKey: Uint8Array;
|
|
41
|
+
} | undefined>;
|
|
42
|
+
loadSignedPreKey(signedPreKeyId: number): Promise<{
|
|
43
|
+
pubKey: Uint8Array;
|
|
44
|
+
privKey: Uint8Array;
|
|
45
|
+
} | undefined>;
|
|
46
|
+
removePreKey(preKeyId: number): Promise<void>;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ChainState, CurrentRatchet, IndexInfo, PendingPreKey, SerializedSessionEntry } from './types.js';
|
|
2
|
+
export declare class SessionEntry {
|
|
3
|
+
registrationId: number;
|
|
4
|
+
currentRatchet: CurrentRatchet;
|
|
5
|
+
indexInfo: IndexInfo;
|
|
6
|
+
pendingPreKey?: PendingPreKey;
|
|
7
|
+
private _chains;
|
|
8
|
+
toString(): string;
|
|
9
|
+
inspect(): string;
|
|
10
|
+
addChain(key: Uint8Array, value: ChainState): void;
|
|
11
|
+
getChain(key: Uint8Array): ChainState | undefined;
|
|
12
|
+
deleteChain(key: Uint8Array): void;
|
|
13
|
+
chains(): Generator<[Uint8Array, ChainState]>;
|
|
14
|
+
private serializeChains;
|
|
15
|
+
private serializePendingPreKey;
|
|
16
|
+
serialize(): SerializedSessionEntry;
|
|
17
|
+
private static deserializeChains;
|
|
18
|
+
private static deserializePendingPreKey;
|
|
19
|
+
static deserialize(data: SerializedSessionEntry): SessionEntry;
|
|
20
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { assertUint8, toBase64, u8 } from '../utils.js';
|
|
2
|
+
export class SessionEntry {
|
|
3
|
+
registrationId;
|
|
4
|
+
currentRatchet;
|
|
5
|
+
indexInfo;
|
|
6
|
+
pendingPreKey;
|
|
7
|
+
_chains = new Map();
|
|
8
|
+
toString() {
|
|
9
|
+
const baseKey = this.indexInfo?.baseKey ? toBase64(this.indexInfo.baseKey) : '—';
|
|
10
|
+
return `<SessionEntry [baseKey=${baseKey}]>`;
|
|
11
|
+
}
|
|
12
|
+
inspect() {
|
|
13
|
+
return this.toString();
|
|
14
|
+
}
|
|
15
|
+
addChain(key, value) {
|
|
16
|
+
assertUint8(key);
|
|
17
|
+
const id = toBase64(key);
|
|
18
|
+
if (this._chains.has(id))
|
|
19
|
+
throw new Error('Overwrite attempt');
|
|
20
|
+
this._chains.set(id, value);
|
|
21
|
+
}
|
|
22
|
+
getChain(key) {
|
|
23
|
+
assertUint8(key);
|
|
24
|
+
return this._chains.get(toBase64(key));
|
|
25
|
+
}
|
|
26
|
+
deleteChain(key) {
|
|
27
|
+
assertUint8(key);
|
|
28
|
+
const id = toBase64(key);
|
|
29
|
+
if (!this._chains.has(id))
|
|
30
|
+
throw new ReferenceError('Not Found');
|
|
31
|
+
this._chains.delete(id);
|
|
32
|
+
}
|
|
33
|
+
*chains() {
|
|
34
|
+
for (const [k, v] of this._chains) {
|
|
35
|
+
yield [fromBase64(k), v];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
serializeChains() {
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const [key, c] of this._chains) {
|
|
41
|
+
if (!c)
|
|
42
|
+
continue;
|
|
43
|
+
const messageKeys = {};
|
|
44
|
+
for (const [idx, mk] of Object.entries(c.messageKeys)) {
|
|
45
|
+
messageKeys[idx] = u8.encode(mk);
|
|
46
|
+
}
|
|
47
|
+
result[key] = {
|
|
48
|
+
chainKey: {
|
|
49
|
+
counter: c.chainKey.counter,
|
|
50
|
+
key: u8.encode(c.chainKey.key),
|
|
51
|
+
},
|
|
52
|
+
chainType: c.chainType,
|
|
53
|
+
messageKeys,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
serializePendingPreKey(ppk) {
|
|
59
|
+
return {
|
|
60
|
+
baseKey: u8.encode(ppk.baseKey),
|
|
61
|
+
signedKeyId: ppk.signedKeyId,
|
|
62
|
+
...(ppk.preKeyId !== undefined && { preKeyId: ppk.preKeyId }),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
serialize() {
|
|
66
|
+
const data = {
|
|
67
|
+
registrationId: this.registrationId,
|
|
68
|
+
currentRatchet: {
|
|
69
|
+
ephemeralKeyPair: {
|
|
70
|
+
pubKey: u8.encode(this.currentRatchet.ephemeralKeyPair.pubKey),
|
|
71
|
+
privKey: u8.encode(this.currentRatchet.ephemeralKeyPair.privKey),
|
|
72
|
+
},
|
|
73
|
+
lastRemoteEphemeralKey: u8.encode(this.currentRatchet.lastRemoteEphemeralKey),
|
|
74
|
+
previousCounter: this.currentRatchet.previousCounter,
|
|
75
|
+
rootKey: u8.encode(this.currentRatchet.rootKey),
|
|
76
|
+
},
|
|
77
|
+
indexInfo: {
|
|
78
|
+
baseKey: u8.encode(this.indexInfo.baseKey),
|
|
79
|
+
baseKeyType: this.indexInfo.baseKeyType,
|
|
80
|
+
closed: this.indexInfo.closed,
|
|
81
|
+
used: this.indexInfo.used,
|
|
82
|
+
created: this.indexInfo.created,
|
|
83
|
+
remoteIdentityKey: u8.encode(this.indexInfo.remoteIdentityKey),
|
|
84
|
+
},
|
|
85
|
+
_chains: this.serializeChains(),
|
|
86
|
+
};
|
|
87
|
+
if (this.pendingPreKey) {
|
|
88
|
+
data.pendingPreKey = this.serializePendingPreKey(this.pendingPreKey);
|
|
89
|
+
}
|
|
90
|
+
return data;
|
|
91
|
+
}
|
|
92
|
+
static deserializeChains(chains) {
|
|
93
|
+
const result = new Map();
|
|
94
|
+
for (const [key, c] of Object.entries(chains)) {
|
|
95
|
+
if (!c)
|
|
96
|
+
continue;
|
|
97
|
+
const messageKeys = {};
|
|
98
|
+
for (const [idx, mk] of Object.entries(c.messageKeys)) {
|
|
99
|
+
messageKeys[idx] = u8.decode(mk);
|
|
100
|
+
}
|
|
101
|
+
result.set(key, {
|
|
102
|
+
chainKey: {
|
|
103
|
+
counter: c.chainKey.counter,
|
|
104
|
+
key: u8.decode(c.chainKey.key),
|
|
105
|
+
},
|
|
106
|
+
chainType: c.chainType,
|
|
107
|
+
messageKeys,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
static deserializePendingPreKey(data) {
|
|
113
|
+
return {
|
|
114
|
+
baseKey: u8.decode(data.baseKey),
|
|
115
|
+
signedKeyId: data.signedKeyId,
|
|
116
|
+
preKeyId: data.preKeyId,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
static deserialize(data) {
|
|
120
|
+
const obj = new SessionEntry();
|
|
121
|
+
obj.registrationId = data.registrationId;
|
|
122
|
+
obj.currentRatchet = {
|
|
123
|
+
ephemeralKeyPair: {
|
|
124
|
+
pubKey: u8.decode(data.currentRatchet.ephemeralKeyPair.pubKey),
|
|
125
|
+
privKey: u8.decode(data.currentRatchet.ephemeralKeyPair.privKey),
|
|
126
|
+
},
|
|
127
|
+
lastRemoteEphemeralKey: u8.decode(data.currentRatchet.lastRemoteEphemeralKey),
|
|
128
|
+
previousCounter: data.currentRatchet.previousCounter,
|
|
129
|
+
rootKey: u8.decode(data.currentRatchet.rootKey),
|
|
130
|
+
};
|
|
131
|
+
obj.indexInfo = {
|
|
132
|
+
baseKey: u8.decode(data.indexInfo.baseKey),
|
|
133
|
+
baseKeyType: data.indexInfo.baseKeyType,
|
|
134
|
+
closed: data.indexInfo.closed,
|
|
135
|
+
used: data.indexInfo.used,
|
|
136
|
+
created: data.indexInfo.created,
|
|
137
|
+
remoteIdentityKey: u8.decode(data.indexInfo.remoteIdentityKey),
|
|
138
|
+
};
|
|
139
|
+
obj._chains = this.deserializeChains(data._chains);
|
|
140
|
+
if (data.pendingPreKey) {
|
|
141
|
+
obj.pendingPreKey = this.deserializePendingPreKey(data.pendingPreKey);
|
|
142
|
+
}
|
|
143
|
+
return obj;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
import { fromBase64 } from '../utils.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SerializedSessionRecord } from './types.js';
|
|
2
|
+
import { SessionEntry } from './session-entry.js';
|
|
3
|
+
export declare class SessionRecord {
|
|
4
|
+
sessions: Record<string, SessionEntry>;
|
|
5
|
+
version: string;
|
|
6
|
+
private _sortedSessions;
|
|
7
|
+
static createEntry(): SessionEntry;
|
|
8
|
+
static deserialize(data: SerializedSessionRecord): SessionRecord;
|
|
9
|
+
serialize(): SerializedSessionRecord;
|
|
10
|
+
private invalidateCache;
|
|
11
|
+
haveOpenSession(): boolean;
|
|
12
|
+
getSession(key: Uint8Array): SessionEntry | undefined;
|
|
13
|
+
getOpenSession(): SessionEntry | undefined;
|
|
14
|
+
setSession(session: SessionEntry): void;
|
|
15
|
+
getSessions(): SessionEntry[];
|
|
16
|
+
closeSession(session: SessionEntry): void;
|
|
17
|
+
openSession(session: SessionEntry): void;
|
|
18
|
+
isClosed(session: SessionEntry): boolean;
|
|
19
|
+
removeOldSessions(): void;
|
|
20
|
+
deleteAllSessions(): void;
|
|
21
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { SessionEntry } from './session-entry.js';
|
|
2
|
+
import { CLOSED_SESSIONS_MAX, SESSION_RECORD_VERSION } from '../constants.js';
|
|
3
|
+
import { assertUint8, toBase64 } from '../utils.js';
|
|
4
|
+
import { BaseKeyType } from '../../base_key_type.js';
|
|
5
|
+
export class SessionRecord {
|
|
6
|
+
sessions = {};
|
|
7
|
+
version = SESSION_RECORD_VERSION;
|
|
8
|
+
_sortedSessions = null;
|
|
9
|
+
static createEntry() {
|
|
10
|
+
return new SessionEntry();
|
|
11
|
+
}
|
|
12
|
+
static deserialize(data) {
|
|
13
|
+
const obj = new SessionRecord();
|
|
14
|
+
if (data._sessions) {
|
|
15
|
+
for (const [key, entry] of Object.entries(data._sessions)) {
|
|
16
|
+
obj.sessions[key] = SessionEntry.deserialize(entry);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return obj;
|
|
20
|
+
}
|
|
21
|
+
serialize() {
|
|
22
|
+
const _sessions = {};
|
|
23
|
+
for (const [key, entry] of Object.entries(this.sessions)) {
|
|
24
|
+
_sessions[key] = entry.serialize();
|
|
25
|
+
}
|
|
26
|
+
return { _sessions, version: this.version };
|
|
27
|
+
}
|
|
28
|
+
invalidateCache() {
|
|
29
|
+
this._sortedSessions = null;
|
|
30
|
+
}
|
|
31
|
+
haveOpenSession() {
|
|
32
|
+
const open = this.getOpenSession();
|
|
33
|
+
return !!open && typeof open.registrationId === 'number';
|
|
34
|
+
}
|
|
35
|
+
getSession(key) {
|
|
36
|
+
assertUint8(key);
|
|
37
|
+
const session = this.sessions[toBase64(key)];
|
|
38
|
+
if (session?.indexInfo.baseKeyType === BaseKeyType.OURS) {
|
|
39
|
+
throw new Error('Tried to lookup a session using our basekey');
|
|
40
|
+
}
|
|
41
|
+
return session;
|
|
42
|
+
}
|
|
43
|
+
getOpenSession() {
|
|
44
|
+
for (const session of Object.values(this.sessions)) {
|
|
45
|
+
if (session.indexInfo.closed === -1)
|
|
46
|
+
return session;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
setSession(session) {
|
|
50
|
+
this.sessions[toBase64(session.indexInfo.baseKey)] = session;
|
|
51
|
+
this.invalidateCache();
|
|
52
|
+
}
|
|
53
|
+
getSessions() {
|
|
54
|
+
if (!this._sortedSessions) {
|
|
55
|
+
this._sortedSessions = Object.values(this.sessions).sort((a, b) => {
|
|
56
|
+
const aUsed = a.indexInfo.used || 0;
|
|
57
|
+
const bUsed = b.indexInfo.used || 0;
|
|
58
|
+
return aUsed === bUsed ? 0 : aUsed < bUsed ? 1 : -1;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return this._sortedSessions;
|
|
62
|
+
}
|
|
63
|
+
closeSession(session) {
|
|
64
|
+
if (session.indexInfo.closed !== -1)
|
|
65
|
+
return;
|
|
66
|
+
session.indexInfo.closed = Date.now();
|
|
67
|
+
}
|
|
68
|
+
openSession(session) {
|
|
69
|
+
session.indexInfo.closed = -1;
|
|
70
|
+
}
|
|
71
|
+
isClosed(session) {
|
|
72
|
+
return session.indexInfo.closed !== -1;
|
|
73
|
+
}
|
|
74
|
+
removeOldSessions() {
|
|
75
|
+
const total = Object.keys(this.sessions).length;
|
|
76
|
+
if (total <= CLOSED_SESSIONS_MAX)
|
|
77
|
+
return;
|
|
78
|
+
const closed = [];
|
|
79
|
+
for (const [key, session] of Object.entries(this.sessions)) {
|
|
80
|
+
if (session.indexInfo.closed !== -1) {
|
|
81
|
+
closed.push({ key, closed: session.indexInfo.closed });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
closed.sort((a, b) => a.closed - b.closed);
|
|
85
|
+
const toRemove = total - CLOSED_SESSIONS_MAX;
|
|
86
|
+
for (let i = 0; i < toRemove && i < closed.length; i++) {
|
|
87
|
+
delete this.sessions[closed[i].key];
|
|
88
|
+
}
|
|
89
|
+
this.invalidateCache();
|
|
90
|
+
}
|
|
91
|
+
deleteAllSessions() {
|
|
92
|
+
this.sessions = {};
|
|
93
|
+
this.invalidateCache();
|
|
94
|
+
}
|
|
95
|
+
}
|