@wireapp/cryptobox
Version:
High-level API with persistent storage for Proteus.
318 lines • 13.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Cryptobox = void 0;
const lru_cache_1 = require("@wireapp/lru-cache");
const priority_queue_1 = require("@wireapp/priority-queue");
const proteus_1 = require("@wireapp/proteus");
const bazinga64_1 = require("bazinga64");
const events_1 = require("events");
const CryptoboxSession_1 = require("./CryptoboxSession");
const error_1 = require("./error/");
const CryptoboxCRUDStore_1 = require("./store/CryptoboxCRUDStore");
const DEFAULT_CAPACITY = 1000;
const { version } = require('../package.json');
var TOPIC;
(function (TOPIC) {
TOPIC["NEW_PREKEYS"] = "new-prekeys";
TOPIC["NEW_SESSION"] = "new-session";
})(TOPIC || (TOPIC = {}));
class Cryptobox extends events_1.EventEmitter {
constructor(engine, minimumAmountOfPreKeys = 1) {
super();
this.queues = new lru_cache_1.LRUCache(DEFAULT_CAPACITY);
if (minimumAmountOfPreKeys > proteus_1.keys.PreKey.MAX_PREKEY_ID) {
minimumAmountOfPreKeys = proteus_1.keys.PreKey.MAX_PREKEY_ID;
}
this.cachedSessions = new lru_cache_1.LRUCache(DEFAULT_CAPACITY);
this.minimumAmountOfPreKeys = minimumAmountOfPreKeys;
this.store = new CryptoboxCRUDStore_1.CryptoboxCRUDStore(engine);
}
get_session_queue(sessionId) {
let queue = this.queues.get(sessionId);
if (!queue) {
queue = new priority_queue_1.PriorityQueue({ maxRetries: 0 });
this.queues.set(sessionId, queue);
}
return queue;
}
save_session_in_cache(session) {
this.cachedSessions.set(session.id, session);
return session;
}
load_session_from_cache(sessionId) {
return this.cachedSessions.get(sessionId);
}
remove_session_from_cache(sessionId) {
this.cachedSessions.delete(sessionId);
}
async create(entropyData) {
await (0, proteus_1.init)();
(0, proteus_1.addEntropy)(entropyData);
await this.create_new_identity();
await this.create_last_resort_prekey();
return this.init(false);
}
getIdentity() {
if (!this.identity) {
throw new error_1.CryptoboxError('Failed to load local identity');
}
return this.identity;
}
async load() {
await (0, proteus_1.init)();
const identity = await this.store.load_identity();
if (!identity) {
throw new error_1.CryptoboxError('Failed to load local identity');
}
this.identity = identity;
(0, proteus_1.addEntropy)(new Uint8Array(identity.serialise()));
const preKeysFromStorage = await this.store.load_prekeys();
const lastResortPreKey = preKeysFromStorage.find(preKey => preKey.key_id === proteus_1.keys.PreKey.MAX_PREKEY_ID);
if (!lastResortPreKey) {
throw new error_1.CryptoboxError('Failed to load last resort PreKey');
}
this.lastResortPreKey = lastResortPreKey;
return this.init(true);
}
async init(publishPrekeys) {
await this.refill_prekeys(publishPrekeys);
const prekeys = await this.store.load_prekeys();
return prekeys.sort((a, b) => a.key_id - b.key_id);
}
async get_serialized_last_resort_prekey() {
if (this.lastResortPreKey) {
return this.serialize_prekey(this.lastResortPreKey);
}
throw new error_1.CryptoboxError('No last resort PreKey available.');
}
get_prekey(prekey_id = proteus_1.keys.PreKey.MAX_PREKEY_ID) {
return this.store.load_prekey(prekey_id);
}
async get_prekey_bundle(preKeyId = proteus_1.keys.PreKey.MAX_PREKEY_ID) {
const preKey = await this.get_prekey(preKeyId);
if (!this.identity) {
throw new error_1.CryptoboxError('No local identity available.');
}
if (!preKey) {
throw new error_1.CryptoboxError(`PreKey with ID "${preKeyId}" cannot be found.`);
}
return new proteus_1.keys.PreKeyBundle(this.identity.public_key, preKey);
}
async get_serialized_standard_prekeys() {
const prekeys = await this.store.load_prekeys();
return prekeys
.filter((preKey) => {
const isLastResortPreKey = preKey.key_id === proteus_1.keys.PreKey.MAX_PREKEY_ID;
return !isLastResortPreKey;
})
.map((preKey) => this.serialize_prekey(preKey));
}
publish_event(topic, event) {
this.emit(topic, event);
}
publish_prekeys(newPreKeys) {
if (newPreKeys.length > 0) {
this.publish_event(Cryptobox.TOPIC.NEW_PREKEYS, newPreKeys);
}
}
publish_session_id(session) {
this.publish_event(Cryptobox.TOPIC.NEW_SESSION, session.id);
}
async refill_prekeys(publishPrekeys = true) {
const prekeys = await this.store.load_prekeys();
const missingAmount = Math.max(0, this.minimumAmountOfPreKeys - prekeys.length);
if (missingAmount > 0) {
const startId = prekeys.reduce((currentHighestValue, currentPreKey) => {
const isLastResortPreKey = currentPreKey.key_id === proteus_1.keys.PreKey.MAX_PREKEY_ID;
return isLastResortPreKey ? currentHighestValue : Math.max(currentPreKey.key_id + 1, currentHighestValue);
}, 0);
const newPreKeys = await this.new_prekeys(startId, missingAmount);
if (publishPrekeys) {
this.publish_prekeys(newPreKeys);
}
prekeys.push(...newPreKeys);
}
return prekeys;
}
async create_new_identity() {
await this.store.delete_all();
const identity = new proteus_1.keys.IdentityKeyPair();
return this.save_identity(identity);
}
save_identity(identity) {
this.identity = identity;
return this.store.save_identity(identity);
}
async session_from_prekey(sessionId, preKeyBundle) {
try {
return await this.session_load(sessionId);
}
catch (sessionLoadError) {
let bundle;
try {
bundle = proteus_1.keys.PreKeyBundle.deserialise(preKeyBundle);
}
catch (error) {
const message = `PreKey bundle for session "${sessionId}" has an unsupported format: ${error.message}`;
throw new error_1.InvalidPreKeyFormatError(message);
}
if (this.identity) {
const session = proteus_1.session.Session.init_from_prekey(this.identity, bundle);
const cryptobox_session = new CryptoboxSession_1.CryptoboxSession(sessionId, session);
return this.session_save(cryptobox_session);
}
throw new error_1.CryptoboxError('No local identity available.');
}
}
async session_from_message(sessionId, envelope) {
const env = proteus_1.message.Envelope.deserialise(envelope);
if (this.identity) {
const [session, decrypted] = await proteus_1.session.Session.init_from_message(this.identity, this.store, env);
const cryptoBoxSession = new CryptoboxSession_1.CryptoboxSession(sessionId, session);
return [cryptoBoxSession, decrypted];
}
throw new error_1.CryptoboxError('No local identity available.');
}
async session_load(sessionId) {
const cachedSession = this.load_session_from_cache(sessionId);
if (cachedSession) {
return cachedSession;
}
if (this.identity) {
const session = await this.store.read_session(this.identity, sessionId);
const cryptoboxSession = new CryptoboxSession_1.CryptoboxSession(sessionId, session);
return this.save_session_in_cache(cryptoboxSession);
}
throw new error_1.CryptoboxError('No local identity available.');
}
async session_save(session) {
await this.store.create_session(session.id, session.session);
return this.save_session_in_cache(session);
}
async session_update(session) {
await this.store.update_session(session.id, session.session);
return this.save_session_in_cache(session);
}
session_delete(sessionId) {
this.remove_session_from_cache(sessionId);
return this.store.delete_session(sessionId);
}
async create_last_resort_prekey() {
this.lastResortPreKey = proteus_1.keys.PreKey.last_resort();
const preKeys = await this.store.save_prekeys([this.lastResortPreKey]);
return preKeys[0];
}
serialize_prekey(prekey) {
if (this.identity) {
return new proteus_1.keys.PreKeyBundle(this.identity.public_key, prekey).serialised_json();
}
throw new error_1.CryptoboxError('No local identity available.');
}
async new_prekeys(start, size = 0) {
if (size === 0) {
return [];
}
const newPreKeys = proteus_1.keys.PreKey.generate_prekeys(start, size);
return this.store.save_prekeys(newPreKeys);
}
async encrypt(sessionId, payload, preKeyBundle) {
return this.get_session_queue(sessionId).add(async () => {
const session = preKeyBundle
? await this.session_from_prekey(sessionId, preKeyBundle)
: await this.session_load(sessionId);
const encryptedBuffer = session.encrypt(payload);
await this.session_update(session);
return encryptedBuffer;
});
}
async decrypt(sessionId, ciphertext) {
if (ciphertext.byteLength === 0) {
throw new error_1.DecryptionError('Cannot decrypt an empty ArrayBuffer');
}
return this.get_session_queue(sessionId).add(async () => {
let session;
let decryptedMessage;
let isNewSession = false;
try {
session = await this.session_load(sessionId);
}
catch (error) {
isNewSession = true;
[session, decryptedMessage] = await this.session_from_message(sessionId, ciphertext);
this.publish_session_id(session);
await this.session_save(session);
}
if (!isNewSession) {
decryptedMessage = await session.decrypt(ciphertext, this.store);
await this.session_update(session);
}
await this.refill_prekeys(true);
return decryptedMessage;
});
}
deleteData() {
this.cachedSessions = new lru_cache_1.LRUCache(DEFAULT_CAPACITY);
this.identity = undefined;
this.lastResortPreKey = undefined;
this.queues = new lru_cache_1.LRUCache(DEFAULT_CAPACITY);
return this.store.delete_all();
}
importIdentity(payload) {
const identityBuffer = bazinga64_1.Decoder.fromBase64(payload).asBytes.buffer;
const identity = proteus_1.keys.IdentityKeyPair.deserialise(identityBuffer);
return this.save_identity(identity);
}
importPreKeys(serializedPreKeys) {
const proteusPreKeys = Object.values(serializedPreKeys).map(preKey => {
const preKeyBuffer = bazinga64_1.Decoder.fromBase64(preKey).asBytes.buffer;
const proteusPreKey = proteus_1.keys.PreKey.deserialise(preKeyBuffer);
if (proteusPreKey.key_id === proteus_1.keys.PreKey.MAX_PREKEY_ID) {
this.lastResortPreKey = proteusPreKey;
}
return proteusPreKey;
});
return this.store.save_prekeys(proteusPreKeys);
}
async importSessions(serializedSessions) {
for (const sessionId in serializedSessions) {
const serializedSession = serializedSessions[sessionId];
const sessionBuffer = bazinga64_1.Decoder.fromBase64(serializedSession).asBytes.buffer;
const proteusSession = proteus_1.session.Session.deserialise(this.identity, sessionBuffer);
const cryptoBoxSession = new CryptoboxSession_1.CryptoboxSession(sessionId, proteusSession);
await this.session_save(cryptoBoxSession);
}
}
async deserialize(payload) {
await this.deleteData();
await this.importIdentity(payload.identity);
await this.importPreKeys(payload.prekeys);
await this.importSessions(payload.sessions);
return this.refill_prekeys(true);
}
async serialize() {
const toBase64 = (buffer) => bazinga64_1.Encoder.toBase64(buffer).asString;
const data = {
identity: '',
prekeys: {},
sessions: {},
};
const identity = await this.store.load_identity();
if (identity) {
data.identity = toBase64(identity.serialise());
const sessions = await this.store.read_sessions(identity);
for (const sessionId in sessions) {
const storedSession = sessions[sessionId];
data.sessions[sessionId] = toBase64(storedSession.serialise());
}
}
const storedPreKeys = await this.store.load_prekeys();
for (const storedPreKey of storedPreKeys) {
data.prekeys[storedPreKey.key_id] = toBase64(storedPreKey.serialise());
}
return data;
}
}
exports.Cryptobox = Cryptobox;
Cryptobox.VERSION = version;
Cryptobox.TOPIC = TOPIC;
//# sourceMappingURL=Cryptobox.js.map