From 7d4c3ec6cb7f5d9726bad40efc3ac68acaef180c Mon Sep 17 00:00:00 2001 From: BochilGaming <79433517+BochilGaming@users.noreply.github.com> Date: Tue, 31 May 2022 15:54:19 +0700 Subject: [PATCH 1/6] Update, from whatsapp web --- src/session_builder.js | 26 +++++++++++++------------- src/session_cipher.js | 17 +++++++++++++++++ src/session_record.js | 32 ++++++++++++++++++++++++++++++-- src/util.js | 16 ++++++++++++++++ 4 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 src/util.js diff --git a/src/session_builder.js b/src/session_builder.js index 9bd333f1..72dcea5a 100644 --- a/src/session_builder.js +++ b/src/session_builder.js @@ -8,6 +8,7 @@ const crypto = require('./crypto'); const curve = require('./curve'); const errors = require('./errors'); const queueJob = require('./queue_job'); +const Util = require('./util'); class SessionBuilder { @@ -40,14 +41,9 @@ class SessionBuilder { let record = await this.storage.loadSession(fqAddr); if (!record) { record = new SessionRecord(); - } else { - const openSession = record.getOpenSession(); - if (openSession) { - console.warn("Closing stale open session for new outgoing prekey bundle"); - record.closeSession(openSession); - } - } - record.setSession(session); + } + record.archiveCurrentState(); + record.updateSessionState(session); await this.storage.storeSession(fqAddr, record); }); } @@ -66,17 +62,21 @@ class SessionBuilder { throw new errors.PreKeyError('Invalid PreKey ID'); } const signedPreKeyPair = await this.storage.loadSignedPreKey(message.signedPreKeyId); + const existingOpenSession = record.getOpenSession(); if (!signedPreKeyPair) { + if (existingOpenSession && existingOpenSession.currentRatchet) return; throw new errors.PreKeyError("Missing SignedPreKey"); } - const existingOpenSession = record.getOpenSession(); if (existingOpenSession) { - console.warn("Closing open session in favor of incoming prekey bundle"); - record.closeSession(existingOpenSession); + record.archiveCurrentState(); } - record.setSession(await this.initSession(false, preKeyPair, signedPreKeyPair, + const session = await this.initSession(false, preKeyPair, signedPreKeyPair, message.identityKey, message.baseKey, - undefined, message.registrationId)); + undefined, message.registrationId); + if (existingOpenSession && session && !Util.isEqual(existingOpenSession.indexInfo.remoteIdentityKey, session.indexInfo.remoteIdentityKey)) { + record.deleteAllSessions(); + record.updateSessionState(session); + } return message.preKeyId; } diff --git a/src/session_cipher.js b/src/session_cipher.js index 0e6df11e..44ccf341 100644 --- a/src/session_cipher.js +++ b/src/session_cipher.js @@ -4,6 +4,7 @@ const ChainType = require('./chain_type'); const ProtocolAddress = require('./protocol_address'); const SessionBuilder = require('./session_builder'); const SessionRecord = require('./session_record'); +const Util = require('./util'); const crypto = require('./crypto'); const curve = require('./curve'); const errors = require('./errors'); @@ -45,6 +46,7 @@ class SessionCipher { return ``; } + /** @returns {Promise} */ async getRecord() { const record = await this.storage.loadSession(this.addr.toString()); if (record && !(record instanceof SessionRecord)) { @@ -102,6 +104,7 @@ class SessionCipher { result[0] = this._encodeTupleByte(VERSION, VERSION); result.set(msgBuf, 1); result.set(mac.slice(0, 8), msgBuf.byteLength + 1); + record.updateSessionState(session); await this.storeRecord(record); let type, body; if (session.pendingPreKey) { @@ -151,6 +154,7 @@ class SessionCipher { plaintext }; } catch(e) { + if (e.name == "MessageCounterError") break; errs.push(e); } } @@ -169,6 +173,10 @@ class SessionCipher { throw new errors.SessionError("No session record"); } const result = await this.decryptWithSessions(data, record.getSessions()); + if (result.session.indexInfo.baseKey != (await this.getRecord()).getOpenSession().indexInfo.baseKey) { + record.archiveCurrentState(); + record.openSession(result.session); + } const remoteIdentityKey = result.session.indexInfo.remoteIdentityKey; if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) { throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey); @@ -181,6 +189,7 @@ class SessionCipher { // a full SessionError response. console.warn("Decrypted message with closed session."); } + record.updateSessionState(result.session); await this.storeRecord(record); return result.plaintext; }); @@ -205,6 +214,14 @@ class SessionCipher { const preKeyId = await builder.initIncoming(record, preKeyProto); const session = record.getSession(preKeyProto.baseKey); const plaintext = await this.doDecryptWhisperMessage(preKeyProto.message, session); + record.updateSessionState(session); + + const openSession = record.getOpenSession(); + if (session && openSession && !Util.isEqual(session.indexInfo.remoteIdentityKey, openSession.indexInfo.remoteIdentityKey)) { + record.archiveCurrentState(); + record.openSession(session); + } + await this.storeRecord(record); if (preKeyId) { await this.storage.removePreKey(preKeyId); diff --git a/src/session_record.js b/src/session_record.js index 7626a392..a2e17c49 100644 --- a/src/session_record.js +++ b/src/session_record.js @@ -237,6 +237,7 @@ class SessionRecord { getSession(key) { assertBuffer(key); + this.detectDuplicateOpenSessions(); const session = this.sessions[key.toString('base64')]; if (session && session.indexInfo.baseKeyType === BaseKeyType.OURS) { throw new Error("Tried to lookup a session using our basekey"); @@ -245,6 +246,7 @@ class SessionRecord { } getOpenSession() { + this.detectDuplicateOpenSessions(); for (const session of Object.values(this.sessions)) { if (!this.isClosed(session)) { return session; @@ -286,6 +288,21 @@ class SessionRecord { return session.indexInfo.closed !== -1; } + updateSessionState(session) { + // this.removeOldChains(session); + this.setSession(session); + this.removeOldSessions(); + + } + + archiveCurrentState() { + let open_session = this.getOpenSession(); + if (open_session !== undefined) { + this.closeSession(open_session); + this.updateSessionState(open_session); + } + } + removeOldSessions() { while (Object.keys(this.sessions).length > CLOSED_SESSIONS_MAX) { let oldestKey; @@ -307,8 +324,19 @@ class SessionRecord { } deleteAllSessions() { - for (const key of Object.keys(this.sessions)) { - delete this.sessions[key]; + this.sessions = {}; + } + + detectDuplicateOpenSessions() { + let openSession; + let sessions = this.sessions; + for (const key in sessions) { + if (!this.isClosed(sessions[key])) { + if (openSession !== undefined) { + throw new Error("Datastore inconsistensy: multiple open sessions"); + } + openSession = sessions[key]; + } } } } diff --git a/src/util.js b/src/util.js new file mode 100644 index 00000000..aa13430b --- /dev/null +++ b/src/util.js @@ -0,0 +1,16 @@ +class Util { + static toString(data) { + if (typeof data === "string") return data; + return data.toString('base64'); + } + static isEqual(a, b) { + if (a == null || b == null) return false; + a = Util.toString(a); + b = Util.toString(b); + const maxLength = Math.max(a.length, b.length); + if (maxLength < 5) throw new Error("a/b compare too short"); + return a.substring(0, Math.min(maxLength, a.length)) == b.substring(0, Math.min(maxLength, b.length)); + } +} + +module.exports = Util; \ No newline at end of file From 0ffc9235bb74138c13cb36a6e8359f234f4ae09b Mon Sep 17 00:00:00 2001 From: BochilGaming <79433517+BochilGaming@users.noreply.github.com> Date: Fri, 3 Jun 2022 13:10:36 +0700 Subject: [PATCH 2/6] Fix session required && no log session closed, open, etc.. --- src/session_builder.js | 5 +++-- src/session_cipher.js | 6 +++++- src/session_record.js | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/session_builder.js b/src/session_builder.js index 72dcea5a..0d0cbd74 100644 --- a/src/session_builder.js +++ b/src/session_builder.js @@ -65,7 +65,7 @@ class SessionBuilder { const existingOpenSession = record.getOpenSession(); if (!signedPreKeyPair) { if (existingOpenSession && existingOpenSession.currentRatchet) return; - throw new errors.PreKeyError("Missing SignedPreKey"); + else throw new errors.PreKeyError("Missing SignedPreKey"); } if (existingOpenSession) { record.archiveCurrentState(); @@ -75,8 +75,9 @@ class SessionBuilder { undefined, message.registrationId); if (existingOpenSession && session && !Util.isEqual(existingOpenSession.indexInfo.remoteIdentityKey, session.indexInfo.remoteIdentityKey)) { record.deleteAllSessions(); - record.updateSessionState(session); } + record.updateSessionState(session); + return message.preKeyId; } diff --git a/src/session_cipher.js b/src/session_cipher.js index 44ccf341..3a485a7b 100644 --- a/src/session_cipher.js +++ b/src/session_cipher.js @@ -104,8 +104,10 @@ class SessionCipher { result[0] = this._encodeTupleByte(VERSION, VERSION); result.set(msgBuf, 1); result.set(mac.slice(0, 8), msgBuf.byteLength + 1); + record.updateSessionState(session); await this.storeRecord(record); + let type, body; if (session.pendingPreKey) { type = 3; // prekey bundle @@ -154,7 +156,9 @@ class SessionCipher { plaintext }; } catch(e) { - if (e.name == "MessageCounterError") break; + if (e.name == "MessageCounterError") { + break; + } errs.push(e); } } diff --git a/src/session_record.js b/src/session_record.js index a2e17c49..a8f80fdd 100644 --- a/src/session_record.js +++ b/src/session_record.js @@ -272,7 +272,7 @@ class SessionRecord { console.warn("Session already closed", session); return; } - console.info("Closing session:", session); + // console.info("Closing session:", session); session.indexInfo.closed = Date.now(); } @@ -280,7 +280,7 @@ class SessionRecord { if (!this.isClosed(session)) { console.warn("Session already open"); } - console.info("Opening session:", session); + // console.info("Opening session:", session); session.indexInfo.closed = -1; } @@ -315,7 +315,7 @@ class SessionRecord { } } if (oldestKey) { - console.info("Removing old closed session:", oldestSession); + // console.info("Removing old closed session:", oldestSession); delete this.sessions[oldestKey]; } else { throw new Error('Corrupt sessions object'); From 56ad2a6eaafe1f174d617224ca3d7e583758904f Mon Sep 17 00:00:00 2001 From: BochilGaming <79433517+BochilGaming@users.noreply.github.com> Date: Wed, 22 Jun 2022 18:49:49 +0700 Subject: [PATCH 3/6] Push error before break --- src/session_cipher.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/session_cipher.js b/src/session_cipher.js index 3a485a7b..a3997272 100644 --- a/src/session_cipher.js +++ b/src/session_cipher.js @@ -156,10 +156,8 @@ class SessionCipher { plaintext }; } catch(e) { - if (e.name == "MessageCounterError") { - break; - } errs.push(e); + if (e.name == "MessageCounterError") break; } } console.error("Failed to decrypt message with any known session..."); From 6ba8b8907ae350664fc428504747b74e099cf576 Mon Sep 17 00:00:00 2001 From: BochilGaming <79433517+BochilGaming@users.noreply.github.com> Date: Tue, 20 Jun 2023 11:03:54 +0700 Subject: [PATCH 4/6] async crypto --- index.js | 1 + src/crypto_async.js | 101 ++++++++++++++++++++++++++++++ src/session_builder.js | 52 +++++++++------- src/session_cipher.js | 136 +++++++++++++++++++++-------------------- 4 files changed, 201 insertions(+), 89 deletions(-) create mode 100644 src/crypto_async.js diff --git a/index.js b/index.js index 169ee0fc..696f8031 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ 'use strict'; +exports.crypto_async = require('./src/crypto_async'); exports.crypto = require('./src/crypto'); exports.curve = require('./src/curve'); exports.keyhelper = require('./src/keyhelper'); diff --git a/src/crypto_async.js b/src/crypto_async.js new file mode 100644 index 00000000..4c54ec8b --- /dev/null +++ b/src/crypto_async.js @@ -0,0 +1,101 @@ +const nodeCrypto = require('crypto'); + +function assertBuffer(value) { + if (!(value instanceof Buffer)) { + throw TypeError(`Expected Buffer instead of: ${value.constructor.name}`); + } + return value; +} + +async function encrypt(key, data, iv) { + assertBuffer(key); + assertBuffer(data); + assertBuffer(iv); + const cipher = await nodeCrypto.webcrypto.subtle.importKey("raw", key, { + name: "AES-CBC" + }, false, ["encrypt"]); + return Buffer.from(await nodeCrypto.webcrypto.subtle.encrypt({ + name: "AES-CBC", + iv + }, cipher, data)); +} + +async function decrypt(key, data, iv) { + assertBuffer(key); + assertBuffer(data); + assertBuffer(iv); + const decipher = await nodeCrypto.webcrypto.subtle.importKey("raw", key, { + name: "AES-CBC" + }, false, ["decrypt"]); + return Buffer.from(await nodeCrypto.webcrypto.subtle.decrypt({ + name: "AES-CBC", + iv + }, decipher, data)); +} + +// sign +async function calculateMAC(key, data) { + assertBuffer(key); + assertBuffer(data); + const hmac = await nodeCrypto.subtle.importKey("raw", key, { + name: "HMAC", + hash: { + name: "SHA-256" + } + }, false, ["sign"]); + return Buffer.from(await nodeCrypto.webcrypto.subtle.sign({ + name: "HMAC", + hash: "SHA-256" + }, hmac, data)); +} + +async function hash(data) { + assertBuffer(data); + return Buffer.from(await nodeCrypto.webcrypto.subtle.digest({ + name: "SHA-512" + }, data)); +} + +async function deriveSecrets(input, salt, info) { + assertBuffer(input); + assertBuffer(salt); + assertBuffer(info); + if (salt.byteLength != 32) { + throw new Error("Got salt of incorrect length"); + } + const PRK = await calculateMAC(salt, input); + const infoArray = new Uint8Array(info.byteLength + 1 + 32); + infoArray.set(info, 32); + infoArray[infoArray.length - 1] = 1; + const signeds = [await calculateMAC(PRK, Buffer.from(infoArray.slice(32)))]; + infoArray.set(signeds[0]); + infoArray[infoArray.length - 1] = 2; + signeds.push(await calculateMAC(PRK, Buffer.from(infoArray))); + infoArray.set(signeds[1]); + infoArray[infoArray.length - 1] = 3; + signeds.push(await calculateMAC(PRK, Buffer.from(infoArray))); + return signeds; +} + +async function verifyMAC(data, key, mac, length) { + const calculatedMac = await calculateMAC(key, data); + if (mac.byteLength != length || calculatedMac.byteLength < length) { + throw new Error("Bad MAC length"); + } + let verified = 0; + for (let i = 0; i < mac.byteLength; ++i) { + verified |= calculatedMac[i] ^ mac[i]; + } + if (verified !== 0) { + throw new Error("Bad MAC"); + } +} + +module.exports = { + encrypt, + decrypt, + calculateMAC, + hash, + deriveSecrets, + verifyMAC +}; \ No newline at end of file diff --git a/src/session_builder.js b/src/session_builder.js index 0d0cbd74..a6934864 100644 --- a/src/session_builder.js +++ b/src/session_builder.js @@ -4,7 +4,7 @@ const BaseKeyType = require('./base_key_type'); const ChainType = require('./chain_type'); const SessionRecord = require('./session_record'); -const crypto = require('./crypto'); +const crypto = require('./crypto_async'); const curve = require('./curve'); const errors = require('./errors'); const queueJob = require('./queue_job'); @@ -25,12 +25,12 @@ class SessionBuilder { throw new errors.UntrustedIdentityKeyError(this.addr.id, device.identityKey); } curve.verifySignature(device.identityKey, device.signedPreKey.publicKey, - device.signedPreKey.signature); + device.signedPreKey.signature); const baseKey = curve.generateKeyPair(); const devicePreKey = device.preKey && device.preKey.publicKey; const session = await this.initSession(true, baseKey, undefined, device.identityKey, - devicePreKey, device.signedPreKey.publicKey, - device.registrationId); + devicePreKey, device.signedPreKey.publicKey, + device.registrationId); session.pendingPreKey = { signedKeyId: device.signedPreKey.keyId, baseKey: baseKey.pubKey @@ -41,8 +41,13 @@ class SessionBuilder { let record = await this.storage.loadSession(fqAddr); if (!record) { record = new SessionRecord(); - } + } + const openSession = record.getOpenSession(); record.archiveCurrentState(); + if (!Util.isEqual(openSession.indexInfo.remoteIdentityKey, session.indexInfo.remoteIdentityKey)) { + console.warn("Deleting all sessions because identity has changed"); + record.deleteAllSessions(); + } record.updateSessionState(session); await this.storage.storeSession(fqAddr, record); }); @@ -57,32 +62,35 @@ class SessionBuilder { // This just means we haven't replied. return; } - const preKeyPair = await this.storage.loadPreKey(message.preKeyId); - if (message.preKeyId && !preKeyPair) { - throw new errors.PreKeyError('Invalid PreKey ID'); - } - const signedPreKeyPair = await this.storage.loadSignedPreKey(message.signedPreKeyId); + const [preKeyPair, signedPreKeyPair] = await Promise.all([ + this.storage.loadPreKey(message.preKeyId), + this.storage.loadSignedPreKey(message.signedPreKeyId) + ]); const existingOpenSession = record.getOpenSession(); - if (!signedPreKeyPair) { + if (!signedPreKeyPair) { if (existingOpenSession && existingOpenSession.currentRatchet) return; - else throw new errors.PreKeyError("Missing SignedPreKey"); - } + throw new errors.PreKeyError("Missing Signed PreKey for PreKeyWhisperMessage"); + } if (existingOpenSession) { record.archiveCurrentState(); } + if (message.preKeyId && !preKeyPair) { + throw new errors.PreKeyError("Invalid PreKey ID"); + } const session = await this.initSession(false, preKeyPair, signedPreKeyPair, - message.identityKey, message.baseKey, - undefined, message.registrationId); + message.identityKey, message.baseKey, + undefined, message.registrationId); if (existingOpenSession && session && !Util.isEqual(existingOpenSession.indexInfo.remoteIdentityKey, session.indexInfo.remoteIdentityKey)) { + console.warn("Deleting all sessions because identity has changed"); record.deleteAllSessions(); } record.updateSessionState(session); - + // this.storage.saveIdentity return message.preKeyId; } async initSession(isInitiator, ourEphemeralKey, ourSignedKey, theirIdentityPubKey, - theirEphemeralPubKey, theirSignedPubKey, registrationId) { + theirEphemeralPubKey, theirSignedPubKey, registrationId) { if (isInitiator) { if (ourSignedKey) { throw new Error("Invalid call to initSession"); @@ -119,8 +127,8 @@ class SessionBuilder { const a4 = curve.calculateAgreement(theirEphemeralPubKey, ourEphemeralKey.privKey); sharedSecret.set(new Uint8Array(a4), 32 * 4); } - const masterKey = crypto.deriveSecrets(Buffer.from(sharedSecret), Buffer.alloc(32), - Buffer.from("WhisperText")); + const masterKey = await crypto.deriveSecrets(Buffer.from(sharedSecret), Buffer.alloc(32), + Buffer.from("WhisperText")); const session = SessionRecord.createEntry(); session.registrationId = registrationId; session.currentRatchet = { @@ -141,15 +149,15 @@ class SessionBuilder { // If we're initiating we go ahead and set our first sending ephemeral key now, // otherwise we figure it out when we first maybeStepRatchet with the remote's // ephemeral key - this.calculateSendingRatchet(session, theirSignedPubKey); + await this.calculateSendingRatchet(session, theirSignedPubKey); } return session; } - calculateSendingRatchet(session, remoteKey) { + async calculateSendingRatchet(session, remoteKey) { const ratchet = session.currentRatchet; const sharedSecret = curve.calculateAgreement(remoteKey, ratchet.ephemeralKeyPair.privKey); - const masterKey = crypto.deriveSecrets(sharedSecret, ratchet.rootKey, Buffer.from("WhisperRatchet")); + const masterKey = await crypto.deriveSecrets(sharedSecret, ratchet.rootKey, Buffer.from("WhisperRatchet")); session.addChain(ratchet.ephemeralKeyPair.pubKey, { messageKeys: {}, chainKey: { diff --git a/src/session_cipher.js b/src/session_cipher.js index 3a485a7b..0146b5b1 100644 --- a/src/session_cipher.js +++ b/src/session_cipher.js @@ -5,7 +5,7 @@ const ProtocolAddress = require('./protocol_address'); const SessionBuilder = require('./session_builder'); const SessionRecord = require('./session_record'); const Util = require('./util'); -const crypto = require('./crypto'); +const crypto = require('./crypto_async'); const curve = require('./curve'); const errors = require('./errors'); const protobufs = require('./protobufs'); @@ -66,9 +66,12 @@ class SessionCipher { async encrypt(data) { assertBuffer(data); - const ourIdentityKey = await this.storage.getOurIdentity(); return await this.queueJob(async () => { - const record = await this.getRecord(); + const [ourIdentityKey, ourRegistrationId, record] = await Promise.all([ + this.storage.getOurIdentity(), + this.storage.getOurRegistrationId(), + this.getRecord() + ]); if (!record) { throw new errors.SessionError("No sessions"); } @@ -76,34 +79,37 @@ class SessionCipher { if (!session) { throw new errors.SessionError("No open session"); } - const remoteIdentityKey = session.indexInfo.remoteIdentityKey; - if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) { - throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey); - } const chain = session.getChain(session.currentRatchet.ephemeralKeyPair.pubKey); if (chain.chainType === ChainType.RECEIVING) { throw new Error("Tried to encrypt on a receiving chain"); } - this.fillMessageKeys(chain, chain.chainKey.counter + 1); - const keys = crypto.deriveSecrets(chain.messageKeys[chain.chainKey.counter], - Buffer.alloc(32), Buffer.from("WhisperMessageKeys")); - delete chain.messageKeys[chain.chainKey.counter]; - const msg = protobufs.WhisperMessage.create(); - msg.ephemeralKey = session.currentRatchet.ephemeralKeyPair.pubKey; - msg.counter = chain.chainKey.counter; - msg.previousCounter = session.currentRatchet.previousCounter; - msg.ciphertext = crypto.encrypt(keys[0], data, keys[2].slice(0, 16)); - const msgBuf = protobufs.WhisperMessage.encode(msg).finish(); - const macInput = Buffer.alloc(msgBuf.byteLength + (33 * 2) + 1); - macInput.set(ourIdentityKey.pubKey); - macInput.set(session.indexInfo.remoteIdentityKey, 33); - macInput[33 * 2] = this._encodeTupleByte(VERSION, VERSION); + await this.fillMessageKeys(chain, chain.chainKey.counter + 1); + const keys = await crypto.deriveSecrets(chain.messageKeys[chain.chainKey.counter], + Buffer.alloc(32), Buffer.from("WhisperMessageKeys")); + delete chain.messageKeys[chain.chainKey.counter]; + const msg = protobufs.WhisperMessage.create(); + msg.ephemeralKey = session.currentRatchet.ephemeralKeyPair.pubKey; + msg.counter = chain.chainKey.counter; + msg.previousCounter = session.currentRatchet.previousCounter; + msg.ciphertext = await crypto.encrypt(keys[0], data, keys[2].slice(0, 16)); + const msgBuf = protobufs.WhisperMessage.encode(msg).finish(); + const macInput = Buffer.alloc(msgBuf.byteLength + (33 * 2) + 1); + macInput.set(ourIdentityKey.pubKey); + macInput.set(session.indexInfo.remoteIdentityKey, 33); + macInput[33 * 2] = this._encodeTupleByte(VERSION, VERSION); // 51 macInput.set(msgBuf, (33 * 2) + 1); - const mac = crypto.calculateMAC(keys[1], macInput); + const mac = await crypto.calculateMAC(keys[1], macInput); const result = Buffer.alloc(msgBuf.byteLength + 9); result[0] = this._encodeTupleByte(VERSION, VERSION); result.set(msgBuf, 1); result.set(mac.slice(0, 8), msgBuf.byteLength + 1); + + const remoteIdentityKey = session.indexInfo.remoteIdentityKey; + if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) { + throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey); + } + + // this.storage.saveIdentity(session.indexInfo.remoteIdentityKey) record.updateSessionState(session); await this.storeRecord(record); @@ -113,7 +119,7 @@ class SessionCipher { type = 3; // prekey bundle const preKeyMsg = protobufs.PreKeyWhisperMessage.create({ identityKey: ourIdentityKey.pubKey, - registrationId: await this.storage.getOurRegistrationId(), + registrationId: ourRegistrationId, baseKey: session.pendingPreKey.baseKey, signedPreKeyId: session.pendingPreKey.signedKeyId, message: result @@ -139,34 +145,26 @@ class SessionCipher { }); } - async decryptWithSessions(data, sessions) { + async decryptWithSessions(data, sessions, errors = []) { // Iterate through the sessions, attempting to decrypt using each one. // Stop and return the result if we get a valid result. if (!sessions.length) { - throw new errors.SessionError("No sessions available"); + throw new errors.SessionError(errors[0] || "No sessions available"); } - const errs = []; - for (const session of sessions) { - let plaintext; - try { - plaintext = await this.doDecryptWhisperMessage(data, session); - session.indexInfo.used = Date.now(); - return { - session, - plaintext - }; - } catch(e) { - if (e.name == "MessageCounterError") { - break; - } - errs.push(e); - } - } - console.error("Failed to decrypt message with any known session..."); - for (const e of errs) { - console.error("Session error:" + e, e.stack); + const session = sessions.pop(); + try { + const plaintext = await this.doDecryptWhisperMessage(data, session); + session.indexInfo.used = Date.now(); + return { + session, + plaintext + }; + } catch (e) { + if (e.name === "MessageCounterError") + throw e; + errors.push(e); + return await this.decryptWithSessions(data, sessions, errors); } - throw new errors.SessionError("No matching sessions found for message"); } async decryptWhisperMessage(data) { @@ -177,7 +175,8 @@ class SessionCipher { throw new errors.SessionError("No session record"); } const result = await this.decryptWithSessions(data, record.getSessions()); - if (result.session.indexInfo.baseKey != (await this.getRecord()).getOpenSession().indexInfo.baseKey) { + const session = (await this.getRecord()).getOpenSession(); + if (result.session.indexInfo.baseKey != session.indexInfo.baseKey) { record.archiveCurrentState(); record.openSession(result.session); } @@ -193,6 +192,7 @@ class SessionCipher { // a full SessionError response. console.warn("Decrypted message with closed session."); } + // this.storage.saveIdentity record.updateSessionState(result.session); await this.storeRecord(record); return result.plaintext; @@ -222,8 +222,10 @@ class SessionCipher { const openSession = record.getOpenSession(); if (session && openSession && !Util.isEqual(session.indexInfo.remoteIdentityKey, openSession.indexInfo.remoteIdentityKey)) { + console.warn("Promote the old session and update identity"); record.archiveCurrentState(); record.openSession(session); + // this.storage.saveIdentity } await this.storeRecord(record); @@ -237,7 +239,7 @@ class SessionCipher { async doDecryptWhisperMessage(messageBuffer, session) { assertBuffer(messageBuffer); if (!session) { - throw new TypeError("session required"); + throw new Error("No session found to decrypt message from " + this.addr.toString()) } const versions = this._decodeTupleByte(messageBuffer[0]); if (versions[1] > 3 || versions[0] < 3) { // min version > 3 or max version < 3 @@ -245,20 +247,20 @@ class SessionCipher { } const messageProto = messageBuffer.slice(1, -8); const message = protobufs.WhisperMessage.decode(messageProto); - this.maybeStepRatchet(session, message.ephemeralKey, message.previousCounter); + await this.maybeStepRatchet(session, message.ephemeralKey, message.previousCounter); const chain = session.getChain(message.ephemeralKey); if (chain.chainType === ChainType.SENDING) { throw new Error("Tried to decrypt on a sending chain"); } - this.fillMessageKeys(chain, message.counter); + await this.fillMessageKeys(chain, message.counter); if (!chain.messageKeys.hasOwnProperty(message.counter)) { // Most likely the message was already decrypted and we are trying to process // twice. This can happen if the user restarts before the server gets an ACK. - throw new errors.MessageCounterError('Key used already or never filled'); + throw new errors.MessageCounterError("Message key not found. The counter was repeated or the key was not filled."); } const messageKey = chain.messageKeys[message.counter]; delete chain.messageKeys[message.counter]; - const keys = crypto.deriveSecrets(messageKey, Buffer.alloc(32), + const keys = await crypto.deriveSecrets(messageKey, Buffer.alloc(32), Buffer.from("WhisperMessageKeys")); const ourIdentityKey = await this.storage.getOurIdentity(); const macInput = Buffer.alloc(messageProto.byteLength + (33 * 2) + 1); @@ -268,40 +270,40 @@ class SessionCipher { macInput.set(messageProto, (33 * 2) + 1); // This is where we most likely fail if the session is not a match. // Don't misinterpret this as corruption. - crypto.verifyMAC(macInput, keys[1], messageBuffer.slice(-8), 8); - const plaintext = crypto.decrypt(keys[0], message.ciphertext, keys[2].slice(0, 16)); + await crypto.verifyMAC(macInput, keys[1], messageBuffer.slice(-8), 8); + const plaintext = await crypto.decrypt(keys[0], message.ciphertext, keys[2].slice(0, 16)); delete session.pendingPreKey; return plaintext; } - fillMessageKeys(chain, counter) { + async fillMessageKeys(chain, counter) { if (chain.chainKey.counter >= counter) { return; } if (counter - chain.chainKey.counter > 2000) { - throw new errors.SessionError('Over 2000 messages into the future!'); + throw new errors.SessionError("Over 2000 messages into the future!"); } if (chain.chainKey.key === undefined) { - throw new errors.SessionError('Chain closed'); + throw new errors.SessionError("Got invalid request to extend chain after it was already closed"); } const key = chain.chainKey.key; - chain.messageKeys[chain.chainKey.counter + 1] = crypto.calculateMAC(key, Buffer.from([1])); - chain.chainKey.key = crypto.calculateMAC(key, Buffer.from([2])); + chain.messageKeys[chain.chainKey.counter + 1] = await crypto.calculateMAC(key, Buffer.from([1])); + chain.chainKey.key = await crypto.calculateMAC(key, Buffer.from([2])); chain.chainKey.counter += 1; - return this.fillMessageKeys(chain, counter); + return await this.fillMessageKeys(chain, counter); } - maybeStepRatchet(session, remoteKey, previousCounter) { + async maybeStepRatchet(session, remoteKey, previousCounter) { if (session.getChain(remoteKey)) { return; } const ratchet = session.currentRatchet; let previousRatchet = session.getChain(ratchet.lastRemoteEphemeralKey); if (previousRatchet) { - this.fillMessageKeys(previousRatchet, previousCounter); + await this.fillMessageKeys(previousRatchet, previousCounter); delete previousRatchet.chainKey.key; // Close } - this.calculateRatchet(session, remoteKey, false); + await this.calculateRatchet(session, remoteKey, false); // Now swap the ephemeral key and calculate the new sending chain const prevCounter = session.getChain(ratchet.ephemeralKeyPair.pubKey); if (prevCounter) { @@ -309,14 +311,14 @@ class SessionCipher { session.deleteChain(ratchet.ephemeralKeyPair.pubKey); } ratchet.ephemeralKeyPair = curve.generateKeyPair(); - this.calculateRatchet(session, remoteKey, true); + await this.calculateRatchet(session, remoteKey, true); ratchet.lastRemoteEphemeralKey = remoteKey; } - calculateRatchet(session, remoteKey, sending) { + async calculateRatchet(session, remoteKey, sending) { let ratchet = session.currentRatchet; const sharedSecret = curve.calculateAgreement(remoteKey, ratchet.ephemeralKeyPair.privKey); - const masterKey = crypto.deriveSecrets(sharedSecret, ratchet.rootKey, + const masterKey = await crypto.deriveSecrets(sharedSecret, ratchet.rootKey, Buffer.from("WhisperRatchet"), /*chunks*/ 2); const chainKey = sending ? ratchet.ephemeralKeyPair.pubKey : remoteKey; session.addChain(chainKey, { @@ -346,7 +348,7 @@ class SessionCipher { if (record) { const openSession = record.getOpenSession(); if (openSession) { - record.closeSession(openSession); + record.archiveCurrentState(); await this.storeRecord(record); } } From 781f67481d992bc324485018ce600907b30211f6 Mon Sep 17 00:00:00 2001 From: BochilGaming <79433517+BochilGaming@users.noreply.github.com> Date: Fri, 23 Jun 2023 08:33:45 +0700 Subject: [PATCH 5/6] Revert crypto async --- index.js | 1 - src/crypto_async.js | 101 ----------------------------------------- src/session_builder.js | 10 ++-- src/session_cipher.js | 36 +++++++-------- 4 files changed, 23 insertions(+), 125 deletions(-) delete mode 100644 src/crypto_async.js diff --git a/index.js b/index.js index 696f8031..169ee0fc 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,5 @@ 'use strict'; -exports.crypto_async = require('./src/crypto_async'); exports.crypto = require('./src/crypto'); exports.curve = require('./src/curve'); exports.keyhelper = require('./src/keyhelper'); diff --git a/src/crypto_async.js b/src/crypto_async.js deleted file mode 100644 index 4c54ec8b..00000000 --- a/src/crypto_async.js +++ /dev/null @@ -1,101 +0,0 @@ -const nodeCrypto = require('crypto'); - -function assertBuffer(value) { - if (!(value instanceof Buffer)) { - throw TypeError(`Expected Buffer instead of: ${value.constructor.name}`); - } - return value; -} - -async function encrypt(key, data, iv) { - assertBuffer(key); - assertBuffer(data); - assertBuffer(iv); - const cipher = await nodeCrypto.webcrypto.subtle.importKey("raw", key, { - name: "AES-CBC" - }, false, ["encrypt"]); - return Buffer.from(await nodeCrypto.webcrypto.subtle.encrypt({ - name: "AES-CBC", - iv - }, cipher, data)); -} - -async function decrypt(key, data, iv) { - assertBuffer(key); - assertBuffer(data); - assertBuffer(iv); - const decipher = await nodeCrypto.webcrypto.subtle.importKey("raw", key, { - name: "AES-CBC" - }, false, ["decrypt"]); - return Buffer.from(await nodeCrypto.webcrypto.subtle.decrypt({ - name: "AES-CBC", - iv - }, decipher, data)); -} - -// sign -async function calculateMAC(key, data) { - assertBuffer(key); - assertBuffer(data); - const hmac = await nodeCrypto.subtle.importKey("raw", key, { - name: "HMAC", - hash: { - name: "SHA-256" - } - }, false, ["sign"]); - return Buffer.from(await nodeCrypto.webcrypto.subtle.sign({ - name: "HMAC", - hash: "SHA-256" - }, hmac, data)); -} - -async function hash(data) { - assertBuffer(data); - return Buffer.from(await nodeCrypto.webcrypto.subtle.digest({ - name: "SHA-512" - }, data)); -} - -async function deriveSecrets(input, salt, info) { - assertBuffer(input); - assertBuffer(salt); - assertBuffer(info); - if (salt.byteLength != 32) { - throw new Error("Got salt of incorrect length"); - } - const PRK = await calculateMAC(salt, input); - const infoArray = new Uint8Array(info.byteLength + 1 + 32); - infoArray.set(info, 32); - infoArray[infoArray.length - 1] = 1; - const signeds = [await calculateMAC(PRK, Buffer.from(infoArray.slice(32)))]; - infoArray.set(signeds[0]); - infoArray[infoArray.length - 1] = 2; - signeds.push(await calculateMAC(PRK, Buffer.from(infoArray))); - infoArray.set(signeds[1]); - infoArray[infoArray.length - 1] = 3; - signeds.push(await calculateMAC(PRK, Buffer.from(infoArray))); - return signeds; -} - -async function verifyMAC(data, key, mac, length) { - const calculatedMac = await calculateMAC(key, data); - if (mac.byteLength != length || calculatedMac.byteLength < length) { - throw new Error("Bad MAC length"); - } - let verified = 0; - for (let i = 0; i < mac.byteLength; ++i) { - verified |= calculatedMac[i] ^ mac[i]; - } - if (verified !== 0) { - throw new Error("Bad MAC"); - } -} - -module.exports = { - encrypt, - decrypt, - calculateMAC, - hash, - deriveSecrets, - verifyMAC -}; \ No newline at end of file diff --git a/src/session_builder.js b/src/session_builder.js index a6934864..08a5f253 100644 --- a/src/session_builder.js +++ b/src/session_builder.js @@ -4,7 +4,7 @@ const BaseKeyType = require('./base_key_type'); const ChainType = require('./chain_type'); const SessionRecord = require('./session_record'); -const crypto = require('./crypto_async'); +const crypto = require('./crypto'); const curve = require('./curve'); const errors = require('./errors'); const queueJob = require('./queue_job'); @@ -127,7 +127,7 @@ class SessionBuilder { const a4 = curve.calculateAgreement(theirEphemeralPubKey, ourEphemeralKey.privKey); sharedSecret.set(new Uint8Array(a4), 32 * 4); } - const masterKey = await crypto.deriveSecrets(Buffer.from(sharedSecret), Buffer.alloc(32), + const masterKey = crypto.deriveSecrets(Buffer.from(sharedSecret), Buffer.alloc(32), Buffer.from("WhisperText")); const session = SessionRecord.createEntry(); session.registrationId = registrationId; @@ -149,15 +149,15 @@ class SessionBuilder { // If we're initiating we go ahead and set our first sending ephemeral key now, // otherwise we figure it out when we first maybeStepRatchet with the remote's // ephemeral key - await this.calculateSendingRatchet(session, theirSignedPubKey); + this.calculateSendingRatchet(session, theirSignedPubKey); } return session; } - async calculateSendingRatchet(session, remoteKey) { + calculateSendingRatchet(session, remoteKey) { const ratchet = session.currentRatchet; const sharedSecret = curve.calculateAgreement(remoteKey, ratchet.ephemeralKeyPair.privKey); - const masterKey = await crypto.deriveSecrets(sharedSecret, ratchet.rootKey, Buffer.from("WhisperRatchet")); + const masterKey = crypto.deriveSecrets(sharedSecret, ratchet.rootKey, Buffer.from("WhisperRatchet")); session.addChain(ratchet.ephemeralKeyPair.pubKey, { messageKeys: {}, chainKey: { diff --git a/src/session_cipher.js b/src/session_cipher.js index 0146b5b1..cb7cadc8 100644 --- a/src/session_cipher.js +++ b/src/session_cipher.js @@ -5,7 +5,7 @@ const ProtocolAddress = require('./protocol_address'); const SessionBuilder = require('./session_builder'); const SessionRecord = require('./session_record'); const Util = require('./util'); -const crypto = require('./crypto_async'); +const crypto = require('./crypto'); const curve = require('./curve'); const errors = require('./errors'); const protobufs = require('./protobufs'); @@ -83,22 +83,22 @@ class SessionCipher { if (chain.chainType === ChainType.RECEIVING) { throw new Error("Tried to encrypt on a receiving chain"); } - await this.fillMessageKeys(chain, chain.chainKey.counter + 1); - const keys = await crypto.deriveSecrets(chain.messageKeys[chain.chainKey.counter], + this.fillMessageKeys(chain, chain.chainKey.counter + 1); + const keys = crypto.deriveSecrets(chain.messageKeys[chain.chainKey.counter], Buffer.alloc(32), Buffer.from("WhisperMessageKeys")); delete chain.messageKeys[chain.chainKey.counter]; const msg = protobufs.WhisperMessage.create(); msg.ephemeralKey = session.currentRatchet.ephemeralKeyPair.pubKey; msg.counter = chain.chainKey.counter; msg.previousCounter = session.currentRatchet.previousCounter; - msg.ciphertext = await crypto.encrypt(keys[0], data, keys[2].slice(0, 16)); + msg.ciphertext = crypto.encrypt(keys[0], data, keys[2].slice(0, 16)); const msgBuf = protobufs.WhisperMessage.encode(msg).finish(); const macInput = Buffer.alloc(msgBuf.byteLength + (33 * 2) + 1); macInput.set(ourIdentityKey.pubKey); macInput.set(session.indexInfo.remoteIdentityKey, 33); macInput[33 * 2] = this._encodeTupleByte(VERSION, VERSION); // 51 macInput.set(msgBuf, (33 * 2) + 1); - const mac = await crypto.calculateMAC(keys[1], macInput); + const mac = crypto.calculateMAC(keys[1], macInput); const result = Buffer.alloc(msgBuf.byteLength + 9); result[0] = this._encodeTupleByte(VERSION, VERSION); result.set(msgBuf, 1); @@ -252,7 +252,7 @@ class SessionCipher { if (chain.chainType === ChainType.SENDING) { throw new Error("Tried to decrypt on a sending chain"); } - await this.fillMessageKeys(chain, message.counter); + this.fillMessageKeys(chain, message.counter); if (!chain.messageKeys.hasOwnProperty(message.counter)) { // Most likely the message was already decrypted and we are trying to process // twice. This can happen if the user restarts before the server gets an ACK. @@ -260,7 +260,7 @@ class SessionCipher { } const messageKey = chain.messageKeys[message.counter]; delete chain.messageKeys[message.counter]; - const keys = await crypto.deriveSecrets(messageKey, Buffer.alloc(32), + const keys = crypto.deriveSecrets(messageKey, Buffer.alloc(32), Buffer.from("WhisperMessageKeys")); const ourIdentityKey = await this.storage.getOurIdentity(); const macInput = Buffer.alloc(messageProto.byteLength + (33 * 2) + 1); @@ -270,13 +270,13 @@ class SessionCipher { macInput.set(messageProto, (33 * 2) + 1); // This is where we most likely fail if the session is not a match. // Don't misinterpret this as corruption. - await crypto.verifyMAC(macInput, keys[1], messageBuffer.slice(-8), 8); - const plaintext = await crypto.decrypt(keys[0], message.ciphertext, keys[2].slice(0, 16)); + crypto.verifyMAC(macInput, keys[1], messageBuffer.slice(-8), 8); + const plaintext = crypto.decrypt(keys[0], message.ciphertext, keys[2].slice(0, 16)); delete session.pendingPreKey; return plaintext; } - async fillMessageKeys(chain, counter) { + fillMessageKeys(chain, counter) { if (chain.chainKey.counter >= counter) { return; } @@ -287,10 +287,10 @@ class SessionCipher { throw new errors.SessionError("Got invalid request to extend chain after it was already closed"); } const key = chain.chainKey.key; - chain.messageKeys[chain.chainKey.counter + 1] = await crypto.calculateMAC(key, Buffer.from([1])); - chain.chainKey.key = await crypto.calculateMAC(key, Buffer.from([2])); + chain.messageKeys[chain.chainKey.counter + 1] = crypto.calculateMAC(key, Buffer.from([1])); + chain.chainKey.key = crypto.calculateMAC(key, Buffer.from([2])); chain.chainKey.counter += 1; - return await this.fillMessageKeys(chain, counter); + return this.fillMessageKeys(chain, counter); } async maybeStepRatchet(session, remoteKey, previousCounter) { @@ -300,10 +300,10 @@ class SessionCipher { const ratchet = session.currentRatchet; let previousRatchet = session.getChain(ratchet.lastRemoteEphemeralKey); if (previousRatchet) { - await this.fillMessageKeys(previousRatchet, previousCounter); + this.fillMessageKeys(previousRatchet, previousCounter); delete previousRatchet.chainKey.key; // Close } - await this.calculateRatchet(session, remoteKey, false); + this.calculateRatchet(session, remoteKey, false); // Now swap the ephemeral key and calculate the new sending chain const prevCounter = session.getChain(ratchet.ephemeralKeyPair.pubKey); if (prevCounter) { @@ -311,14 +311,14 @@ class SessionCipher { session.deleteChain(ratchet.ephemeralKeyPair.pubKey); } ratchet.ephemeralKeyPair = curve.generateKeyPair(); - await this.calculateRatchet(session, remoteKey, true); + this.calculateRatchet(session, remoteKey, true); ratchet.lastRemoteEphemeralKey = remoteKey; } - async calculateRatchet(session, remoteKey, sending) { + calculateRatchet(session, remoteKey, sending) { let ratchet = session.currentRatchet; const sharedSecret = curve.calculateAgreement(remoteKey, ratchet.ephemeralKeyPair.privKey); - const masterKey = await crypto.deriveSecrets(sharedSecret, ratchet.rootKey, + const masterKey = crypto.deriveSecrets(sharedSecret, ratchet.rootKey, Buffer.from("WhisperRatchet"), /*chunks*/ 2); const chainKey = sending ? ratchet.ephemeralKeyPair.pubKey : remoteKey; session.addChain(chainKey, { From 5ce78d5a484f628c4d2b0aaf3ed80ffbd6a9dbe3 Mon Sep 17 00:00:00 2001 From: BochilGaming <79433517+BochilGaming@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:02:48 +0700 Subject: [PATCH 6/6] FixCannot read properties of undefined (reading 'indexInfo') --- src/session_builder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session_builder.js b/src/session_builder.js index 08a5f253..1d651747 100644 --- a/src/session_builder.js +++ b/src/session_builder.js @@ -44,7 +44,7 @@ class SessionBuilder { } const openSession = record.getOpenSession(); record.archiveCurrentState(); - if (!Util.isEqual(openSession.indexInfo.remoteIdentityKey, session.indexInfo.remoteIdentityKey)) { + if (openSession && session && !Util.isEqual(openSession.indexInfo.remoteIdentityKey, session.indexInfo.remoteIdentityKey)) { console.warn("Deleting all sessions because identity has changed"); record.deleteAllSessions(); }