From c93aff7ebea758d8fed8e64fa66940d657977a91 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Wed, 5 Dec 2018 15:45:02 +1100 Subject: [PATCH 1/9] Setup grunt/yarn etc for loki tests plus wrote first 2 simple libloki-protocol tests --- .eslintignore | 2 +- .gitignore | 1 + Gruntfile.js | 22 ++++++- libloki/test/.eslintrc | 26 ++++++++ libloki/test/_test.js | 66 +++++++++++++++++++ libloki/test/index.html | 39 +++++++++++ libloki/test/libloki-protocol_test.js | 60 +++++++++++++++++ .../test/in_memory_signal_protocol_store.js | 12 ++++ main.js | 14 +++- package.json | 1 + 10 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 libloki/test/.eslintrc create mode 100644 libloki/test/_test.js create mode 100644 libloki/test/index.html create mode 100644 libloki/test/libloki-protocol_test.js diff --git a/.eslintignore b/.eslintignore index c3446d5d0..65b9ea36c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,6 +11,7 @@ js/libloki.js js/util_worker.js js/libsignal-protocol-worker.js libtextsecure/components.js +libloki/test/test.js libtextsecure/test/test.js test/test.js @@ -25,4 +26,3 @@ test/blanket_mocha.js # TypeScript generated files ts/**/*.js - diff --git a/.gitignore b/.gitignore index 586046d86..cd183624e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ js/libtextsecure.js js/libloki.js libtextsecure/components.js libtextsecure/test/test.js +libloki/test/test.js stylesheets/*.css test/test.js diff --git a/Gruntfile.js b/Gruntfile.js index 5c392b946..a97bf9860 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -91,6 +91,14 @@ module.exports = grunt => { src: ['libloki/libloki-protocol.js'], dest: 'js/libloki.js', }, + lokitest: { + src: [ + 'node_modules/mocha/mocha.js', + 'node_modules/chai/chai.js', + 'libloki/test/_test.js', + ], + dest: 'libloki/test/test.js', + }, libtextsecuretest: { src: [ 'node_modules/jquery/dist/jquery.js', @@ -355,6 +363,17 @@ module.exports = grunt => { } ); + grunt.registerTask( + 'loki-unit-tests', + 'Run loki unit tests w/Electron', + function thisNeeded() { + const environment = grunt.option('env') || 'test-loki'; + const done = this.async(); + + runTests(environment, done); + } + ); + grunt.registerMultiTask( 'test-release', 'Test packaged releases', @@ -442,7 +461,8 @@ module.exports = grunt => { 'locale-patch', ]); grunt.registerTask('dev', ['default', 'watch']); - grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']); + grunt.registerTask('test', ['unit-tests', 'lib-unit-tests', 'loki-unit-tests']); + grunt.registerTask('test-loki', ['loki-unit-tests']); grunt.registerTask('date', ['gitinfo', 'getExpireTime']); grunt.registerTask('default', [ 'exec:build-protobuf', diff --git a/libloki/test/.eslintrc b/libloki/test/.eslintrc new file mode 100644 index 000000000..29971976c --- /dev/null +++ b/libloki/test/.eslintrc @@ -0,0 +1,26 @@ +{ + "env": { + "browser": true, + "node": false, + "mocha": true + }, + "parserOptions": { + "sourceType": "script" + }, + "rules": { + "strict": "off", + "more/no-then": "off" + }, + "globals": { + "assert": true, + "assertEqualArrayBuffers": true, + "dcodeIO": true, + "getString": true, + "hexToArrayBuffer": true, + "MockServer": true, + "MockSocket": true, + "clearDatabase": true, + "PROTO_ROOT": true, + "stringToArrayBuffer": true + } +} diff --git a/libloki/test/_test.js b/libloki/test/_test.js new file mode 100644 index 000000000..af7726e93 --- /dev/null +++ b/libloki/test/_test.js @@ -0,0 +1,66 @@ +/* global mocha, chai, assert, Whisper */ + +mocha.setup('bdd'); +window.assert = chai.assert; +window.PROTO_ROOT = '../../protos'; + +const OriginalReporter = mocha._reporter; + +const SauceReporter = function Constructor(runner) { + const failedTests = []; + + runner.on('end', () => { + window.mochaResults = runner.stats; + window.mochaResults.reports = failedTests; + }); + + runner.on('fail', (test, err) => { + const flattenTitles = item => { + const titles = []; + while (item.parent.title) { + titles.push(item.parent.title); + // eslint-disable-next-line no-param-reassign + item = item.parent; + } + return titles.reverse(); + }; + failedTests.push({ + name: test.title, + result: false, + message: err.message, + stack: err.stack, + titles: flattenTitles(test), + }); + }); + + // eslint-disable-next-line no-new + new OriginalReporter(runner); +}; + +SauceReporter.prototype = OriginalReporter.prototype; + +mocha.reporter(SauceReporter); + +// Override the database id. +window.Whisper = window.Whisper || {}; +window.Whisper.Database = window.Whisper.Database || {}; +Whisper.Database.id = 'test'; + +/* + * global helpers for tests + */ +window.assertEqualArrayBuffers = (ab1, ab2) => { + assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); +}; + +window.hexToArrayBuffer = str => { + const ret = new ArrayBuffer(str.length / 2); + const array = new Uint8Array(ret); + for (let i = 0; i < str.length / 2; i += 1) + array[i] = parseInt(str.substr(i * 2, 2), 16); + return ret; +}; + +window.clearDatabase = async () => { + await window.Signal.Data.removeAll(); +}; diff --git a/libloki/test/index.html b/libloki/test/index.html new file mode 100644 index 000000000..2245c97e2 --- /dev/null +++ b/libloki/test/index.html @@ -0,0 +1,39 @@ + + + + + libloki test runner + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libloki/test/libloki-protocol_test.js b/libloki/test/libloki-protocol_test.js new file mode 100644 index 000000000..c165b6d27 --- /dev/null +++ b/libloki/test/libloki-protocol_test.js @@ -0,0 +1,60 @@ +/* global libsignal, libloki, textsecure, StringView */ + +'use strict'; + +describe('ConversationCollection', () => { + let fallbackCipher; + let identityKey; + let testKey; + let address; + const store = textsecure.storage.protocol; + + beforeEach(async () => { + clearDatabase(); + identityKey = await libsignal.KeyHelper.generateIdentityKeyPair(); + store.put('identityKey', identityKey); + const key = libsignal.crypto.getRandomBytes(32); + const pubKeyString = StringView.arrayBufferToHex(key); + address = new libsignal.SignalProtocolAddress( + pubKeyString, + 1 // sourceDevice + ); + testKey = { + pubKey: libsignal.crypto.getRandomBytes(33), + privKey: libsignal.crypto.getRandomBytes(32), + }; + fallbackCipher = new libloki.FallBackSessionCipher(address); + textsecure.storage.put('maxPreKeyId', 0); + textsecure.storage.put('signedKeyId', 2); + await store.storeSignedPreKey(1, testKey); + }); + + it('should encrypt fallback cipher messages as friend requests', async () => { + const buffer = new ArrayBuffer(10); + const { type } = await fallbackCipher.encrypt(buffer); + assert(type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST); + }); + + it('should should generate a new prekey bundle for a new contact', async () => { + const pubKey = libsignal.crypto.getRandomBytes(32); + const pubKeyString = StringView.arrayBufferToHex(pubKey); + const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1); + const newBundle = await libloki.getPreKeyBundleForNumber(pubKeyString); + const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1); + assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1); + + const testKeyArray = new Uint8Array(testKey.pubKey); + assert.isDefined(newBundle); + assert.isDefined(newBundle.identityKey); + assert.isDefined(newBundle.deviceId); + assert.isDefined(newBundle.preKeyId); + assert.isDefined(newBundle.signedKeyId); + assert.isDefined(newBundle.preKey); + assert.isDefined(newBundle.signedKey); + assert.isDefined(newBundle.signature); + const signedKeyArray = new Uint8Array(newBundle.signedKey.toArrayBuffer()); + assert.strictEqual(testKeyArray.byteLength, signedKeyArray.byteLength); + for (let i = 0 ; i !== testKeyArray.byteLength ; i += 1) + assert.strictEqual(testKeyArray[i], signedKeyArray[i]); + }); +}); diff --git a/libtextsecure/test/in_memory_signal_protocol_store.js b/libtextsecure/test/in_memory_signal_protocol_store.js index 9b8ce813a..712208853 100644 --- a/libtextsecure/test/in_memory_signal_protocol_store.js +++ b/libtextsecure/test/in_memory_signal_protocol_store.js @@ -152,4 +152,16 @@ SignalProtocolStore.prototype = { resolve(deviceIds); }); }, + async loadPreKeyForContactIdentityKeyString(contactIdentityKeyString) { + return new Promise(resolve => { + const key = this.get(`25519KeypreKey${contactIdentityKeyString}`); + if (!key) resolve(undefined); + resolve({ + pubKey: key.publicKey, + privKey: key.privateKey, + keyId: key.id, + recipient: key.recipient, + }); + }); + }, }; diff --git a/main.js b/main.js index 1e794c6da..62c41fc52 100644 --- a/main.js +++ b/main.js @@ -318,6 +318,10 @@ function createWindow() { mainWindow.loadURL( prepareURL([__dirname, 'libtextsecure', 'test', 'index.html']) ); + } else if (config.environment === 'test-loki') { + mainWindow.loadURL( + prepareURL([__dirname, 'libloki', 'test', 'index.html']) + ); } else { mainWindow.loadURL(prepareURL([__dirname, 'background.html'])); } @@ -341,6 +345,7 @@ function createWindow() { if ( config.environment === 'test' || config.environment === 'test-lib' || + config.environment === 'test-loki' || (mainWindow.readyForShutdown && windowState.shouldQuit()) ) { return; @@ -611,7 +616,11 @@ app.on('ready', async () => { const userDataPath = await getRealPath(app.getPath('userData')); const installPath = await getRealPath(app.getAppPath()); - if (process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== 'test-lib') { + if ( + process.env.NODE_ENV !== 'test' && + process.env.NODE_ENV !== 'test-lib' && + process.env.NODE_ENV !== 'test-loki' + ) { installFileHandler({ protocol: electronProtocol, userDataPath, @@ -777,7 +786,8 @@ app.on('window-all-closed', () => { if ( process.platform !== 'darwin' || config.environment === 'test' || - config.environment === 'test-lib' + config.environment === 'test-lib' || + config.environment === 'test-loki' ) { app.quit(); } diff --git a/package.json b/package.json index a0de59a19..dcaf564a0 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "prepare-import-build": "node prepare_import_build.js", "publish-to-apt": "NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh", "test": "yarn test-node && yarn test-electron", + "test-loki": "NODE_ENV=test-loki yarn run start", "test-electron": "yarn grunt test", "test-node": "mocha --recursive test/app test/modules ts/test", "test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test", From a6764592297649286fd312f2fc1467fd32cd0084 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Wed, 5 Dec 2018 16:15:14 +1100 Subject: [PATCH 2/9] Added test for returning the correct prekeybundle after creating a contact, updated the in memory store to reflect how sasha updated the actual store a while ago --- libloki/test/libloki-protocol_test.js | 53 +++++++++++++++++++ .../test/in_memory_signal_protocol_store.js | 13 ++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/libloki/test/libloki-protocol_test.js b/libloki/test/libloki-protocol_test.js index c165b6d27..8b3318079 100644 --- a/libloki/test/libloki-protocol_test.js +++ b/libloki/test/libloki-protocol_test.js @@ -57,4 +57,57 @@ describe('ConversationCollection', () => { for (let i = 0 ; i !== testKeyArray.byteLength ; i += 1) assert.strictEqual(testKeyArray[i], signedKeyArray[i]); }); + + it('should should return the same prekey bundle after creating a contact', async () => { + const pubKey = libsignal.crypto.getRandomBytes(32); + const pubKeyString = StringView.arrayBufferToHex(pubKey); + const bundle1 = await libloki.getPreKeyBundleForNumber(pubKeyString); + const bundle2 = await libloki.getPreKeyBundleForNumber(pubKeyString); + + assert.isDefined(bundle1); + assert.isDefined(bundle1.identityKey); + assert.isDefined(bundle1.deviceId); + assert.isDefined(bundle1.preKeyId); + assert.isDefined(bundle1.signedKeyId); + assert.isDefined(bundle1.preKey); + assert.isDefined(bundle1.signedKey); + assert.isDefined(bundle1.signature); + + assert.isDefined(bundle2); + assert.isDefined(bundle2.identityKey); + assert.isDefined(bundle2.deviceId); + assert.isDefined(bundle2.preKeyId); + assert.isDefined(bundle2.signedKeyId); + assert.isDefined(bundle2.preKey); + assert.isDefined(bundle2.signedKey); + assert.isDefined(bundle2.signature); + + const identityKeyArray1 = new Uint8Array(bundle1.identityKey.toArrayBuffer()); + const identityKeyArray2 = new Uint8Array(bundle2.identityKey.toArrayBuffer()); + assert.strictEqual(identityKeyArray2.byteLength, identityKeyArray2.byteLength); + for (let i = 0 ; i !== identityKeyArray2.byteLength ; i += 1) + assert.strictEqual(identityKeyArray1[i], identityKeyArray2[i]); + + assert.strictEqual(bundle1.deviceId, bundle2.deviceId); + assert.strictEqual(bundle1.preKeyId, bundle2.preKeyId); + assert.strictEqual(bundle1.signedKeyId, bundle2.signedKeyId); + + const preKeyArray1 = new Uint8Array(bundle1.preKey.toArrayBuffer()); + const preKeyArray2 = new Uint8Array(bundle2.preKey.toArrayBuffer()); + assert.strictEqual(preKeyArray2.byteLength, preKeyArray2.byteLength); + for (let i = 0 ; i !== preKeyArray2.byteLength ; i += 1) + assert.strictEqual(preKeyArray1[i], preKeyArray2[i]); + + const signedKeyArray1 = new Uint8Array(bundle1.signedKey.toArrayBuffer()); + const signedKeyArray2 = new Uint8Array(bundle2.signedKey.toArrayBuffer()); + assert.strictEqual(signedKeyArray2.byteLength, signedKeyArray2.byteLength); + for (let i = 0 ; i !== signedKeyArray2.byteLength ; i += 1) + assert.strictEqual(signedKeyArray1[i], signedKeyArray2[i]); + + const signatureArray1 = new Uint8Array(bundle1.signature.toArrayBuffer()); + const signatureArray2 = new Uint8Array(bundle2.signature.toArrayBuffer()); + assert.strictEqual(signatureArray2.byteLength, signatureArray2.byteLength); + for (let i = 0 ; i !== signatureArray2.byteLength ; i += 1) + assert.strictEqual(signatureArray1[i], signatureArray2[i]); + }); }); diff --git a/libtextsecure/test/in_memory_signal_protocol_store.js b/libtextsecure/test/in_memory_signal_protocol_store.js index 712208853..35d5e145d 100644 --- a/libtextsecure/test/in_memory_signal_protocol_store.js +++ b/libtextsecure/test/in_memory_signal_protocol_store.js @@ -75,7 +75,18 @@ SignalProtocolStore.prototype = { resolve(res); }); }, - storePreKey(keyId, keyPair) { + storePreKey(keyId, keyPair, contactIdentityKeyString = null) { + if (contactIdentityKeyString) { + const data = { + id: keyId, + publicKey: keyPair.pubKey, + privateKey: keyPair.privKey, + recipient: contactIdentityKeyString, + }; + return new Promise(resolve => { + resolve(this.put(`25519KeypreKey${contactIdentityKeyString}`, data)); + }); + } return new Promise(resolve => { resolve(this.put(`25519KeypreKey${keyId}`, keyPair)); }); From be878f00a49da5491b677bdbc5b4413559de4173 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Thu, 6 Dec 2018 14:52:57 +1100 Subject: [PATCH 3/9] Updated names of some test functions. Added yarn commands to view output of different tests. Added a test and updated existing test to use more efficient asserts --- libloki/test/_test.js | 2 +- libloki/test/libloki-protocol_test.js | 76 +++++++------------ .../test/in_memory_signal_protocol_store.js | 40 ++++++++-- package.json | 4 +- 4 files changed, 64 insertions(+), 58 deletions(-) diff --git a/libloki/test/_test.js b/libloki/test/_test.js index af7726e93..ef33c6a91 100644 --- a/libloki/test/_test.js +++ b/libloki/test/_test.js @@ -1,4 +1,4 @@ -/* global mocha, chai, assert, Whisper */ +/* global window, mocha, chai, assert, Whisper */ mocha.setup('bdd'); window.assert = chai.assert; diff --git a/libloki/test/libloki-protocol_test.js b/libloki/test/libloki-protocol_test.js index 8b3318079..cac7a4330 100644 --- a/libloki/test/libloki-protocol_test.js +++ b/libloki/test/libloki-protocol_test.js @@ -32,14 +32,14 @@ describe('ConversationCollection', () => { it('should encrypt fallback cipher messages as friend requests', async () => { const buffer = new ArrayBuffer(10); const { type } = await fallbackCipher.encrypt(buffer); - assert(type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST); + assert.strictEqual(type, textsecure.protobuf.Envelope.Type.FRIEND_REQUEST); }); it('should should generate a new prekey bundle for a new contact', async () => { const pubKey = libsignal.crypto.getRandomBytes(32); const pubKeyString = StringView.arrayBufferToHex(pubKey); const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1); - const newBundle = await libloki.getPreKeyBundleForNumber(pubKeyString); + const newBundle = await libloki.getPreKeyBundleForContact(pubKeyString); const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1); assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1); @@ -52,62 +52,38 @@ describe('ConversationCollection', () => { assert.isDefined(newBundle.preKey); assert.isDefined(newBundle.signedKey); assert.isDefined(newBundle.signature); - const signedKeyArray = new Uint8Array(newBundle.signedKey.toArrayBuffer()); - assert.strictEqual(testKeyArray.byteLength, signedKeyArray.byteLength); - for (let i = 0 ; i !== testKeyArray.byteLength ; i += 1) - assert.strictEqual(testKeyArray[i], signedKeyArray[i]); + assert.strictEqual(testKeyArray.byteLength, newBundle.signedKey.byteLength); + for (let i = 0 ; i !== testKeyArray.byteLength; i += 1) + assert.strictEqual(testKeyArray[i], newBundle.signedKey[i]); }); it('should should return the same prekey bundle after creating a contact', async () => { const pubKey = libsignal.crypto.getRandomBytes(32); const pubKeyString = StringView.arrayBufferToHex(pubKey); - const bundle1 = await libloki.getPreKeyBundleForNumber(pubKeyString); - const bundle2 = await libloki.getPreKeyBundleForNumber(pubKeyString); - + const bundle1 = await libloki.getPreKeyBundleForContact(pubKeyString); + const bundle2 = await libloki.getPreKeyBundleForContact(pubKeyString); assert.isDefined(bundle1); - assert.isDefined(bundle1.identityKey); - assert.isDefined(bundle1.deviceId); - assert.isDefined(bundle1.preKeyId); - assert.isDefined(bundle1.signedKeyId); - assert.isDefined(bundle1.preKey); - assert.isDefined(bundle1.signedKey); - assert.isDefined(bundle1.signature); - assert.isDefined(bundle2); - assert.isDefined(bundle2.identityKey); - assert.isDefined(bundle2.deviceId); - assert.isDefined(bundle2.preKeyId); - assert.isDefined(bundle2.signedKeyId); - assert.isDefined(bundle2.preKey); - assert.isDefined(bundle2.signedKey); - assert.isDefined(bundle2.signature); - - const identityKeyArray1 = new Uint8Array(bundle1.identityKey.toArrayBuffer()); - const identityKeyArray2 = new Uint8Array(bundle2.identityKey.toArrayBuffer()); - assert.strictEqual(identityKeyArray2.byteLength, identityKeyArray2.byteLength); - for (let i = 0 ; i !== identityKeyArray2.byteLength ; i += 1) - assert.strictEqual(identityKeyArray1[i], identityKeyArray2[i]); - - assert.strictEqual(bundle1.deviceId, bundle2.deviceId); - assert.strictEqual(bundle1.preKeyId, bundle2.preKeyId); - assert.strictEqual(bundle1.signedKeyId, bundle2.signedKeyId); - - const preKeyArray1 = new Uint8Array(bundle1.preKey.toArrayBuffer()); - const preKeyArray2 = new Uint8Array(bundle2.preKey.toArrayBuffer()); - assert.strictEqual(preKeyArray2.byteLength, preKeyArray2.byteLength); - for (let i = 0 ; i !== preKeyArray2.byteLength ; i += 1) - assert.strictEqual(preKeyArray1[i], preKeyArray2[i]); + assert.deepEqual(bundle1, bundle2); + }); - const signedKeyArray1 = new Uint8Array(bundle1.signedKey.toArrayBuffer()); - const signedKeyArray2 = new Uint8Array(bundle2.signedKey.toArrayBuffer()); - assert.strictEqual(signedKeyArray2.byteLength, signedKeyArray2.byteLength); - for (let i = 0 ; i !== signedKeyArray2.byteLength ; i += 1) - assert.strictEqual(signedKeyArray1[i], signedKeyArray2[i]); + it('should save the signed keys and prekeys from a bundle', async () => { + const pubKey = libsignal.crypto.getRandomBytes(32); + const pubKeyString = StringView.arrayBufferToHex(pubKey); + const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1); + const newBundle = await libloki.getPreKeyBundleForContact(pubKeyString); + const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1); + assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1); - const signatureArray1 = new Uint8Array(bundle1.signature.toArrayBuffer()); - const signatureArray2 = new Uint8Array(bundle2.signature.toArrayBuffer()); - assert.strictEqual(signatureArray2.byteLength, signatureArray2.byteLength); - for (let i = 0 ; i !== signatureArray2.byteLength ; i += 1) - assert.strictEqual(signatureArray1[i], signatureArray2[i]); + const testKeyArray = new Uint8Array(testKey.pubKey); + assert.isDefined(newBundle); + assert.isDefined(newBundle.identityKey); + assert.isDefined(newBundle.deviceId); + assert.isDefined(newBundle.preKeyId); + assert.isDefined(newBundle.signedKeyId); + assert.isDefined(newBundle.preKey); + assert.isDefined(newBundle.signedKey); + assert.isDefined(newBundle.signature); + assert.deepEqual(testKeyArray, newBundle.signedKey); }); }); diff --git a/libtextsecure/test/in_memory_signal_protocol_store.js b/libtextsecure/test/in_memory_signal_protocol_store.js index 35d5e145d..397a82ff5 100644 --- a/libtextsecure/test/in_memory_signal_protocol_store.js +++ b/libtextsecure/test/in_memory_signal_protocol_store.js @@ -75,16 +75,16 @@ SignalProtocolStore.prototype = { resolve(res); }); }, - storePreKey(keyId, keyPair, contactIdentityKeyString = null) { - if (contactIdentityKeyString) { + storePreKey(keyId, keyPair, contactPubKey= null) { + if (contactPubKey) { const data = { id: keyId, publicKey: keyPair.pubKey, privateKey: keyPair.privKey, - recipient: contactIdentityKeyString, + recipient: contactPubKey, }; return new Promise(resolve => { - resolve(this.put(`25519KeypreKey${contactIdentityKeyString}`, data)); + resolve(this.put(`25519KeypreKey${contactPubKey}`, data)); }); } return new Promise(resolve => { @@ -163,9 +163,9 @@ SignalProtocolStore.prototype = { resolve(deviceIds); }); }, - async loadPreKeyForContactIdentityKeyString(contactIdentityKeyString) { + async loadPreKeyForContact(contactPubKey) { return new Promise(resolve => { - const key = this.get(`25519KeypreKey${contactIdentityKeyString}`); + const key = this.get(`25519KeypreKey${contactPubKey}`); if (!key) resolve(undefined); resolve({ pubKey: key.publicKey, @@ -175,4 +175,32 @@ SignalProtocolStore.prototype = { }); }); }, + async storeContactSignedPreKey(pubKey, signedPreKey) { + const key = { + // id: (autoincrement) + identityKeyString: pubKey, + keyId: signedPreKey.keyId, + publicKey: signedPreKey.publicKey, + signature: signedPreKey.signature, + created_at: Date.now(), + confirmed: false, + }; + this.put(`contactSignedPreKey${pubKey}`, key); + }, + async loadContactSignedPreKey(pubKey) { + const preKey = this.get(`contactSignedPreKey${pubKey}`); + if (preKey) { + return { + id: preKey.id, + identityKeyString: preKey.identityKeyString, + publicKey: preKey.publicKey, + signature: preKey.signature, + created_at: preKey.created_at, + keyId: preKey.keyId, + confirmed: preKey.confirmed, + }; + } + window.log.warn('Failed to fetch contact signed prekey:', pubKey); + return undefined; + }, }; diff --git a/package.json b/package.json index dcaf564a0..cce536cd7 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "prepare-import-build": "node prepare_import_build.js", "publish-to-apt": "NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh", "test": "yarn test-node && yarn test-electron", - "test-loki": "NODE_ENV=test-loki yarn run start", + "test-view": "NODE_ENV=test yarn run start", + "test-lib-view": "NODE_ENV=test-lib yarn run start", + "test-loki-view": "NODE_ENV=test-loki yarn run start", "test-electron": "yarn grunt test", "test-node": "mocha --recursive test/app test/modules ts/test", "test-node-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/app test/modules ts/test", From 11883cb9896a156ab051b3891c4ae823c389624b Mon Sep 17 00:00:00 2001 From: Beaudan Date: Thu, 6 Dec 2018 15:07:49 +1100 Subject: [PATCH 4/9] Cleaner separation of loki tests --- libloki/test/libloki-protocol_test.js | 28 ++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/libloki/test/libloki-protocol_test.js b/libloki/test/libloki-protocol_test.js index cac7a4330..1cb942d0d 100644 --- a/libloki/test/libloki-protocol_test.js +++ b/libloki/test/libloki-protocol_test.js @@ -2,14 +2,13 @@ 'use strict'; -describe('ConversationCollection', () => { +describe('FallBackSessionCipher', () => { let fallbackCipher; let identityKey; - let testKey; let address; const store = textsecure.storage.protocol; - beforeEach(async () => { + before(async () => { clearDatabase(); identityKey = await libsignal.KeyHelper.generateIdentityKeyPair(); store.put('identityKey', identityKey); @@ -17,16 +16,9 @@ describe('ConversationCollection', () => { const pubKeyString = StringView.arrayBufferToHex(key); address = new libsignal.SignalProtocolAddress( pubKeyString, - 1 // sourceDevice + 1 ); - testKey = { - pubKey: libsignal.crypto.getRandomBytes(33), - privKey: libsignal.crypto.getRandomBytes(32), - }; fallbackCipher = new libloki.FallBackSessionCipher(address); - textsecure.storage.put('maxPreKeyId', 0); - textsecure.storage.put('signedKeyId', 2); - await store.storeSignedPreKey(1, testKey); }); it('should encrypt fallback cipher messages as friend requests', async () => { @@ -34,6 +26,20 @@ describe('ConversationCollection', () => { const { type } = await fallbackCipher.encrypt(buffer); assert.strictEqual(type, textsecure.protobuf.Envelope.Type.FRIEND_REQUEST); }); +}); + +describe('LibLoki Protocol', () => { + let testKey; + const store = textsecure.storage.protocol; + + beforeEach(async () => { + clearDatabase(); + testKey = { + pubKey: libsignal.crypto.getRandomBytes(33), + privKey: libsignal.crypto.getRandomBytes(32), + }; + await store.storeSignedPreKey(1, testKey); + }); it('should should generate a new prekey bundle for a new contact', async () => { const pubKey = libsignal.crypto.getRandomBytes(32); From 7782c58d1455c39aa37c114c9f7bc383b85c7250 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Fri, 7 Dec 2018 13:50:41 +1100 Subject: [PATCH 5/9] Slight refactor of PoW file to make easier to test plus some tests for the PoW functionality --- Gruntfile.js | 1 - libloki/proof-of-work.js | 60 +++++++----- libloki/test/.eslintrc | 10 +- libloki/test/_test.js | 12 --- .../test/in_memory_signal_protocol_store.js | 3 +- test/app/proof-of-work_test.js | 97 +++++++++++++++++++ 6 files changed, 135 insertions(+), 48 deletions(-) create mode 100644 test/app/proof-of-work_test.js diff --git a/Gruntfile.js b/Gruntfile.js index a97bf9860..5b521683f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -462,7 +462,6 @@ module.exports = grunt => { ]); grunt.registerTask('dev', ['default', 'watch']); grunt.registerTask('test', ['unit-tests', 'lib-unit-tests', 'loki-unit-tests']); - grunt.registerTask('test-loki', ['loki-unit-tests']); grunt.registerTask('date', ['gitinfo', 'getExpireTime']); grunt.registerTask('default', [ 'exec:build-protobuf', diff --git a/libloki/proof-of-work.js b/libloki/proof-of-work.js index 494c4987a..0381d70a8 100644 --- a/libloki/proof-of-work.js +++ b/libloki/proof-of-work.js @@ -2,9 +2,17 @@ const hash = require('js-sha512'); const bb = require('bytebuffer'); const { BigInteger } = require('jsbn'); +module.exports = { + calcTarget, + incrementNonce, + bufferToBase64, + bigIntToUint8Array, + greaterThan, +}; + const NONCE_LEN = 8; // Modify this value for difficulty scaling -let NONCE_TRIALS = 1000; +let NONCE_TRIALS = 10; // Increment Uint8Array nonce by 1 with carrying function incrementNonce(nonce) { @@ -62,27 +70,7 @@ function calcPoW(timestamp, ttl, pubKey, data) { bb.wrap(timestamp.toString() + ttl.toString() + pubKey + data, 'binary').toArrayBuffer() ); - // payloadLength + NONCE_LEN - const totalLen = new BigInteger(payload.length.toString()).add( - new BigInteger(NONCE_LEN.toString()) - ); - // ttl * totalLen - const ttlMult = new BigInteger(ttl.toString()).multiply(totalLen); - // ttlMult / (2^16 - 1) - const innerFrac = ttlMult.divide( - new BigInteger('2').pow(16).subtract(new BigInteger('1')) - ); - // totalLen + innerFrac - const lenPlusInnerFrac = totalLen.add(innerFrac); - // NONCE_TRIALS * lenPlusInnerFrac - const denominator = new BigInteger(NONCE_TRIALS.toString()).multiply( - lenPlusInnerFrac - ); - // 2^64 - 1 - const two64 = new BigInteger('2').pow(64).subtract(new BigInteger('1')); - // two64 / denominator - const targetNum = two64.divide(denominator); - const target = bigIntToUint8Array(targetNum); + const target = calcTarget(ttl, payload.length); let nonce = new Uint8Array(NONCE_LEN); let trialValue = bigIntToUint8Array( @@ -105,10 +93,34 @@ function calcPoW(timestamp, ttl, pubKey, data) { return bufferToBase64(nonce); } +function calcTarget(ttl, payloadLen) { + // payloadLength + NONCE_LEN + const totalLen = new BigInteger(payloadLen.toString()).add( + new BigInteger(NONCE_LEN.toString()) + ); + // ttl * totalLen + const ttlMult = new BigInteger(ttl.toString()).multiply(totalLen); + // ttlMult / (2^16 - 1) + const innerFrac = ttlMult.divide( + new BigInteger('2').pow(16).subtract(new BigInteger('1')) + ); + // totalLen + innerFrac + const lenPlusInnerFrac = totalLen.add(innerFrac); + // NONCE_TRIALS * lenPlusInnerFrac + const denominator = new BigInteger(NONCE_TRIALS.toString()).multiply( + lenPlusInnerFrac + ); + // 2^64 - 1 + const two64 = new BigInteger('2').pow(64).subtract(new BigInteger('1')); + // two64 / denominator + const targetNum = two64.divide(denominator); + return bigIntToUint8Array(targetNum); +} + // Start calculation in child process when main process sends message data process.on('message', msg => { - if (msg.development) - NONCE_TRIALS = 10; + if (!msg.development) + NONCE_TRIALS = 1000; process.send({ nonce: calcPoW( msg.timestamp, diff --git a/libloki/test/.eslintrc b/libloki/test/.eslintrc index 29971976c..194818b3a 100644 --- a/libloki/test/.eslintrc +++ b/libloki/test/.eslintrc @@ -13,14 +13,6 @@ }, "globals": { "assert": true, - "assertEqualArrayBuffers": true, - "dcodeIO": true, - "getString": true, - "hexToArrayBuffer": true, - "MockServer": true, - "MockSocket": true, - "clearDatabase": true, - "PROTO_ROOT": true, - "stringToArrayBuffer": true + "clearDatabase": true } } diff --git a/libloki/test/_test.js b/libloki/test/_test.js index ef33c6a91..e26c8ab40 100644 --- a/libloki/test/_test.js +++ b/libloki/test/_test.js @@ -49,18 +49,6 @@ Whisper.Database.id = 'test'; /* * global helpers for tests */ -window.assertEqualArrayBuffers = (ab1, ab2) => { - assert.deepEqual(new Uint8Array(ab1), new Uint8Array(ab2)); -}; - -window.hexToArrayBuffer = str => { - const ret = new ArrayBuffer(str.length / 2); - const array = new Uint8Array(ret); - for (let i = 0; i < str.length / 2; i += 1) - array[i] = parseInt(str.substr(i * 2, 2), 16); - return ret; -}; - window.clearDatabase = async () => { await window.Signal.Data.removeAll(); }; diff --git a/libtextsecure/test/in_memory_signal_protocol_store.js b/libtextsecure/test/in_memory_signal_protocol_store.js index 397a82ff5..dcdb0d01a 100644 --- a/libtextsecure/test/in_memory_signal_protocol_store.js +++ b/libtextsecure/test/in_memory_signal_protocol_store.js @@ -75,7 +75,7 @@ SignalProtocolStore.prototype = { resolve(res); }); }, - storePreKey(keyId, keyPair, contactPubKey= null) { + storePreKey(keyId, keyPair, contactPubKey = null) { if (contactPubKey) { const data = { id: keyId, @@ -177,7 +177,6 @@ SignalProtocolStore.prototype = { }, async storeContactSignedPreKey(pubKey, signedPreKey) { const key = { - // id: (autoincrement) identityKeyString: pubKey, keyId: signedPreKey.keyId, publicKey: signedPreKey.publicKey, diff --git a/test/app/proof-of-work_test.js b/test/app/proof-of-work_test.js new file mode 100644 index 000000000..311380548 --- /dev/null +++ b/test/app/proof-of-work_test.js @@ -0,0 +1,97 @@ +/* global require */ +const { assert } = require('chai'); +const { BigInteger } = require('jsbn'); + +const { + calcTarget, + incrementNonce, + bufferToBase64, + bigIntToUint8Array, + greaterThan, +} = require('../../libloki/proof-of-work'); + +describe('Proof of Work Worker', () => { + it('should increment a Uint8Array nonce correctly', () => { + const arr1Before = new Uint8Array([0,0,0,0,0,0,0,0]); + const arr1After = incrementNonce(arr1Before); + assert.strictEqual(arr1After[0], 0); + assert.strictEqual(arr1After[1], 0); + assert.strictEqual(arr1After[2], 0); + assert.strictEqual(arr1After[3], 0); + assert.strictEqual(arr1After[4], 0); + assert.strictEqual(arr1After[5], 0); + assert.strictEqual(arr1After[6], 0); + assert.strictEqual(arr1After[7], 1); + }); + + it('should increment a Uint8Array nonce correctly', () => { + let arr = new Uint8Array([0,0,0,0,0,0,0,0]); + assert.deepEqual(incrementNonce(arr), new Uint8Array([0,0,0,0,0,0,0,1])); + arr = new Uint8Array([0,0,0,0,0,0,0,0]); + for(let i = 0; i <= 255; i += 1) { + arr = incrementNonce(arr); + } + assert.deepEqual(arr, new Uint8Array([0,0,0,0,0,0,1,0])); + arr = new Uint8Array([255,255,255,255,255,255,255,255]); + assert.deepEqual(incrementNonce(arr), new Uint8Array([0,0,0,0,0,0,0,0])); + }); + + it('should calculate a correct difficulty target', () => { + // These values will need to be updated if we adjust the difficulty settings + let payloadLen = 625; + const ttl = 86400; + let expectedTarget = new Uint8Array([0,4,119,164,35,224,222,64]); + + let actualTarget = calcTarget(ttl, payloadLen); + assert.deepEqual(actualTarget, expectedTarget); + payloadLen = 6597; + expectedTarget = new Uint8Array([0,0,109,145,174,146,124,3]); + actualTarget = calcTarget(ttl, payloadLen); + assert.deepEqual(actualTarget, expectedTarget); + }); + + it('should correclty compare two Uint8Arrays', () => { + let arr1 = new Uint8Array([0,0,0,0,0,0,0,0,0,1]); + let arr2 = new Uint8Array([0,0,0,0,0,0,0,0,0,1]); + assert.isFalse(greaterThan(arr1, arr2)) + arr1 = new Uint8Array([0,0,0,0,0,0,0,0,0,2]); + arr2 = new Uint8Array([0,0,0,0,0,0,0,0,0,1]); + assert.isTrue(greaterThan(arr1, arr2)) + arr1 = new Uint8Array([255,255,255,255,255,255,255,255,255,255]); + arr2 = new Uint8Array([255,255,255,255,255,255,255,255,255,254]); + assert.isTrue(greaterThan(arr1, arr2)) + arr1 = new Uint8Array([254,255,255,255,255,255,255,255,255,255]); + arr2 = new Uint8Array([255,255,255,255,255,255,255,255,255,255]); + assert.isFalse(greaterThan(arr1, arr2)); + arr1 = new Uint8Array([0]); + arr2 = new Uint8Array([0,0]); + assert.isFalse(greaterThan(arr1, arr2)) + }); + + it('should correclty convert a Uint8Array to a base64 string', () => { + let arr = new Uint8Array([1,2,3]); + let expected = 'AQID'; + assert.strictEqual(bufferToBase64(arr), expected); + arr = new Uint8Array([123,25,3,121,45,87,24,111]); + expected = 'exkDeS1XGG8='; + assert.strictEqual(bufferToBase64(arr), expected); + arr = new Uint8Array([]); + expected = ''; + assert.strictEqual(bufferToBase64(arr), expected); + }); + + it('should correclty convert a BigInteger to a Uint8Array', () => { + let bigInt = new BigInteger(Number.MAX_SAFE_INTEGER.toString()); + let expected = new Uint8Array([0, 31, 255, 255, 255, 255, 255, 255]); + assert.deepEqual(bigIntToUint8Array(bigInt), expected); + bigInt = new BigInteger('0'); + expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]); + assert.deepEqual(bigIntToUint8Array(bigInt), expected); + bigInt = new BigInteger('255'); + expected = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 255]); + assert.deepEqual(bigIntToUint8Array(bigInt), expected); + bigInt = new BigInteger('256'); + expected = new Uint8Array([0, 0, 0, 0, 0, 0, 1, 0]); + assert.deepEqual(bigIntToUint8Array(bigInt), expected); + }); +}); From 1bc1213705283b16eef5fcd061f1559208af01be Mon Sep 17 00:00:00 2001 From: sachaaaaa <40749766+sachaaaaa@users.noreply.github.com> Date: Fri, 7 Dec 2018 15:56:24 +1100 Subject: [PATCH 6/9] Update libloki/test/libloki-protocol_test.js Co-Authored-By: BeaudanBrown --- libloki/test/libloki-protocol_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libloki/test/libloki-protocol_test.js b/libloki/test/libloki-protocol_test.js index 1cb942d0d..ce0dd6aa7 100644 --- a/libloki/test/libloki-protocol_test.js +++ b/libloki/test/libloki-protocol_test.js @@ -41,7 +41,7 @@ describe('LibLoki Protocol', () => { await store.storeSignedPreKey(1, testKey); }); - it('should should generate a new prekey bundle for a new contact', async () => { + it('should generate a new prekey bundle for a new contact', async () => { const pubKey = libsignal.crypto.getRandomBytes(32); const pubKeyString = StringView.arrayBufferToHex(pubKey); const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1); From 5084d8cfec6c5bb6072fca370195b90c7e8ea509 Mon Sep 17 00:00:00 2001 From: sachaaaaa <40749766+sachaaaaa@users.noreply.github.com> Date: Fri, 7 Dec 2018 15:56:35 +1100 Subject: [PATCH 7/9] Update libloki/test/libloki-protocol_test.js Co-Authored-By: BeaudanBrown --- libloki/test/libloki-protocol_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libloki/test/libloki-protocol_test.js b/libloki/test/libloki-protocol_test.js index ce0dd6aa7..66496b166 100644 --- a/libloki/test/libloki-protocol_test.js +++ b/libloki/test/libloki-protocol_test.js @@ -63,7 +63,7 @@ describe('LibLoki Protocol', () => { assert.strictEqual(testKeyArray[i], newBundle.signedKey[i]); }); - it('should should return the same prekey bundle after creating a contact', async () => { + it('should return the same prekey bundle after creating a contact', async () => { const pubKey = libsignal.crypto.getRandomBytes(32); const pubKeyString = StringView.arrayBufferToHex(pubKey); const bundle1 = await libloki.getPreKeyBundleForContact(pubKeyString); From a0584a68c90376052917628f4e188830ab9a5c3c Mon Sep 17 00:00:00 2001 From: Beaudan Date: Fri, 7 Dec 2018 17:53:34 +1100 Subject: [PATCH 8/9] Review suggested test case --- libloki/test/libloki-protocol_test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libloki/test/libloki-protocol_test.js b/libloki/test/libloki-protocol_test.js index 66496b166..5d0d6d11f 100644 --- a/libloki/test/libloki-protocol_test.js +++ b/libloki/test/libloki-protocol_test.js @@ -26,6 +26,13 @@ describe('FallBackSessionCipher', () => { const { type } = await fallbackCipher.encrypt(buffer); assert.strictEqual(type, textsecure.protobuf.Envelope.Type.FRIEND_REQUEST); }); + + it('should encrypt and then decrypt a message with the same result', async () => { + const arr = new Uint8Array([1,2,3,4,5]); + const { body } = await fallbackCipher.encrypt(arr.buffer); + const result = await fallbackCipher.decrypt(body); + assert.deepEqual(result, arr.buffer); + }); }); describe('LibLoki Protocol', () => { From f6ab6495ae0dc69fd7771fa3fa7d44ad2652e7fe Mon Sep 17 00:00:00 2001 From: Beaudan Date: Mon, 10 Dec 2018 14:28:39 +1100 Subject: [PATCH 9/9] Updated the nonce trials variables to be constants for production and development --- libloki/proof-of-work.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libloki/proof-of-work.js b/libloki/proof-of-work.js index 0381d70a8..2034919f4 100644 --- a/libloki/proof-of-work.js +++ b/libloki/proof-of-work.js @@ -12,7 +12,9 @@ module.exports = { const NONCE_LEN = 8; // Modify this value for difficulty scaling -let NONCE_TRIALS = 10; +const DEV_NONCE_TRIALS = 10; +const PROD_NONCE_TRIALS = 1000; +let development = true; // Increment Uint8Array nonce by 1 with carrying function incrementNonce(nonce) { @@ -106,8 +108,9 @@ function calcTarget(ttl, payloadLen) { ); // totalLen + innerFrac const lenPlusInnerFrac = totalLen.add(innerFrac); - // NONCE_TRIALS * lenPlusInnerFrac - const denominator = new BigInteger(NONCE_TRIALS.toString()).multiply( + // nonceTrials * lenPlusInnerFrac + const nonceTrials = development ? DEV_NONCE_TRIALS : PROD_NONCE_TRIALS; + const denominator = new BigInteger(nonceTrials.toString()).multiply( lenPlusInnerFrac ); // 2^64 - 1 @@ -119,8 +122,7 @@ function calcTarget(ttl, payloadLen) { // Start calculation in child process when main process sends message data process.on('message', msg => { - if (!msg.development) - NONCE_TRIALS = 1000; + ({ development } = msg); process.send({ nonce: calcPoW( msg.timestamp,