UNPKG

@wireapp/cryptobox

Version:

High-level API with persistent storage for Proteus.

318 lines 13.5 kB
"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