UNPKG

@wireapp/cryptobox

Version:

High-level API with persistent storage for Proteus.

207 lines (177 loc) 7.58 kB
/* * 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/. * */ /* eslint-disable no-magic-numbers */ const {StoreEngine} = require('@wireapp/store-engine'); const cryptobox = typeof window === 'object' ? window.cryptobox : require('@wireapp/cryptobox'); const Proteus = typeof window === 'object' ? window.Proteus : require('@wireapp/proteus'); describe('cryptobox.CryptoboxSession', () => { let sodium = undefined; beforeAll(async done => { if (typeof window === 'object') { sodium = window.sodium; done(); } else { const _sodium = require('libsodium-wrappers-sumo'); await _sodium.ready; sodium = _sodium; done(); } }); describe('PreKey messages', () => { let alice = undefined; let bob = undefined; async function generatePreKeys(cryptobox_store) { // Generate one PreKey and the Last Resort PreKey const pre_keys = await Proteus.keys.PreKey.generate_prekeys(0, 1); pre_keys.push(await Proteus.keys.PreKey.new(Proteus.keys.PreKey.MAX_PREKEY_ID)); const promises = pre_keys.map(pre_key => cryptobox_store.save_prekey(pre_key)); return Promise.all(promises) .then(() => { return pre_keys; }) .catch(error => { console.log('Error in Test PreKey generation.'); throw error; }); } function setupAliceToBob(preKeyId) { // 1. Bob creates and "uploads" a PreKey, which can be "consumed" by Alice return generatePreKeys(bob.cryptobox_store) .then(() => { return bob.cryptobox_store.load_prekey(preKeyId); }) .then(async prekey => { // 2. Alice takes Bob's PreKey bundle to initiate a session bob.bundle = await Proteus.keys.PreKeyBundle.new(bob.identity.public_key, prekey); return Proteus.session.Session.init_from_prekey(alice.identity, bob.bundle); }) .then(session => { // 3. Alice upgrades the basic Proteus session into a high-level Cryptobox session return new cryptobox.CryptoboxSession('bobs_client_id', alice.pre_key_store, session); }) .catch(error => { console.log('Error in Test Alice setup!', error); throw error; }); } beforeEach(async done => { const aliceEngine = new StoreEngine.MemoryEngine(); await aliceEngine.init('cache'); alice = { cryptobox_store: new cryptobox.store.CryptoboxCRUDStore(aliceEngine), identity: await Proteus.keys.IdentityKeyPair.new(), }; alice.pre_key_store = new cryptobox.store.ReadOnlyStore(alice.cryptobox_store); const bobEngine = new StoreEngine.MemoryEngine(); await bobEngine.init('cache'); bob = { cryptobox_store: new cryptobox.store.CryptoboxCRUDStore(bobEngine), identity: await Proteus.keys.IdentityKeyPair.new(), }; bob.pre_key_store = new cryptobox.store.ReadOnlyStore(bob.cryptobox_store); done(); }); describe('fingerprints', () => { it('returns the local & remote fingerpint', done => { setupAliceToBob(0) .then(sessionWithBob => { expect(sessionWithBob.fingerprint_local()).toBe(alice.identity.public_key.fingerprint()); expect(sessionWithBob.fingerprint_remote()).toBe(bob.identity.public_key.fingerprint()); done(); }) .catch(done.fail); }); }); describe('encryption & decryption', () => { const plaintext = 'Hello Bob, I am Alice.'; it('encrypts a message from Alice which can be decrypted by Bob', done => { Promise.resolve() .then(() => { return setupAliceToBob(0); }) .then(sessionWithBob => { return sessionWithBob.encrypt(plaintext); }) .then(serialisedCipherText => { const envelope = Proteus.message.Envelope.deserialise(serialisedCipherText); expect(bob.pre_key_store.prekeys.length).toBe(0); return Proteus.session.Session.init_from_message(bob.identity, bob.pre_key_store, envelope); }) .then(proteusSession => { // When Bob decrypts a PreKey message, he knows that one of his PreKeys has been "consumed" expect(bob.pre_key_store.prekeys.length).toBe(1); const decryptedBuffer = proteusSession[1]; const decrypted = sodium.to_string(decryptedBuffer); expect(decrypted).toBe(plaintext); done(); }) .catch(done.fail); }); it("doesn't remove the last resort PreKey if consumed", done => { Promise.resolve() .then(() => { return setupAliceToBob(Proteus.keys.PreKey.MAX_PREKEY_ID); }) .then(sessionWithBob => { return sessionWithBob.encrypt(plaintext); }) .then(serialisedCipherText => { const envelope = Proteus.message.Envelope.deserialise(serialisedCipherText); expect(bob.pre_key_store.prekeys.length).toBe(0); return Proteus.session.Session.init_from_message(bob.identity, bob.pre_key_store, envelope); }) .then(proteusSession => { expect(bob.pre_key_store.prekeys.length).toBe(0); const decryptedBuffer = proteusSession[1]; const decrypted = sodium.to_string(decryptedBuffer); expect(decrypted).toBe(plaintext); done(); }) .catch(done.fail); }); }); }); describe('Session reset', () => { it('throws an error when a session is broken', async done => { const aliceEngine = new StoreEngine.MemoryEngine(); await aliceEngine.init('store-alice'); const bobEngine = new StoreEngine.MemoryEngine(); await bobEngine.init('store-bob'); const alice = new cryptobox.Cryptobox(new cryptobox.store.CryptoboxCRUDStore(aliceEngine), 5); await alice.create(); const bob = new cryptobox.Cryptobox(new cryptobox.store.CryptoboxCRUDStore(bobEngine), 5); await bob.create(); const preKeyBundle = await bob.get_serialized_standard_prekeys(); const deserialisedBundle = sodium.from_base64(preKeyBundle[1].key, sodium.base64_variants.ORIGINAL); const message = 'Hello Bob!'; const ciphertext = await alice.encrypt('alice-to-bob', message, deserialisedBundle.buffer); const decrypted = await bob.decrypt('bob-to-alice', ciphertext); expect(sodium.to_string(decrypted)).toBe(message); const deletedSessionId = await alice.session_delete('alice-to-bob'); expect(deletedSessionId).toBe('alice-to-bob'); try { await alice.encrypt('alice-to-bob', `I'm back!`); } catch (error) { expect(error).toEqual(jasmine.any(StoreEngine.error.RecordNotFoundError)); expect(error.code).toBe(2); } done(); }); }); });