UNPKG

@wireapp/cryptobox

Version:

High-level API with persistent storage for Proteus.

388 lines (387 loc) 19.5 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [0, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); var ProteusKeys = require("@wireapp/proteus/dist/keys/root"); var ProteusMessage = require("@wireapp/proteus/dist/message/root"); var ProteusSession = require("@wireapp/proteus/dist/session/root"); var root_1 = require("./error/root"); var CryptoboxSession_1 = require("./CryptoboxSession"); var DecryptionError_1 = require("./DecryptionError"); var InvalidPreKeyFormatError_1 = require("./InvalidPreKeyFormatError"); var root_2 = require("./store/root"); var lru_cache_1 = require("@wireapp/lru-cache"); var EventEmitter = require("events"); var PQueue = require("p-queue"); var logdown = require('logdown'); var Cryptobox = (function (_super) { __extends(Cryptobox, _super); function Cryptobox(cryptoBoxStore, minimumAmountOfPreKeys) { if (minimumAmountOfPreKeys === void 0) { minimumAmountOfPreKeys = 1; } var _this = _super.call(this) || this; _this.logger = logdown('@wireapp/cryptobox/Cryptobox', { logger: console, markdown: false, }); _this.queue = new PQueue({ concurrency: 1 }); _this.VERSION = ''; if (!cryptoBoxStore) { throw new Error("You cannot initialize Cryptobox without a storage component."); } if (minimumAmountOfPreKeys > ProteusKeys.PreKey.MAX_PREKEY_ID) { minimumAmountOfPreKeys = ProteusKeys.PreKey.MAX_PREKEY_ID; } _this.cachedPreKeys = []; _this.cachedSessions = new lru_cache_1.default(1000); _this.minimumAmountOfPreKeys = minimumAmountOfPreKeys; _this.store = cryptoBoxStore; _this.pk_store = new root_2.ReadOnlyStore(_this.store); var storageEngine = cryptoBoxStore.constructor.name; _this.logger.log("Constructed Cryptobox. Minimum amount of PreKeys is \"" + minimumAmountOfPreKeys + "\". Storage engine is \"" + storageEngine + "\"."); return _this; } Cryptobox.prototype.save_session_in_cache = function (session) { this.logger.log("Saving Session with ID \"" + session.id + "\" in cache..."); this.cachedSessions.set(session.id, session); return session; }; Cryptobox.prototype.load_session_from_cache = function (session_id) { this.logger.log("Trying to load Session with ID \"" + session_id + "\" from cache..."); return this.cachedSessions.get(session_id); }; Cryptobox.prototype.remove_session_from_cache = function (session_id) { this.logger.log("Removing Session with ID \"" + session_id + "\" from cache..."); this.cachedSessions.delete(session_id); }; Cryptobox.prototype.create = function () { var _this = this; this.logger.log("Initializing Cryptobox. Creating local identity..."); return this.create_new_identity() .then(function (identity) { _this.identity = identity; _this.logger.log("Initialized Cryptobox with new local identity. Fingerprint is \"" + identity.public_key.fingerprint() + "\".", _this.identity); return _this.create_last_resort_prekey(); }) .then(function (lastResortPreKey) { _this.cachedPreKeys = [lastResortPreKey]; _this.logger.log("Created Last Resort PreKey with ID \"" + lastResortPreKey.key_id + "\".", lastResortPreKey); return _this.init(); }); }; Cryptobox.prototype.load = function () { var _this = this; this.logger.log("Initializing Cryptobox. Loading local identity..."); return this.store .load_identity() .then(function (identity) { if (identity) { _this.logger.log("Initialized Cryptobox with existing local identity. Fingerprint is \"" + identity.public_key.fingerprint() + "\".", _this.identity); _this.identity = identity; _this.logger.log("Loading PreKeys..."); return _this.store.load_prekeys(); } throw new root_1.CryptoboxError('Failed to load local identity'); }) .then(function (preKeysFromStorage) { var lastResortPreKey = preKeysFromStorage.find(function (preKey) { return preKey.key_id === ProteusKeys.PreKey.MAX_PREKEY_ID; }); if (lastResortPreKey) { _this.logger.log("Loaded Last Resort PreKey with ID \"" + lastResortPreKey.key_id + "\".", lastResortPreKey); _this.lastResortPreKey = lastResortPreKey; _this.logger.log("Loaded \"" + (_this.minimumAmountOfPreKeys - 1) + "\" standard PreKeys..."); _this.cachedPreKeys = preKeysFromStorage; return _this.init(); } throw new root_1.CryptoboxError('Failed to load last resort PreKey'); }); }; Cryptobox.prototype.init = function () { var _this = this; return this.refill_prekeys().then(function () { var ids = _this.cachedPreKeys.map(function (preKey) { return preKey.key_id.toString(); }); _this.logger.log("Initialized Cryptobox with a total amount of \"" + _this.cachedPreKeys.length + "\" PreKey(s) (" + ids.join(', ') + ").", _this.cachedPreKeys); return _this.cachedPreKeys.sort(function (a, b) { return a.key_id - b.key_id; }); }); }; Cryptobox.prototype.get_serialized_last_resort_prekey = function () { if (this.lastResortPreKey) { return Promise.resolve(this.serialize_prekey(this.lastResortPreKey)); } return Promise.reject(new root_1.CryptoboxError('No last resort PreKey available.')); }; Cryptobox.prototype.get_serialized_standard_prekeys = function () { var _this = this; var standardPreKeys = this.cachedPreKeys .filter(function (preKey) { var isLastResortPreKey = preKey.key_id === ProteusKeys.PreKey.MAX_PREKEY_ID; return !isLastResortPreKey; }) .map(function (preKey) { return _this.serialize_prekey(preKey); }); return Promise.resolve(standardPreKeys); }; Cryptobox.prototype.publish_event = function (topic, event) { this.emit(topic, event); this.logger.log("Published event \"" + topic + "\".", event); }; Cryptobox.prototype.publish_prekeys = function (newPreKeys) { if (newPreKeys.length > 0) { this.publish_event(Cryptobox.TOPIC.NEW_PREKEYS, newPreKeys); } }; Cryptobox.prototype.publish_session_id = function (session) { this.publish_event(Cryptobox.TOPIC.NEW_SESSION, session.id); }; Cryptobox.prototype.refill_prekeys = function () { var _this = this; return Promise.resolve() .then(function () { var missingAmount = Math.max(0, _this.minimumAmountOfPreKeys - _this.cachedPreKeys.length); if (missingAmount > 0) { var startId = _this.cachedPreKeys.reduce(function (currentHighestValue, currentPreKey) { var isLastResortPreKey = currentPreKey.key_id === ProteusKeys.PreKey.MAX_PREKEY_ID; return isLastResortPreKey ? currentHighestValue : Math.max(currentPreKey.key_id + 1, currentHighestValue); }, 0); _this.logger.warn("There are not enough PreKeys in the storage. Generating \"" + missingAmount + "\" new PreKey(s), starting from ID \"" + startId + "\"..."); return _this.new_prekeys(startId, missingAmount); } return []; }) .then(function (newPreKeys) { if (newPreKeys.length > 0) { _this.logger.log("Generated PreKeys from ID \"" + newPreKeys[0].key_id + "\" to ID \"" + newPreKeys[newPreKeys.length - 1].key_id + "\"."); _this.cachedPreKeys = _this.cachedPreKeys.concat(newPreKeys); } return newPreKeys; }); }; Cryptobox.prototype.create_new_identity = function () { var _this = this; return Promise.resolve() .then(function () { return _this.store.delete_all(); }) .then(function () { return ProteusKeys.IdentityKeyPair.new(); }) .then(function (identity) { _this.logger.warn("Cleaned cryptographic items prior to saving a new local identity.", identity); return _this.store.save_identity(identity); }); }; Cryptobox.prototype.session_from_prekey = function (session_id, pre_key_bundle) { var _this = this; return this.session_load(session_id).catch(function (sessionLoadError) { _this.logger.warn("Creating new session because session with ID \"" + session_id + "\" could not be loaded: " + sessionLoadError.message); var bundle; try { bundle = ProteusKeys.PreKeyBundle.deserialise(pre_key_bundle); } catch (error) { throw new InvalidPreKeyFormatError_1.default("PreKey bundle for session \"" + session_id + "\" has an unsupported format: " + error.message); } if (_this.identity) { return ProteusSession.Session.init_from_prekey(_this.identity, bundle).then(function (session) { var cryptobox_session = new CryptoboxSession_1.default(session_id, _this.pk_store, session); return _this.session_save(cryptobox_session); }); } return Promise.reject(new root_1.CryptoboxError('No local identity available.')); }); }; Cryptobox.prototype.session_from_message = function (session_id, envelope) { var _this = this; var env = ProteusMessage.Envelope.deserialise(envelope); if (this.identity) { return ProteusSession.Session.init_from_message(this.identity, this.pk_store, env).then(function (tuple) { var session = tuple[0]; var decrypted = tuple[1]; var cryptoBoxSession = new CryptoboxSession_1.default(session_id, _this.pk_store, session); return [cryptoBoxSession, decrypted]; }); } return Promise.reject(new root_1.CryptoboxError('No local identity available.')); }; Cryptobox.prototype.session_load = function (session_id) { var _this = this; this.logger.log("Trying to load Session with ID \"" + session_id + "\"..."); var cachedSession = this.load_session_from_cache(session_id); if (cachedSession) { return Promise.resolve(cachedSession); } if (this.identity) { return this.store.read_session(this.identity, session_id).then(function (session) { var cryptobox_session = new CryptoboxSession_1.default(session_id, _this.pk_store, session); return _this.save_session_in_cache(cryptobox_session); }); } throw new root_1.CryptoboxError('No local identity available.'); }; Cryptobox.prototype.session_cleanup = function (session) { var _this = this; return this.pk_store .get_prekeys() .then(function (pks) { var preKeyDeletionPromises = pks.map(function (pk) { return _this.store.delete_prekey(pk.key_id); }); return Promise.all(preKeyDeletionPromises); }) .then(function (deletedPreKeyIds) { _this.cachedPreKeys = _this.cachedPreKeys.filter(function (preKey) { return !deletedPreKeyIds.includes(preKey.key_id); }); _this.pk_store.release_prekeys(deletedPreKeyIds); return _this.refill_prekeys(); }) .then(function (newPreKeys) { _this.publish_prekeys(newPreKeys); return _this.save_session_in_cache(session); }) .then(function () { return session; }); }; Cryptobox.prototype.session_save = function (session) { var _this = this; return this.store.create_session(session.id, session.session).then(function () { return _this.session_cleanup(session); }); }; Cryptobox.prototype.session_update = function (session) { var _this = this; return this.store.update_session(session.id, session.session).then(function () { return _this.session_cleanup(session); }); }; Cryptobox.prototype.session_delete = function (session_id) { this.remove_session_from_cache(session_id); return this.store.delete_session(session_id); }; Cryptobox.prototype.create_last_resort_prekey = function () { var _this = this; return Promise.resolve() .then(function () { return __awaiter(_this, void 0, void 0, function () { var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: this.logger.log("Creating Last Resort PreKey with ID \"" + ProteusKeys.PreKey.MAX_PREKEY_ID + "\"..."); _a = this; return [4, ProteusKeys.PreKey.last_resort()]; case 1: _a.lastResortPreKey = _b.sent(); return [2, this.store.save_prekeys([this.lastResortPreKey])]; } }); }); }) .then(function (preKeys) { return preKeys[0]; }); }; Cryptobox.prototype.serialize_prekey = function (prekey) { if (this.identity) { return ProteusKeys.PreKeyBundle.new(this.identity.public_key, prekey).serialised_json(); } throw new root_1.CryptoboxError('No local identity available.'); }; Cryptobox.prototype.new_prekeys = function (start, size) { var _this = this; if (size === void 0) { size = 0; } if (size === 0) { return Promise.resolve([]); } return Promise.resolve() .then(function () { return ProteusKeys.PreKey.generate_prekeys(start, size); }) .then(function (newPreKeys) { return _this.store.save_prekeys(newPreKeys); }); }; Cryptobox.prototype.encrypt = function (session_id, payload, pre_key_bundle) { var _this = this; var encryptedBuffer; var loadedSession; return this.queue.add(function () { return Promise.resolve() .then(function () { if (pre_key_bundle) { return _this.session_from_prekey(session_id, pre_key_bundle); } return _this.session_load(session_id); }) .then(function (session) { loadedSession = session; return loadedSession.encrypt(payload); }) .then(function (encrypted) { encryptedBuffer = encrypted; return _this.session_update(loadedSession); }) .then(function () { return encryptedBuffer; }); }); }; Cryptobox.prototype.decrypt = function (session_id, ciphertext) { var _this = this; var is_new_session = false; var message; var session; if (ciphertext.byteLength === 0) { return Promise.reject(new DecryptionError_1.default('Cannot decrypt an empty ArrayBuffer.')); } return this.queue.add(function () { return (_this.session_load(session_id) .catch(function () { return _this.session_from_message(session_id, ciphertext); }) .then(function (value) { var decrypted_message; if (value[0] !== undefined) { session = value[0], decrypted_message = value[1]; _this.publish_session_id(session); is_new_session = true; return decrypted_message; } session = value; return session.decrypt(ciphertext); }) .then(function (decrypted_message) { message = decrypted_message; if (is_new_session) { return _this.session_save(session); } return _this.session_update(session); }) .then(function () { return message; })); }); }; Cryptobox.TOPIC = { NEW_PREKEYS: 'new-prekeys', NEW_SESSION: 'new-session', }; return Cryptobox; }(EventEmitter)); Cryptobox.prototype.VERSION = require('../../package.json').version; exports.default = Cryptobox;