@wireapp/cryptobox
Version:
High-level API with persistent storage for Proteus.
388 lines (387 loc) • 19.5 kB
JavaScript
"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;