UNPKG

proteus-hd

Version:

Signal Protocol (with header encryption) implementation for JavaScript. Based on Proteus.js.

495 lines (434 loc) 15.5 kB
/* * Wire * Copyright (C) 2016 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * */ 'use strict'; const CBOR = require('wire-webapp-cbor'); const ClassUtil = require('../util/ClassUtil'); const DontCallConstructor = require('../errors/DontCallConstructor'); const MemoryUtil = require('../util/MemoryUtil'); const TypeUtil = require('../util/TypeUtil'); const DecodeError = require('../errors/DecodeError'); const DecryptError = require('../errors/DecryptError'); const ProteusError = require('../errors/ProteusError'); const IdentityKey = require('../keys/IdentityKey'); const IdentityKeyPair = require('../keys/IdentityKeyPair'); const KeyPair = require('../keys/KeyPair'); const PreKey = require('../keys/PreKey'); const PreKeyBundle = require('../keys/PreKeyBundle'); const PublicKey = require('../keys/PublicKey'); const HeaderMessage = require('../message/HeaderMessage'); const Envelope = require('../message/Envelope'); const PreKeyMessage = require('../message/PreKeyMessage'); const PreKeyStore = require('./PreKeyStore'); /** @module session */ /** * @class Session * @throws {DontCallConstructor} */ class Session { constructor() { this.local_identity = null; this.pending_prekey = null; this.remote_identity = null; this.session_states = []; this.version = 1; throw new DontCallConstructor(this); } /** @type {number} */ static get MAX_RECV_CHAINS() { return 5; } /** @type {number} */ static get MAX_SESSION_STATES() { return 100; } /** * @param {!keys.IdentityKeyPair} local_identity - Alice's Identity Key Pair * @param {!keys.PreKeyBundle} remote_pkbundle - Bob's Pre-Key Bundle * @returns {Promise<Session>} */ static init_from_prekey(local_identity, remote_pkbundle) { return new Promise((resolve) => { TypeUtil.assert_is_instance(IdentityKeyPair, local_identity); TypeUtil.assert_is_instance(PreKeyBundle, remote_pkbundle); const alice_base = KeyPair.new(); const state = SessionState.init_as_alice(local_identity, alice_base, remote_pkbundle); const session = ClassUtil.new_instance(this); session.local_identity = local_identity; session.remote_identity = remote_pkbundle.identity_key; session.pending_prekey = [remote_pkbundle.prekey_id, alice_base.public_key]; session.session_states = []; session._insert_session_state(state); return resolve(session); }); } /** * @param {!keys.IdentityKeyPair} our_identity * @param {!session.PreKeyStore} prekey_store * @param {!message.Envelope} envelope * @returns {Promise<Array<Session|Uint8Array>>} * @throws {errors.DecryptError.InvalidMessage} * @throws {errors.DecryptError.PrekeyNotFound} */ static init_from_message(our_identity, prekey_store, envelope) { return new Promise((resolve, reject) => { TypeUtil.assert_is_instance(IdentityKeyPair, our_identity); TypeUtil.assert_is_instance(PreKeyStore, prekey_store); TypeUtil.assert_is_instance(Envelope, envelope); const pkmsg = (() => { if (envelope.message instanceof HeaderMessage) { throw new DecryptError.InvalidMessage( 'Can\'t initialise a session from a HeaderMessage.', DecryptError.CODE.CASE_201 ); } else if (envelope.message instanceof PreKeyMessage) { return envelope.message; } else { throw new DecryptError.InvalidMessage( 'Unknown message format: The message is neither a "HeaderMessage" nor a "PreKeyMessage".', DecryptError.CODE.CASE_202 ); } })(); const session = ClassUtil.new_instance(Session); session.local_identity = our_identity; session.remote_identity = pkmsg.identity_key; session.pending_prekey = null; session.session_states = []; return session._new_state(prekey_store, pkmsg).then((state) => { const plain = state.decrypt(envelope, pkmsg.message); session._insert_session_state(state); if (pkmsg.prekey_id < PreKey.MAX_PREKEY_ID) { MemoryUtil.zeroize(prekey_store.prekeys[pkmsg.prekey_id]); return prekey_store.remove(pkmsg.prekey_id).then(() => resolve([session, plain])).catch((error) => { reject(new DecryptError.PrekeyNotFound(`Could not delete PreKey: ${error.message}`, DecryptError.CODE.CASE_203)); }); } else { return resolve([session, plain]); } }).catch(reject); }); } /** * @param {!session.PreKeyStore} pre_key_store * @param {!message.PreKeyMessage} pre_key_message * @returns {Promise<session.SessionState>} * @private * @throws {errors.ProteusError} */ _new_state(pre_key_store, pre_key_message) { return pre_key_store.get_prekey(pre_key_message.prekey_id).then((pre_key) => { if (pre_key) { return SessionState.init_as_bob( this.local_identity, pre_key.key_pair, pre_key_message.identity_key, pre_key_message.base_key ); } throw new ProteusError('Unable to get PreKey from PreKey store.', ProteusError.prototype.CODE.CASE_101); }); } /** * @param {!session.SessionState} state * @returns {boolean} * @private */ _insert_session_state(state) { this.session_states.unshift(state); const size = this.session_states.length; if (size < Session.MAX_SESSION_STATES) { return true; } // if we get here, it means that we have more than MAX_SESSION_STATES and // we need to evict the oldest one. return delete this.session_states[size - 1]; } /** @returns {keys.PublicKey} */ get_local_identity() { return this.local_identity.public_key; } /** * @param {!(string|Uint8Array)} plaintext - The plaintext which needs to be encrypted * @param {number} confuse_pre_key_id - Use to create confused pre-key message * @return {Promise<message.Envelope>} Encrypted message */ encrypt(plaintext, confuse_pre_key_id) { return new Promise((resolve, reject) => { const state = this.session_states[0]; if (!state) { return reject(new ProteusError( 'Could not find session.', ProteusError.prototype.CODE.CASE_102 )); } return resolve(state.encrypt( this.local_identity.public_key, this.pending_prekey, plaintext, confuse_pre_key_id )); }); } /** * @param {!session.PreKeyStore} prekey_store * @param {!message.Envelope} envelope * @returns {Promise<Uint8Array>} * @throws {errors.DecryptError} */ decrypt(prekey_store, envelope) { return new Promise((resolve) => { TypeUtil.assert_is_instance(PreKeyStore, prekey_store); TypeUtil.assert_is_instance(Envelope, envelope); const msg = envelope.message; if (msg instanceof HeaderMessage) { return resolve(this._try_decrypt_header_message(envelope, msg, 0)); } else if (msg instanceof PreKeyMessage) { const actual_fingerprint = msg.identity_key.fingerprint(); const expected_fingerprint = this.remote_identity.fingerprint(); if (actual_fingerprint !== expected_fingerprint) { const message = `Fingerprints do not match: We expected '${expected_fingerprint}', but received '${actual_fingerprint}'.`; throw new DecryptError.RemoteIdentityChanged(message, DecryptError.CODE.CASE_204); } return resolve(this._decrypt_prekey_message(envelope, msg, prekey_store)); } else { throw new DecryptError('Unknown message type.', DecryptError.CODE.CASE_200); } }); } /** * @param {!message.Envelope} envelope * @param {!message.Message} msg * @param {!session.PreKeyStore} prekey_store * @private * @returns {Promise<Uint8Array>} * @throws {errors.DecryptError} */ _decrypt_prekey_message(envelope, msg, prekey_store) { return Promise.resolve().then(() => this._decrypt_header_message(envelope, msg.message)).catch((error) => { const try_create_new_state_and_decrypt = () => { return this._new_state(prekey_store, msg).then((state) => { const plaintext = state.decrypt(envelope, msg.message); if (msg.prekey_id !== PreKey.MAX_PREKEY_ID) { MemoryUtil.zeroize(prekey_store.prekeys[msg.prekey_id]); prekey_store.remove(msg.prekey_id); } this._insert_session_state(state); this.pending_prekey = null; return plaintext; }); }; if (error instanceof DecryptError.InvalidMessage) { // session state not exist try_create_new_state_and_decrypt(); } if (error instanceof DecryptError.HeaderDecryptionFailed) { // we had tried it once already let fail_counter = 1; const state_size = this.session_states.length; if (state_size === fail_counter) { return try_create_new_state_and_decrypt(); } // start from index 1 return this._try_decrypt_header_message(envelope, msg.message, 1) .catch((err) => { if (err instanceof DecryptError.HeaderDecryptionFailed) { return try_create_new_state_and_decrypt(); } else { throw err; } }); } throw error; }); } /** * @param {!message.Envelope} envelope * @param {!message.Message} message * @param {!number} start * @private * @returns {Promise<Uint8Array>} */ _try_decrypt_header_message(envelope, message, start) { return new Promise((resolve, reject) => { let fail_counter = start; const state_size = this.session_states.length; const HeaderDecryptionFailed = DecryptError.HeaderDecryptionFailed; const try_decrypt_header_message = () => this._decrypt_header_message(envelope, message, fail_counter); const handle_error = (err) => { if (err instanceof HeaderDecryptionFailed) { fail_counter++; if (fail_counter === state_size) { reject(new HeaderDecryptionFailed('All states failed', DecryptError.CODE.CASE_216)); } Promise.resolve() .then(try_decrypt_header_message) .then(resolve) .catch(handle_error); } else { // if we get here, it means that we had decrypted header, but something else has gone wrong reject(err); } }; Promise.resolve() .then(try_decrypt_header_message) .then(resolve) .catch(handle_error); }); } /** * @param {!message.Envelope} envelope * @param {!message.Message} msg * @param {number} state_index * @private * @returns {Uint8Array} */ _decrypt_header_message(envelope, msg, state_index = 0) { let state = this.session_states[state_index]; if (!state) { throw new DecryptError.InvalidMessage('Local session not found.', DecryptError.CODE.CASE_205); } // serialise and de-serialise for a deep clone // THIS IS IMPORTANT, DO NOT MUTATE THE SESSION STATE IN-PLACE // mutating in-place can lead to undefined behavior and undefined state in edge cases state = SessionState.deserialise(state.serialise()); const plaintext = state.decrypt(envelope, msg); this.pending_prekey = null; // Avoid `unshift` operation when possible if (state_index === 0) { this.session_states[0] = state; } else { this.session_states.splice(state_index, 1); this._insert_session_state(state); } return plaintext; } /** * @returns {ArrayBuffer} */ serialise() { const e = new CBOR.Encoder(); this.encode(e); return e.get_buffer(); } /** * @param {!keys.IdentityKeyPair} local_identity * @param {!ArrayBuffer} buf * @returns {Session} */ static deserialise(local_identity, buf) { TypeUtil.assert_is_instance(IdentityKeyPair, local_identity); TypeUtil.assert_is_instance(ArrayBuffer, buf); const d = new CBOR.Decoder(buf); return this.decode(local_identity, d); } /** * @param {!CBOR.Encoder} e * @returns {void} */ encode(e) { e.object(5); e.u8(0); e.u8(this.version); e.u8(1); this.local_identity.public_key.encode(e); e.u8(2); this.remote_identity.encode(e); e.u8(3); if (this.pending_prekey) { e.object(2); e.u8(0); e.u16(this.pending_prekey[0]); e.u8(1); this.pending_prekey[1].encode(e); } else { e.null(); } e.u8(4); e.array(this.session_states.length); this.session_states.map((session_state) => session_state.encode(e)); } /** * @param {!keys.IdentityKeyPair} local_identity * @param {!CBOR.Decoder} d * @returns {Session} */ static decode(local_identity, d) { TypeUtil.assert_is_instance(IdentityKeyPair, local_identity); TypeUtil.assert_is_instance(CBOR.Decoder, d); const self = ClassUtil.new_instance(this); const nprops = d.object(); for (let n = 0; n <= nprops - 1; n++) { switch (d.u8()) { case 0: { self.version = d.u8(); break; } case 1: { const ik = IdentityKey.decode(d); if (local_identity.public_key.fingerprint() !== ik.fingerprint()) { throw new DecodeError.LocalIdentityChanged(null, DecodeError.CODE.CASE_300); } self.local_identity = local_identity; break; } case 2: { self.remote_identity = IdentityKey.decode(d); break; } case 3: { switch (d.optional(() => d.object())) { case null: self.pending_prekey = null; break; case 2: self.pending_prekey = [null, null]; for (let k = 0; k <= 1; ++k) { switch (d.u8()) { case 0: self.pending_prekey[0] = d.u16(); break; case 1: self.pending_prekey[1] = PublicKey.decode(d); } } break; default: throw new DecodeError.InvalidType(null, DecodeError.CODE.CASE_301); } break; } case 4: { self.session_states = []; let len = d.array(); while (len--) { self.session_states.push(SessionState.decode(d)); } break; } default: { d.skip(); } } } TypeUtil.assert_is_integer(self.version); TypeUtil.assert_is_instance(IdentityKeyPair, self.local_identity); TypeUtil.assert_is_instance(IdentityKey, self.remote_identity); TypeUtil.assert_is_instance(Array, self.session_states); return self; } } module.exports = Session; const SessionState = require('./SessionState');