@mikudev/libsignal-node 2.0.3
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 +31 -0
- package/install.js +495 -0
- package/package.json +29 -0
- package/src/.eslintrc.json +31 -0
- package/src/WhisperTextProtocol.js +949 -0
- package/src/base_key_type.js +6 -0
- package/src/chain_type.js +6 -0
- package/src/crypto.js +99 -0
- package/src/curve.js +142 -0
- package/src/errors.js +34 -0
- package/src/eslintrc.json +32 -0
- package/src/keyhelper.js +46 -0
- package/src/numeric_fingerprint.js +71 -0
- package/src/protobufs.js +11 -0
- package/src/protocol_address.js +41 -0
- package/src/queue_job.js +71 -0
- package/src/session_builder.js +164 -0
- package/src/session_cipher.js +332 -0
- package/src/session_record.js +325 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const BaseKeyType = require('./base_key_type');
|
|
5
|
+
const ChainType = require('./chain_type');
|
|
6
|
+
const SessionRecord = require('./session_record');
|
|
7
|
+
const crypto = require('./crypto');
|
|
8
|
+
const curve = require('./curve');
|
|
9
|
+
const errors = require('./errors');
|
|
10
|
+
const queueJob = require('./queue_job');
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SessionBuilder {
|
|
14
|
+
|
|
15
|
+
constructor(storage, protocolAddress) {
|
|
16
|
+
this.addr = protocolAddress;
|
|
17
|
+
this.storage = storage;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async initOutgoing(device) {
|
|
21
|
+
const fqAddr = this.addr.toString();
|
|
22
|
+
return await queueJob(fqAddr, async () => {
|
|
23
|
+
if (!await this.storage.isTrustedIdentity(this.addr.id, device.identityKey)) {
|
|
24
|
+
throw new errors.UntrustedIdentityKeyError(this.addr.id, device.identityKey);
|
|
25
|
+
}
|
|
26
|
+
curve.verifySignature(device.identityKey, device.signedPreKey.publicKey,
|
|
27
|
+
device.signedPreKey.signature, true);
|
|
28
|
+
const baseKey = curve.generateKeyPair();
|
|
29
|
+
const devicePreKey = device.preKey && device.preKey.publicKey;
|
|
30
|
+
const session = await this.initSession(true, baseKey, undefined, device.identityKey,
|
|
31
|
+
devicePreKey, device.signedPreKey.publicKey,
|
|
32
|
+
device.registrationId);
|
|
33
|
+
session.pendingPreKey = {
|
|
34
|
+
signedKeyId: device.signedPreKey.keyId,
|
|
35
|
+
baseKey: baseKey.pubKey
|
|
36
|
+
};
|
|
37
|
+
if (device.preKey) {
|
|
38
|
+
session.pendingPreKey.preKeyId = device.preKey.keyId;
|
|
39
|
+
}
|
|
40
|
+
let record = await this.storage.loadSession(fqAddr);
|
|
41
|
+
if (!record) {
|
|
42
|
+
record = new SessionRecord();
|
|
43
|
+
} else {
|
|
44
|
+
const openSession = record.getOpenSession();
|
|
45
|
+
if (openSession) {
|
|
46
|
+
//console.warn("Closing stale open session for new outgoing prekey bundle");
|
|
47
|
+
record.closeSession(openSession);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
record.setSession(session);
|
|
51
|
+
await this.storage.storeSession(fqAddr, record);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async initIncoming(record, message) {
|
|
56
|
+
const fqAddr = this.addr.toString();
|
|
57
|
+
if (!await this.storage.isTrustedIdentity(fqAddr, message.identityKey)) {
|
|
58
|
+
throw new errors.UntrustedIdentityKeyError(this.addr.id, message.identityKey);
|
|
59
|
+
}
|
|
60
|
+
if (record.getSession(message.baseKey)) {
|
|
61
|
+
// This just means we haven't replied.
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const preKeyPair = await this.storage.loadPreKey(message.preKeyId);
|
|
65
|
+
if (message.preKeyId && !preKeyPair) {
|
|
66
|
+
throw new errors.PreKeyError('Invalid PreKey ID');
|
|
67
|
+
}
|
|
68
|
+
const signedPreKeyPair = await this.storage.loadSignedPreKey(message.signedPreKeyId);
|
|
69
|
+
if (!signedPreKeyPair) {
|
|
70
|
+
throw new errors.PreKeyError("Missing SignedPreKey");
|
|
71
|
+
}
|
|
72
|
+
const existingOpenSession = record.getOpenSession();
|
|
73
|
+
if (existingOpenSession) {
|
|
74
|
+
//console.warn("Closing open session in favor of incoming prekey bundle");
|
|
75
|
+
record.closeSession(existingOpenSession);
|
|
76
|
+
}
|
|
77
|
+
record.setSession(await this.initSession(false, preKeyPair, signedPreKeyPair,
|
|
78
|
+
message.identityKey, message.baseKey,
|
|
79
|
+
undefined, message.registrationId));
|
|
80
|
+
return message.preKeyId;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async initSession(isInitiator, ourEphemeralKey, ourSignedKey, theirIdentityPubKey,
|
|
84
|
+
theirEphemeralPubKey, theirSignedPubKey, registrationId) {
|
|
85
|
+
if (isInitiator) {
|
|
86
|
+
if (ourSignedKey) {
|
|
87
|
+
throw new Error("Invalid call to initSession");
|
|
88
|
+
}
|
|
89
|
+
ourSignedKey = ourEphemeralKey;
|
|
90
|
+
} else {
|
|
91
|
+
if (theirSignedPubKey) {
|
|
92
|
+
throw new Error("Invalid call to initSession");
|
|
93
|
+
}
|
|
94
|
+
theirSignedPubKey = theirEphemeralPubKey;
|
|
95
|
+
}
|
|
96
|
+
let sharedSecret;
|
|
97
|
+
if (!ourEphemeralKey || !theirEphemeralPubKey) {
|
|
98
|
+
sharedSecret = new Uint8Array(32 * 4);
|
|
99
|
+
} else {
|
|
100
|
+
sharedSecret = new Uint8Array(32 * 5);
|
|
101
|
+
}
|
|
102
|
+
for (var i = 0; i < 32; i++) {
|
|
103
|
+
sharedSecret[i] = 0xff;
|
|
104
|
+
}
|
|
105
|
+
const ourIdentityKey = await this.storage.getOurIdentity();
|
|
106
|
+
const a1 = curve.calculateAgreement(theirSignedPubKey, ourIdentityKey.privKey);
|
|
107
|
+
const a2 = curve.calculateAgreement(theirIdentityPubKey, ourSignedKey.privKey);
|
|
108
|
+
const a3 = curve.calculateAgreement(theirSignedPubKey, ourSignedKey.privKey);
|
|
109
|
+
if (isInitiator) {
|
|
110
|
+
sharedSecret.set(new Uint8Array(a1), 32);
|
|
111
|
+
sharedSecret.set(new Uint8Array(a2), 32 * 2);
|
|
112
|
+
} else {
|
|
113
|
+
sharedSecret.set(new Uint8Array(a1), 32 * 2);
|
|
114
|
+
sharedSecret.set(new Uint8Array(a2), 32);
|
|
115
|
+
}
|
|
116
|
+
sharedSecret.set(new Uint8Array(a3), 32 * 3);
|
|
117
|
+
if (ourEphemeralKey && theirEphemeralPubKey) {
|
|
118
|
+
const a4 = curve.calculateAgreement(theirEphemeralPubKey, ourEphemeralKey.privKey);
|
|
119
|
+
sharedSecret.set(new Uint8Array(a4), 32 * 4);
|
|
120
|
+
}
|
|
121
|
+
const masterKey = crypto.deriveSecrets(Buffer.from(sharedSecret), Buffer.alloc(32),
|
|
122
|
+
Buffer.from("WhisperText"));
|
|
123
|
+
const session = SessionRecord.createEntry();
|
|
124
|
+
session.registrationId = registrationId;
|
|
125
|
+
session.currentRatchet = {
|
|
126
|
+
rootKey: masterKey[0],
|
|
127
|
+
ephemeralKeyPair: isInitiator ? curve.generateKeyPair() : ourSignedKey,
|
|
128
|
+
lastRemoteEphemeralKey: theirSignedPubKey,
|
|
129
|
+
previousCounter: 0
|
|
130
|
+
};
|
|
131
|
+
session.indexInfo = {
|
|
132
|
+
created: Date.now(),
|
|
133
|
+
used: Date.now(),
|
|
134
|
+
remoteIdentityKey: theirIdentityPubKey,
|
|
135
|
+
baseKey: isInitiator ? ourEphemeralKey.pubKey : theirEphemeralPubKey,
|
|
136
|
+
baseKeyType: isInitiator ? BaseKeyType.OURS : BaseKeyType.THEIRS,
|
|
137
|
+
closed: -1
|
|
138
|
+
};
|
|
139
|
+
if (isInitiator) {
|
|
140
|
+
// If we're initiating we go ahead and set our first sending ephemeral key now,
|
|
141
|
+
// otherwise we figure it out when we first maybeStepRatchet with the remote's
|
|
142
|
+
// ephemeral key
|
|
143
|
+
this.calculateSendingRatchet(session, theirSignedPubKey);
|
|
144
|
+
}
|
|
145
|
+
return session;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
calculateSendingRatchet(session, remoteKey) {
|
|
149
|
+
const ratchet = session.currentRatchet;
|
|
150
|
+
const sharedSecret = curve.calculateAgreement(remoteKey, ratchet.ephemeralKeyPair.privKey);
|
|
151
|
+
const masterKey = crypto.deriveSecrets(sharedSecret, ratchet.rootKey, Buffer.from("WhisperRatchet"));
|
|
152
|
+
session.addChain(ratchet.ephemeralKeyPair.pubKey, {
|
|
153
|
+
messageKeys: {},
|
|
154
|
+
chainKey: {
|
|
155
|
+
counter: -1,
|
|
156
|
+
key: masterKey[1]
|
|
157
|
+
},
|
|
158
|
+
chainType: ChainType.SENDING
|
|
159
|
+
});
|
|
160
|
+
ratchet.rootKey = masterKey[0];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = SessionBuilder;
|
|
@@ -0,0 +1,332 @@
|
|
|
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
|
+
throw new errors.SessionError("No matching sessions found for message");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async decryptWhisperMessage(data) {
|
|
161
|
+
assertBuffer(data);
|
|
162
|
+
return await this.queueJob(async () => {
|
|
163
|
+
const record = await this.getRecord();
|
|
164
|
+
if (!record) {
|
|
165
|
+
throw new errors.SessionError("No session record");
|
|
166
|
+
}
|
|
167
|
+
const result = await this.decryptWithSessions(data, record.getSessions());
|
|
168
|
+
const remoteIdentityKey = result.session.indexInfo.remoteIdentityKey;
|
|
169
|
+
if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) {
|
|
170
|
+
throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey);
|
|
171
|
+
}
|
|
172
|
+
if (record.isClosed(result.session)) {
|
|
173
|
+
// It's possible for this to happen when processing a backlog of messages.
|
|
174
|
+
// The message was, hopefully, just sent back in a time when this session
|
|
175
|
+
// was the most current. Simply make a note of it and continue. If our
|
|
176
|
+
// actual open session is for reason invalid, that must be handled via
|
|
177
|
+
// a full SessionError response.
|
|
178
|
+
//console.warn("Decrypted message with closed session.");
|
|
179
|
+
}
|
|
180
|
+
await this.storeRecord(record);
|
|
181
|
+
return result.plaintext;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async decryptPreKeyWhisperMessage(data) {
|
|
186
|
+
assertBuffer(data);
|
|
187
|
+
const versions = this._decodeTupleByte(data[0]);
|
|
188
|
+
if (versions[1] > 3 || versions[0] < 3) { // min version > 3 or max version < 3
|
|
189
|
+
throw new Error("Incompatible version number on PreKeyWhisperMessage");
|
|
190
|
+
}
|
|
191
|
+
return await this.queueJob(async () => {
|
|
192
|
+
let record = await this.getRecord();
|
|
193
|
+
const preKeyProto = protobufs.PreKeyWhisperMessage.decode(data.slice(1));
|
|
194
|
+
if (!record) {
|
|
195
|
+
if (preKeyProto.registrationId == null) {
|
|
196
|
+
throw new Error("No registrationId");
|
|
197
|
+
}
|
|
198
|
+
record = new SessionRecord();
|
|
199
|
+
}
|
|
200
|
+
const builder = new SessionBuilder(this.storage, this.addr);
|
|
201
|
+
const preKeyId = await builder.initIncoming(record, preKeyProto);
|
|
202
|
+
const session = record.getSession(preKeyProto.baseKey);
|
|
203
|
+
const plaintext = await this.doDecryptWhisperMessage(preKeyProto.message, session);
|
|
204
|
+
await this.storeRecord(record);
|
|
205
|
+
if (preKeyId) {
|
|
206
|
+
await this.storage.removePreKey(preKeyId);
|
|
207
|
+
}
|
|
208
|
+
return plaintext;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async doDecryptWhisperMessage(messageBuffer, session) {
|
|
213
|
+
assertBuffer(messageBuffer);
|
|
214
|
+
if (!session) {
|
|
215
|
+
throw new TypeError("session required");
|
|
216
|
+
}
|
|
217
|
+
const versions = this._decodeTupleByte(messageBuffer[0]);
|
|
218
|
+
if (versions[1] > 3 || versions[0] < 3) { // min version > 3 or max version < 3
|
|
219
|
+
throw new Error("Incompatible version number on WhisperMessage");
|
|
220
|
+
}
|
|
221
|
+
const messageProto = messageBuffer.slice(1, -8);
|
|
222
|
+
const message = protobufs.WhisperMessage.decode(messageProto);
|
|
223
|
+
this.maybeStepRatchet(session, message.ephemeralKey, message.previousCounter);
|
|
224
|
+
const chain = session.getChain(message.ephemeralKey);
|
|
225
|
+
if (chain.chainType === ChainType.SENDING) {
|
|
226
|
+
throw new Error("Tried to decrypt on a sending chain");
|
|
227
|
+
}
|
|
228
|
+
this.fillMessageKeys(chain, message.counter);
|
|
229
|
+
if (!chain.messageKeys.hasOwnProperty(message.counter)) {
|
|
230
|
+
// Most likely the message was already decrypted and we are trying to process
|
|
231
|
+
// twice. This can happen if the user restarts before the server gets an ACK.
|
|
232
|
+
throw new errors.MessageCounterError('Key used already or never filled');
|
|
233
|
+
}
|
|
234
|
+
const messageKey = chain.messageKeys[message.counter];
|
|
235
|
+
delete chain.messageKeys[message.counter];
|
|
236
|
+
const keys = crypto.deriveSecrets(messageKey, Buffer.alloc(32),
|
|
237
|
+
Buffer.from("WhisperMessageKeys"));
|
|
238
|
+
const ourIdentityKey = await this.storage.getOurIdentity();
|
|
239
|
+
const macInput = Buffer.alloc(messageProto.byteLength + (33 * 2) + 1);
|
|
240
|
+
macInput.set(session.indexInfo.remoteIdentityKey);
|
|
241
|
+
macInput.set(ourIdentityKey.pubKey, 33);
|
|
242
|
+
macInput[33 * 2] = this._encodeTupleByte(VERSION, VERSION);
|
|
243
|
+
macInput.set(messageProto, (33 * 2) + 1);
|
|
244
|
+
// This is where we most likely fail if the session is not a match.
|
|
245
|
+
// Don't misinterpret this as corruption.
|
|
246
|
+
crypto.verifyMAC(macInput, keys[1], messageBuffer.slice(-8), 8);
|
|
247
|
+
const plaintext = crypto.decrypt(keys[0], message.ciphertext, keys[2].slice(0, 16));
|
|
248
|
+
delete session.pendingPreKey;
|
|
249
|
+
return plaintext;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fillMessageKeys(chain, counter) {
|
|
253
|
+
if (chain.chainKey.counter >= counter) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (counter - chain.chainKey.counter > 2000) {
|
|
257
|
+
throw new errors.SessionError('Over 2000 messages into the future!');
|
|
258
|
+
}
|
|
259
|
+
if (chain.chainKey.key === undefined) {
|
|
260
|
+
throw new errors.SessionError('Chain closed');
|
|
261
|
+
}
|
|
262
|
+
const key = chain.chainKey.key;
|
|
263
|
+
chain.messageKeys[chain.chainKey.counter + 1] = crypto.calculateMAC(key, Buffer.from([1]));
|
|
264
|
+
chain.chainKey.key = crypto.calculateMAC(key, Buffer.from([2]));
|
|
265
|
+
chain.chainKey.counter += 1;
|
|
266
|
+
return this.fillMessageKeys(chain, counter);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
maybeStepRatchet(session, remoteKey, previousCounter) {
|
|
270
|
+
if (session.getChain(remoteKey)) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const ratchet = session.currentRatchet;
|
|
274
|
+
let previousRatchet = session.getChain(ratchet.lastRemoteEphemeralKey);
|
|
275
|
+
if (previousRatchet) {
|
|
276
|
+
this.fillMessageKeys(previousRatchet, previousCounter);
|
|
277
|
+
delete previousRatchet.chainKey.key; // Close
|
|
278
|
+
}
|
|
279
|
+
this.calculateRatchet(session, remoteKey, false);
|
|
280
|
+
// Now swap the ephemeral key and calculate the new sending chain
|
|
281
|
+
const prevCounter = session.getChain(ratchet.ephemeralKeyPair.pubKey);
|
|
282
|
+
if (prevCounter) {
|
|
283
|
+
ratchet.previousCounter = prevCounter.chainKey.counter;
|
|
284
|
+
session.deleteChain(ratchet.ephemeralKeyPair.pubKey);
|
|
285
|
+
}
|
|
286
|
+
ratchet.ephemeralKeyPair = curve.generateKeyPair();
|
|
287
|
+
this.calculateRatchet(session, remoteKey, true);
|
|
288
|
+
ratchet.lastRemoteEphemeralKey = remoteKey;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
calculateRatchet(session, remoteKey, sending) {
|
|
292
|
+
let ratchet = session.currentRatchet;
|
|
293
|
+
const sharedSecret = curve.calculateAgreement(remoteKey, ratchet.ephemeralKeyPair.privKey);
|
|
294
|
+
const masterKey = crypto.deriveSecrets(sharedSecret, ratchet.rootKey,
|
|
295
|
+
Buffer.from("WhisperRatchet"), /*chunks*/ 2);
|
|
296
|
+
const chainKey = sending ? ratchet.ephemeralKeyPair.pubKey : remoteKey;
|
|
297
|
+
session.addChain(chainKey, {
|
|
298
|
+
messageKeys: {},
|
|
299
|
+
chainKey: {
|
|
300
|
+
counter: -1,
|
|
301
|
+
key: masterKey[1]
|
|
302
|
+
},
|
|
303
|
+
chainType: sending ? ChainType.SENDING : ChainType.RECEIVING
|
|
304
|
+
});
|
|
305
|
+
ratchet.rootKey = masterKey[0];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async hasOpenSession() {
|
|
309
|
+
return await this.queueJob(async () => {
|
|
310
|
+
const record = await this.getRecord();
|
|
311
|
+
if (!record) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
return record.haveOpenSession();
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async closeOpenSession() {
|
|
319
|
+
return await this.queueJob(async () => {
|
|
320
|
+
const record = await this.getRecord();
|
|
321
|
+
if (record) {
|
|
322
|
+
const openSession = record.getOpenSession();
|
|
323
|
+
if (openSession) {
|
|
324
|
+
record.closeSession(openSession);
|
|
325
|
+
await this.storeRecord(record);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = SessionCipher;
|