Merge pull request #78 from BeaudanBrown/loki-tests

Loki tests
pull/83/head
sachaaaaa 6 years ago committed by GitHub
commit f77da7c218
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

1
.gitignore vendored

@ -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

@ -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,7 @@ 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('date', ['gitinfo', 'getExpireTime']);
grunt.registerTask('default', [
'exec:build-protobuf',

@ -2,9 +2,19 @@ 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;
const DEV_NONCE_TRIALS = 10;
const PROD_NONCE_TRIALS = 1000;
let development = true;
// Increment Uint8Array nonce by 1 with carrying
function incrementNonce(nonce) {
@ -62,27 +72,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 +95,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);
// nonceTrials * lenPlusInnerFrac
const nonceTrials = development ? DEV_NONCE_TRIALS : PROD_NONCE_TRIALS;
const denominator = new BigInteger(nonceTrials.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;
({ development } = msg);
process.send({
nonce: calcPoW(
msg.timestamp,

@ -0,0 +1,18 @@
{
"env": {
"browser": true,
"node": false,
"mocha": true
},
"parserOptions": {
"sourceType": "script"
},
"rules": {
"strict": "off",
"more/no-then": "off"
},
"globals": {
"assert": true,
"clearDatabase": true
}
}

@ -0,0 +1,54 @@
/* global window, 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.clearDatabase = async () => {
await window.Signal.Data.removeAll();
};

@ -0,0 +1,39 @@
<html>
<head>
<meta charset='utf-8'>
<title>libloki test runner</title>
<link rel="stylesheet" href="../../node_modules/mocha/mocha.css" />
</head>
<body>
<div id="mocha">
</div>
<div id="tests">
</div>
<script type="text/javascript" src="test.js"></script>
<script type="text/javascript" src="../../libtextsecure/test/in_memory_signal_protocol_store.js"></script>
<script type="text/javascript" src="../../libtextsecure/components.js"></script>
<script type="text/javascript" src="../../libtextsecure/helpers.js" data-cover></script>
<script type="text/javascript" src="../../libtextsecure/storage.js" data-cover></script>
<script type="text/javascript" src="../../libtextsecure/libsignal-protocol.js"></script>
<script type="text/javascript" src="../../libtextsecure/protocol_wrapper.js" data-cover></script>
<script type="text/javascript" src="../../libtextsecure/protobufs.js" data-cover></script>
<script type="text/javascript" src="../../libtextsecure/stringview.js" data-cover></script>
<script type="text/javascript" src="../libloki-protocol.js" data-cover></script>
<script type="text/javascript" src="libloki-protocol_test.js"></script>
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->
<!-- NOTE: blanket doesn't support modern syntax and will choke until we find a replacement. :0( -->
<!-- <script type="text/javascript" src="blanket_mocha.js"></script> -->
<!-- Uncomment to start tests without code coverage enabled -->
<script type="text/javascript">
mocha.run();
</script>
</body>
</html>

@ -0,0 +1,102 @@
/* global libsignal, libloki, textsecure, StringView */
'use strict';
describe('FallBackSessionCipher', () => {
let fallbackCipher;
let identityKey;
let address;
const store = textsecure.storage.protocol;
before(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
);
fallbackCipher = new libloki.FallBackSessionCipher(address);
});
it('should encrypt fallback cipher messages as friend requests', async () => {
const buffer = new ArrayBuffer(10);
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', () => {
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 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.getPreKeyBundleForContact(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);
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 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);
const bundle2 = await libloki.getPreKeyBundleForContact(pubKeyString);
assert.isDefined(bundle1);
assert.isDefined(bundle2);
assert.deepEqual(bundle1, bundle2);
});
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 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);
});
});

@ -75,7 +75,18 @@ SignalProtocolStore.prototype = {
resolve(res);
});
},
storePreKey(keyId, keyPair) {
storePreKey(keyId, keyPair, contactPubKey = null) {
if (contactPubKey) {
const data = {
id: keyId,
publicKey: keyPair.pubKey,
privateKey: keyPair.privKey,
recipient: contactPubKey,
};
return new Promise(resolve => {
resolve(this.put(`25519KeypreKey${contactPubKey}`, data));
});
}
return new Promise(resolve => {
resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
});
@ -152,4 +163,43 @@ SignalProtocolStore.prototype = {
resolve(deviceIds);
});
},
async loadPreKeyForContact(contactPubKey) {
return new Promise(resolve => {
const key = this.get(`25519KeypreKey${contactPubKey}`);
if (!key) resolve(undefined);
resolve({
pubKey: key.publicKey,
privKey: key.privateKey,
keyId: key.id,
recipient: key.recipient,
});
});
},
async storeContactSignedPreKey(pubKey, signedPreKey) {
const key = {
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;
},
};

@ -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();
}

@ -27,6 +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-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",

@ -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);
});
});
Loading…
Cancel
Save