proteus-hd
Version:
Signal Protocol (with header encryption) implementation for JavaScript. Based on Proteus.js.
294 lines (246 loc) • 8.68 kB
JavaScript
/*
* 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/.
*
*/
;
const CBOR = require('wire-webapp-cbor');
const ClassUtil = require('../util/ClassUtil');
const DontCallConstructor = require('../errors/DontCallConstructor');
const TypeUtil = require('../util/TypeUtil');
const PublicKey = require('../keys/PublicKey');
const DecryptError = require('../errors/DecryptError');
const ProteusError = require('../errors/ProteusError');
const Header = require('../message/Header');
const Envelope = require('../message/Envelope');
const ChainKey = require('./ChainKey');
const HeadKey = require('../derived/HeadKey');
const MessageKeys = require('./MessageKeys');
/** @module session */
/**
* @class RecvChain
* @throws {DontCallConstructor}
*/
class RecvChain {
constructor() {
throw new DontCallConstructor(this);
}
/**
* @param {!session.ChainKey} chain_key
* @param {!keys.PublicKey} public_key
* @param {!derived.HeadKey} head_key
* @returns {RecvChain}
*/
static new(chain_key, public_key, head_key) {
TypeUtil.assert_is_instance(ChainKey, chain_key);
TypeUtil.assert_is_instance(PublicKey, public_key);
const rc = ClassUtil.new_instance(RecvChain);
rc.chain_key = chain_key;
rc.ratchet_key = public_key;
rc.head_key = head_key;
rc.final_count = null;
rc.message_keys = [];
return rc;
}
/**
* @param {!number} start_index
* @param {!number} end_index
* @param {!Uint8Array} encrypted_header - encrypted header
* @param {!derived.HeadKey} head_key
* @returns {message.Header}
* @private
*/
static _try_head_key(start_index, end_index, encrypted_header, head_key) {
const [header, index] = (() => {
for (let i = start_index; i <= end_index; i++) {
try {
const header_typed_array = head_key.decrypt(encrypted_header, HeadKey.index_as_nonce(i));
return [Header.deserialise(header_typed_array.buffer), i];
} catch (err) {
// noop
}
}
return [null, 0];
})();
if (!header) {
throw new DecryptError.HeaderDecryptionFailed('Head key not match', DecryptError.CODE.CASE_213);
}
if (header.counter !== index) {
throw new DecryptError.InvalidHeader('Invalid header', DecryptError.CODE.CASE_214);
}
return header;
}
/**
* @param {!Uint8Array} encrypted_header - encrypted header
* @param {!derived.HeadKey} next_head_key
* @returns {message.Header}
*/
static try_next_head_key(encrypted_header, next_head_key) {
return RecvChain._try_head_key(0, RecvChain.MAX_COUNTER_GAP, encrypted_header, next_head_key);
}
/**
* @param {!Uint8Array} encrypted_header - encrypted header
* @returns {message.Header}
*/
try_head_key(encrypted_header) {
const final_count = this.final_count;
const start_index = this.message_keys.length > 0
? this.message_keys[0].counter
: this.chain_key.idx;
const end_index = final_count !== null
? final_count - 1
: start_index + RecvChain.MAX_COUNTER_GAP;
return RecvChain._try_head_key(start_index, end_index, encrypted_header, this.head_key);
}
/**
* @param {!message.Envelope} envelope
* @param {!message.Header} header
* @param {!Uint8Array} cipher_text
* @returns {Uint8Array}
*/
try_message_keys(envelope, header, cipher_text) {
TypeUtil.assert_is_instance(Envelope, envelope);
TypeUtil.assert_is_instance(Header, header);
TypeUtil.assert_is_instance(Uint8Array, cipher_text);
if (this.message_keys[0] && this.message_keys[0].counter > header.counter) {
const message = `Message too old. Counter for oldest staged chain key is '${this.message_keys[0].counter}' while message counter is '${header.counter}'.`;
throw new DecryptError.OutdatedMessage(message, DecryptError.CODE.CASE_208);
}
const idx = this.message_keys.findIndex((mk) => {
return mk.counter === header.counter;
});
if (idx === -1) {
throw new DecryptError.DuplicateMessage(null, DecryptError.CODE.CASE_209);
}
const mk = this.message_keys.splice(idx, 1)[0];
if (!envelope.verify(mk.mac_key)) {
const message = `Envelope verification failed for message with counter behind. Message index is '${header.counter}' while receive chain index is '${this.chain_key.idx}'.`;
throw new DecryptError.InvalidSignature(message, DecryptError.CODE.CASE_210);
}
return mk.decrypt(cipher_text);
}
/**
* @param {!message.Header} header
* @returns {Array<session.ChainKey>|session.MessageKeys}
*/
stage_message_keys(header) {
TypeUtil.assert_is_instance(Header, header);
const num = header.counter - this.chain_key.idx;
if (num > RecvChain.MAX_COUNTER_GAP) {
if (this.chain_key.idx === 0) {
throw new DecryptError.TooDistantFuture('Skipped too many message at the beginning of a receive chain.', DecryptError.CODE.CASE_211);
}
throw new DecryptError.TooDistantFuture(`Skipped too many message within a used receive chain. Receive chain counter is '${this.chain_key.idx}'`, DecryptError.CODE.CASE_212);
}
let keys = [];
let chk = this.chain_key;
for (let i = 0; i <= num - 1; i++) {
keys.push(chk.message_keys());
chk = chk.next();
}
const mk = chk.message_keys();
return [chk, mk, keys];
}
/**
* @param {!Array<session.MessageKeys>} keys
* @returns {void}
*/
commit_message_keys(keys) {
TypeUtil.assert_is_instance(Array, keys);
keys.map((k) => TypeUtil.assert_is_instance(MessageKeys, k));
if (keys.length > RecvChain.MAX_COUNTER_GAP) {
throw new ProteusError(`Number of message keys (${keys.length}) exceed message chain counter gap (${RecvChain.MAX_COUNTER_GAP}).`, ProteusError.prototype.CODE.CASE_103);
}
const excess = this.message_keys.length + keys.length - RecvChain.MAX_COUNTER_GAP;
for (let i = 0; i <= excess - 1; i++) {
this.message_keys.shift();
}
keys.map((k) => this.message_keys.push(k));
if (keys.length > RecvChain.MAX_COUNTER_GAP) {
throw new ProteusError(`Skipped message keys which exceed the message chain counter gap (${RecvChain.MAX_COUNTER_GAP}).`, ProteusError.prototype.CODE.CASE_104);
}
}
/**
* @param {!CBOR.Encoder} e
* @returns {Array<CBOR.Encoder>}
*/
encode(e) {
e.object(5);
e.u8(0);
this.chain_key.encode(e);
e.u8(1);
this.ratchet_key.encode(e);
e.u8(2);
this.head_key.encode(e);
e.u8(3);
if (this.final_count !== null) {
e.u32(this.final_count);
} else {
e.null();
}
e.u8(4);
e.array(this.message_keys.length);
return this.message_keys.map((k) => k.encode(e));
}
/**
* @param {!CBOR.Decoder} d
* @returns {RecvChain}
*/
static decode(d) {
TypeUtil.assert_is_instance(CBOR.Decoder, d);
const self = ClassUtil.new_instance(RecvChain);
const nprops = d.object();
for (let i = 0; i <= nprops - 1; i++) {
switch (d.u8()) {
case 0: {
self.chain_key = ChainKey.decode(d);
break;
}
case 1: {
self.ratchet_key = PublicKey.decode(d);
break;
}
case 2: {
self.head_key = HeadKey.decode(d);
break;
}
case 3: {
self.final_count = d.optional(() => d.u32());
break;
}
case 4: {
self.message_keys = [];
let len = d.array();
while (len--) {
self.message_keys.push(MessageKeys.decode(d));
}
break;
}
default: {
d.skip();
}
}
}
TypeUtil.assert_is_instance(ChainKey, self.chain_key);
TypeUtil.assert_is_instance(PublicKey, self.ratchet_key);
TypeUtil.assert_is_instance(HeadKey, self.head_key);
TypeUtil.assert_is_instance(Array, self.message_keys);
return self;
}
}
/** @type {number} */
RecvChain.MAX_COUNTER_GAP = 1000;
module.exports = RecvChain;