UNPKG

@wireapp/cryptobox

Version:

High-level API with persistent storage for Proteus.

296 lines (263 loc) 9.78 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 no-magic-numbers: "off" */ const cryptobox = typeof window === 'object' ? window.cryptobox : require('@wireapp/cryptobox'); const Proteus = typeof window === 'object' ? window.Proteus : require('@wireapp/proteus'); const {StoreEngine} = require('@wireapp/store-engine'); describe('cryptobox.Cryptobox', () => { let sodium = undefined; let store = 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(); } }); beforeEach(async () => { const engine = new StoreEngine.MemoryEngine(); await engine.init('cache'); store = new cryptobox.store.CryptoboxCRUDStore(engine); }); describe('"decrypt"', () => { it("doesn't decrypt empty ArrayBuffers", done => { const box = new cryptobox.Cryptobox(store); const sessionId = 'sessionWithBob'; box .decrypt(sessionId, new ArrayBuffer(0)) .then(done.fail) .catch(error => { expect(error).toEqual(jasmine.any(cryptobox.DecryptionError)); done(); }); }); }); describe('"create"', () => { it('initializes a Cryptobox with a new identity and the last resort PreKey and saves these', done => { const box = new cryptobox.Cryptobox(store); box .create() .then(() => { expect(box.identity).toBeDefined(); return store.load_identity(); }) .then(identity => { expect(identity).toBeDefined(); expect(identity.public_key.fingerprint()).toBeDefined(); return store.load_prekey(Proteus.keys.PreKey.MAX_PREKEY_ID); }) .then(preKey => { expect(preKey.key_id).toBe(Proteus.keys.PreKey.MAX_PREKEY_ID); done(); }) .catch(done.fail); }); it('initializes a Cryptobox with a defined amount of PreKeys (including the last resort PreKey)', done => { const box = new cryptobox.Cryptobox(store, 10); box.create().then(() => { const preKeys = box.cachedPreKeys; const lastResortPreKey = preKeys.filter(preKey => preKey.key_id === Proteus.keys.PreKey.MAX_PREKEY_ID); expect(preKeys.length).toBe(10); expect(box.lastResortPreKey).toBeDefined(); expect(box.lastResortPreKey).toBe(lastResortPreKey[0]); done(); }); }); it('returns the current version', () => { expect(cryptobox.Cryptobox.prototype.VERSION).toBeDefined(); }); }); describe('"load"', () => { it('initializes a Cryptobox with an existing identity and the last resort PreKey', done => { let box = new cryptobox.Cryptobox(store, 4); let initialFingerPrint = undefined; box .create() .then(initialPreKeys => { const lastResortPreKey = initialPreKeys[initialPreKeys.length - 1]; expect(lastResortPreKey.key_id).toBe(Proteus.keys.PreKey.MAX_PREKEY_ID); const identity = box.identity; expect(identity).toBeDefined(); expect(identity.public_key.fingerprint()).toBeDefined(); initialFingerPrint = identity.public_key.fingerprint(); box = new cryptobox.Cryptobox(store); expect(box.identity).not.toBeDefined(); return box.load(); }) .then(() => { expect(box.identity.public_key.fingerprint()).toBe(initialFingerPrint); done(); }) .catch(done.fail); }); it('fails to initialize a Cryptobox of which the identity is missing', done => { let box = new cryptobox.Cryptobox(store); box .create() .then(() => { expect(box.identity).toBeDefined(); return store.delete_all(); }) .then(() => { return store.load_identity(); }) .then(identity => { expect(identity).not.toBeDefined(); box = new cryptobox.Cryptobox(store); expect(box.identity).not.toBeDefined(); return box.load(); }) .then(done.fail) .catch(error => { expect(error).toEqual(jasmine.any(cryptobox.error.CryptoboxError)); done(); }); }); it('fails to initialize a Cryptobox of which the last resort PreKey is missing', done => { let box = new cryptobox.Cryptobox(store); box .create() .then(() => { expect(box.identity).toBeDefined(); return store.delete_prekey(Proteus.keys.PreKey.MAX_PREKEY_ID); }) .then(() => { return store.load_prekey(Proteus.keys.PreKey.MAX_PREKEY_ID); }) .then(prekey => { expect(prekey).not.toBeDefined(); box = new cryptobox.Cryptobox(store); expect(box.identity).not.toBeDefined(); return box.load(); }) .then(done.fail) .catch(error => { expect(error).toEqual(jasmine.any(cryptobox.error.CryptoboxError)); done(); }); }); }); describe('PreKeys', () => { describe('"serialize_prekey"', () => { it('generates a JSON format', async done => { const box = new cryptobox.Cryptobox(store, 10); box.identity = await Proteus.keys.IdentityKeyPair.new(); const preKeyId = 72; const preKey = await Proteus.keys.PreKey.new(preKeyId); const json = box.serialize_prekey(preKey); expect(json.id).toBe(preKeyId); const decodedPreKeyBundleBuffer = sodium.from_base64(json.key, sodium.base64_variants.ORIGINAL).buffer; expect(decodedPreKeyBundleBuffer).toBeDefined(); done(); }); }); }); describe('Sessions', () => { let box = undefined; const sessionIdUnique = 'unique_identifier'; beforeEach(done => { box = new cryptobox.Cryptobox(store); box .create() .then(async () => { const bob = { identity: await Proteus.keys.IdentityKeyPair.new(), prekey: await Proteus.keys.PreKey.new(Proteus.keys.PreKey.MAX_PREKEY_ID), }; bob.bundle = await Proteus.keys.PreKeyBundle.new(bob.identity.public_key, bob.prekey); return Proteus.session.Session.init_from_prekey(box.identity, bob.bundle); }) .then(session => { const cryptoBoxSession = new cryptobox.CryptoboxSession(sessionIdUnique, box.pk_store, session); return box.session_save(cryptoBoxSession); }) .then(() => { done(); }) .catch(done.fail); }); describe('"session_from_prekey"', () => { it('creates a session from a valid PreKey format', done => { const remotePreKey = { id: 65535, key: 'pQABARn//wKhAFggY/Yre8URI2xF93otjO7pUJ3ZjP4aM+sNJb6pL6J+iYgDoQChAFggZ049puHgS2zw8wjJorpl+EG9/op9qEOANG7ecEU2hfwE9g==', }; const sessionId = 'session_id'; const decodedPreKeyBundleBuffer = sodium.from_base64(remotePreKey.key, sodium.base64_variants.ORIGINAL).buffer; box .session_from_prekey(sessionId, decodedPreKeyBundleBuffer) .then(session => { expect(session.id).toBe(sessionId); done(); }) .catch(done.fail); }); it('fails for outdated PreKey formats', done => { const remotePreKey = { id: 65535, key: 'hAEZ//9YIOxZw78oQCH6xKyAI7WqagtbvRZ/LaujG+T790hOTbf7WCDqAE5Dc75VfmYji6wEz976hJ2hYuODYE6pA59DNFn/KQ==', }; const sessionId = 'session_id'; const decodedPreKeyBundleBuffer = sodium.from_base64(remotePreKey.key, sodium.base64_variants.ORIGINAL).buffer; box .session_from_prekey(sessionId, decodedPreKeyBundleBuffer) .then(done.fail) .catch(error => { if (error instanceof cryptobox.InvalidPreKeyFormatError) { done(); } else { done.fail(); } }); }); }); describe('"session_load"', () => { it('loads a session from the cache', done => { spyOn(box, 'load_session_from_cache').and.callThrough(); spyOn(box.store, 'read_session').and.callThrough(); box .session_load(sessionIdUnique) .then(session => { expect(session.id).toBe(sessionIdUnique); expect(box.load_session_from_cache.calls.count()).toBe(1); done(); }) .catch(done.fail); }); }); describe('"encrypt"', () => { it('saves the session after successful encryption', done => { spyOn(box.store, 'update_session').and.callThrough(); box .encrypt(sessionIdUnique, 'Hello World.') .then(encryptedBuffer => { expect(encryptedBuffer).toBeDefined(); expect(box.store.update_session.calls.count()).toBe(1); done(); }) .catch(done.fail); }); }); }); });