Merge pull request #125 from Mikunj/refactor-lib-loki

Refactor libloki protocol
pull/129/head
Beaudan Campbell-Brown 6 years ago committed by GitHub
commit 80077325e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -101,8 +101,10 @@ module.exports = grunt => {
}, },
libloki: { libloki: {
src: [ src: [
'libloki/libloki-protocol.js', 'libloki/api.js',
'libloki/crypto.js',
'libloki/service_nodes.js', 'libloki/service_nodes.js',
'libloki/storage.js',
], ],
dest: 'js/libloki.js', dest: 'js/libloki.js',
}, },

@ -709,7 +709,6 @@
<script type='text/javascript' src='js/storage.js'></script> <script type='text/javascript' src='js/storage.js'></script>
<script type='text/javascript' src='js/legacy_storage.js'></script> <script type='text/javascript' src='js/legacy_storage.js'></script>
<script type='text/javascript' src='js/signal_protocol_store.js'></script> <script type='text/javascript' src='js/signal_protocol_store.js'></script>
<script type='text/javascript' src='js/loki_protocol_store.js'></script>
<script type='text/javascript' src='js/libtextsecure.js'></script> <script type='text/javascript' src='js/libtextsecure.js'></script>
<script type='text/javascript' src='js/libloki.js'></script> <script type='text/javascript' src='js/libloki.js'></script>

@ -620,7 +620,7 @@
response: 'declined', response: 'declined',
direction: 'incoming', direction: 'incoming',
}); });
await window.libloki.removeContactPreKeyBundle(this.id); await window.libloki.storage.removeContactPreKeyBundle(this.id);
}, },
// We have accepted an incoming friend request // We have accepted an incoming friend request
async onAcceptFriendRequest() { async onAcceptFriendRequest() {
@ -630,7 +630,7 @@
response: 'accepted', response: 'accepted',
direction: 'incoming', direction: 'incoming',
}); });
window.libloki.sendFriendRequestAccepted(this.id); window.libloki.api.sendFriendRequestAccepted(this.id);
} }
}, },
// Our outgoing friend request has been accepted // Our outgoing friend request has been accepted
@ -1548,7 +1548,7 @@
await this.setSessionResetStatus(SessionResetEnum.request_received); await this.setSessionResetStatus(SessionResetEnum.request_received);
// send empty message, this will trigger the new session to propagate // send empty message, this will trigger the new session to propagate
// to the reset initiator. // to the reset initiator.
await window.libloki.sendEmptyMessage(this.id); await window.libloki.api.sendEmptyMessage(this.id);
}, },
isSessionResetReceived() { isSessionResetReceived() {
@ -1582,7 +1582,7 @@
async onNewSessionAdopted() { async onNewSessionAdopted() {
if (this.get('sessionResetStatus') === SessionResetEnum.initiated) { if (this.get('sessionResetStatus') === SessionResetEnum.initiated) {
// send empty message to confirm that we have adopted the new session // send empty message to confirm that we have adopted the new session
await window.libloki.sendEmptyMessage(this.id); await window.libloki.api.sendEmptyMessage(this.id);
} }
await this.createAndStoreEndSessionMessage({ type: 'incoming', endSessionType: 'done' }); await this.createAndStoreEndSessionMessage({ type: 'incoming', endSessionType: 'done' });
await this.setSessionResetStatus(SessionResetEnum.none); await this.setSessionResetStatus(SessionResetEnum.none);

@ -1407,7 +1407,7 @@
autoAccept = true; autoAccept = true;
message.set({ friendStatus: 'accepted' }); message.set({ friendStatus: 'accepted' });
await conversation.onFriendRequestAccepted(); await conversation.onFriendRequestAccepted();
window.libloki.sendFriendRequestAccepted(message.get('source')); window.libloki.api.sendFriendRequestAccepted(message.get('source'));
} else if (!conversation.isFriend()) { } else if (!conversation.isFriend()) {
await conversation.onFriendRequestReceived(); await conversation.onFriendRequestReceived();
} }

@ -0,0 +1,41 @@
/* global window, textsecure, log */
// eslint-disable-next-line func-names
(function () {
window.libloki = window.libloki || {};
async function sendFriendRequestAccepted(pubKey) {
return sendEmptyMessage(pubKey);
}
async function sendEmptyMessage(pubKey) {
// empty content message
const content = new textsecure.protobuf.Content();
// will be called once the transmission succeeded or failed
const callback = res => {
if (res.errors.length > 0) {
res.errors.forEach(error => log.error(error));
} else {
log.info('empty message sent successfully');
}
};
const options = {};
// send an empty message. The logic in ougoing_message will attach the prekeys.
const outgoingMessage = new textsecure.OutgoingMessage(
null, // server
Date.now(), // timestamp,
[pubKey], // numbers
content, // message
true, // silent
callback, // callback
options
);
await outgoingMessage.sendToNumber(pubKey);
}
window.libloki.api = {
sendFriendRequestAccepted,
sendEmptyMessage,
};
})();

@ -0,0 +1,53 @@
/* global window, libsignal, textsecure, StringView */
// eslint-disable-next-line func-names
(function () {
window.libloki = window.libloki || {};
class FallBackDecryptionError extends Error { }
const IV_LENGTH = 16;
class FallBackSessionCipher {
constructor(address) {
this.identityKeyString = address.getName();
this.pubKey = StringView.hexToArrayBuffer(address.getName());
}
async encrypt(plaintext) {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const myPrivateKey = myKeyPair.privKey;
const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey);
const iv = libsignal.crypto.getRandomBytes(IV_LENGTH);
const ciphertext = await libsignal.crypto.encrypt(symmetricKey, plaintext, iv);
const ivAndCiphertext = new Uint8Array(iv.byteLength + ciphertext.byteLength);
ivAndCiphertext.set(new Uint8Array(iv));
ivAndCiphertext.set(new Uint8Array(ciphertext), iv.byteLength);
return {
type: textsecure.protobuf.Envelope.Type.FRIEND_REQUEST,
body: ivAndCiphertext,
registrationId: null,
};
}
async decrypt(ivAndCiphertext) {
const iv = ivAndCiphertext.slice(0, IV_LENGTH);
const cipherText = ivAndCiphertext.slice(IV_LENGTH);
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const myPrivateKey = myKeyPair.privKey;
const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey);
try {
return await libsignal.crypto.decrypt(symmetricKey, cipherText, iv);
}
catch (e) {
throw new FallBackDecryptionError(`Could not decrypt message from ${this.identityKeyString} using FallBack encryption.`);
}
}
}
window.libloki.crypto = {
FallBackSessionCipher,
FallBackDecryptionError,
};
})();

@ -1,167 +0,0 @@
/* global window, libsignal, textsecure, StringView, log */
// eslint-disable-next-line func-names
(function () {
window.libloki = window.libloki || {};
class FallBackDecryptionError extends Error { }
const IV_LENGTH = 16;
class FallBackSessionCipher {
constructor(address) {
this.identityKeyString = address.getName();
this.pubKey = StringView.hexToArrayBuffer(address.getName());
}
async encrypt(plaintext) {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const myPrivateKey = myKeyPair.privKey;
const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey);
const iv = libsignal.crypto.getRandomBytes(IV_LENGTH);
const ciphertext = await libsignal.crypto.encrypt(symmetricKey, plaintext, iv);
const ivAndCiphertext = new Uint8Array(iv.byteLength + ciphertext.byteLength);
ivAndCiphertext.set(new Uint8Array(iv));
ivAndCiphertext.set(new Uint8Array(ciphertext), iv.byteLength);
return {
type: textsecure.protobuf.Envelope.Type.FRIEND_REQUEST,
body: ivAndCiphertext,
registrationId: null,
};
}
async decrypt(ivAndCiphertext) {
const iv = ivAndCiphertext.slice(0, IV_LENGTH);
const cipherText = ivAndCiphertext.slice(IV_LENGTH);
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const myPrivateKey = myKeyPair.privKey;
const symmetricKey = libsignal.Curve.calculateAgreement(this.pubKey, myPrivateKey);
try {
return await libsignal.crypto.decrypt(symmetricKey, cipherText, iv);
}
catch (e) {
throw new FallBackDecryptionError(`Could not decrypt message from ${this.identityKeyString} using FallBack encryption.`);
}
}
}
async function getPreKeyBundleForContact(pubKey) {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const identityKey = myKeyPair.pubKey;
// Retrieve ids. The ids stored are always the latest generated + 1
const signedKeyId = textsecure.storage.get('signedKeyId', 2) - 1;
const [signedKey, preKey] = await Promise.all([
textsecure.storage.protocol.loadSignedPreKey(signedKeyId),
new Promise(async resolve => {
// retrieve existing prekey if we already generated one for that recipient
const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContact(
pubKey
);
if (storedPreKey) {
resolve({ pubKey: storedPreKey.pubKey, keyId: storedPreKey.keyId });
} else {
// generate and store new prekey
const preKeyId = textsecure.storage.get('maxPreKeyId', 1);
textsecure.storage.put('maxPreKeyId', preKeyId + 1);
const newPreKey = await libsignal.KeyHelper.generatePreKey(preKeyId);
await textsecure.storage.protocol.storePreKey(
newPreKey.keyId,
newPreKey.keyPair,
pubKey
);
resolve({ pubKey: newPreKey.keyPair.pubKey, keyId: preKeyId });
}
}),
]);
return {
identityKey: new Uint8Array(identityKey),
deviceId: 1, // TODO: fetch from somewhere
preKeyId: preKey.keyId,
signedKeyId,
preKey: new Uint8Array(preKey.pubKey),
signedKey: new Uint8Array(signedKey.pubKey),
signature: new Uint8Array(signedKey.signature),
};
}
async function saveContactPreKeyBundle({
pubKey,
preKeyId,
preKey,
signedKeyId,
signedKey,
signature,
}) {
const signedPreKey = {
keyId: signedKeyId,
publicKey: signedKey,
signature,
};
const signedKeyPromise = textsecure.storage.protocol.storeContactSignedPreKey(
pubKey,
signedPreKey
);
const preKeyObject = {
publicKey: preKey,
keyId: preKeyId,
};
const preKeyPromise = textsecure.storage.protocol.storeContactPreKey(
pubKey,
preKeyObject
);
await Promise.all([signedKeyPromise, preKeyPromise]);
}
async function removeContactPreKeyBundle(pubKey) {
await Promise.all([
textsecure.storage.protocol.removeContactPreKey(pubKey),
textsecure.storage.protocol.removeContactSignedPreKey(pubKey),
]);
}
async function sendFriendRequestAccepted(pubKey) {
return sendEmptyMessage(pubKey);
}
async function sendEmptyMessage(pubKey) {
// empty content message
const content = new textsecure.protobuf.Content();
// will be called once the transmission succeeded or failed
const callback = res => {
if (res.errors.length > 0) {
res.errors.forEach(error => log.error(error));
} else {
log.info('empty message sent successfully');
}
};
const options = {};
// send an empty message. The logic in ougoing_message will attach the prekeys.
const outgoingMessage = new textsecure.OutgoingMessage(
null, // server
Date.now(), // timestamp,
[pubKey], // numbers
content, // message
true, // silent
callback, // callback
options
);
await outgoingMessage.sendToNumber(pubKey);
}
window.libloki.FallBackSessionCipher = FallBackSessionCipher;
window.libloki.getPreKeyBundleForContact = getPreKeyBundleForContact;
window.libloki.FallBackDecryptionError = FallBackDecryptionError;
window.libloki.saveContactPreKeyBundle = saveContactPreKeyBundle;
window.libloki.removeContactPreKeyBundle = removeContactPreKeyBundle;
window.libloki.sendFriendRequestAccepted = sendFriendRequestAccepted;
window.libloki.sendEmptyMessage = sendEmptyMessage;
})();

@ -3,7 +3,6 @@
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function () {
window.libloki = window.libloki || {}; window.libloki = window.libloki || {};
window.libloki.serviceNodes = window.libloki.serviceNodes || {};
function consolidateLists(lists, threshold = 1){ function consolidateLists(lists, threshold = 1){
if (typeof threshold !== 'number') { if (typeof threshold !== 'number') {
@ -31,5 +30,7 @@
.map(keyValue => keyValue[0]); .map(keyValue => keyValue[0]);
} }
window.libloki.serviceNodes.consolidateLists = consolidateLists; window.libloki.serviceNodes = {
consolidateLists,
};
})(); })();

@ -1,6 +1,97 @@
/* global window, libsignal, textsecure */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function () { (function () {
'use strict'; window.libloki = window.libloki || {};
async function getPreKeyBundleForContact(pubKey) {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const identityKey = myKeyPair.pubKey;
// Retrieve ids. The ids stored are always the latest generated + 1
const signedKeyId = textsecure.storage.get('signedKeyId', 2) - 1;
const [signedKey, preKey] = await Promise.all([
textsecure.storage.protocol.loadSignedPreKey(signedKeyId),
new Promise(async resolve => {
// retrieve existing prekey if we already generated one for that recipient
const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContact(
pubKey
);
if (storedPreKey) {
resolve({ pubKey: storedPreKey.pubKey, keyId: storedPreKey.keyId });
} else {
// generate and store new prekey
const preKeyId = textsecure.storage.get('maxPreKeyId', 1);
textsecure.storage.put('maxPreKeyId', preKeyId + 1);
const newPreKey = await libsignal.KeyHelper.generatePreKey(preKeyId);
await textsecure.storage.protocol.storePreKey(
newPreKey.keyId,
newPreKey.keyPair,
pubKey
);
resolve({ pubKey: newPreKey.keyPair.pubKey, keyId: preKeyId });
}
}),
]);
return {
identityKey: new Uint8Array(identityKey),
deviceId: 1, // TODO: fetch from somewhere
preKeyId: preKey.keyId,
signedKeyId,
preKey: new Uint8Array(preKey.pubKey),
signedKey: new Uint8Array(signedKey.pubKey),
signature: new Uint8Array(signedKey.signature),
};
}
async function saveContactPreKeyBundle({
pubKey,
preKeyId,
preKey,
signedKeyId,
signedKey,
signature,
}) {
const signedPreKey = {
keyId: signedKeyId,
publicKey: signedKey,
signature,
};
const signedKeyPromise = textsecure.storage.protocol.storeContactSignedPreKey(
pubKey,
signedPreKey
);
const preKeyObject = {
publicKey: preKey,
keyId: preKeyId,
};
const preKeyPromise = textsecure.storage.protocol.storeContactPreKey(
pubKey,
preKeyObject
);
await Promise.all([signedKeyPromise, preKeyPromise]);
}
async function removeContactPreKeyBundle(pubKey) {
await Promise.all([
textsecure.storage.protocol.removeContactPreKey(pubKey),
textsecure.storage.protocol.removeContactSignedPreKey(pubKey),
]);
}
window.libloki.storage = {
getPreKeyBundleForContact,
saveContactPreKeyBundle,
removeContactPreKeyBundle,
};
// Libloki protocol store
const store = window.SignalProtocolStore.prototype; const store = window.SignalProtocolStore.prototype;
@ -116,5 +207,4 @@
store.clearContactSignedPreKeysStore = async () => { store.clearContactSignedPreKeysStore = async () => {
await window.Signal.Data.removeAllContactSignedPreKeys(); await window.Signal.Data.removeAllContactSignedPreKeys();
}; };
})(); })();

@ -0,0 +1,38 @@
/* global libsignal, libloki, textsecure, StringView */
'use strict';
describe('Crypto', () => {
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.crypto.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);
});
});
});

@ -24,12 +24,15 @@
<script type="text/javascript" src="../../libtextsecure/protobufs.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="../../libtextsecure/stringview.js" data-cover></script>
<script type="text/javascript" src="../libloki-protocol.js" data-cover></script> <script type="text/javascript" src="../api.js" data-cover></script>
<script type="text/javascript" src="../crypto.js" data-cover></script>
<script type="text/javascript" src="../service_nodes.js" data-cover></script> <script type="text/javascript" src="../service_nodes.js" data-cover></script>
<script type="text/javascript" src="../storage.js" data-cover></script>
<script type="text/javascript" src="crypto_test.js"></script>
<script type="text/javascript" src="proof-of-work_test.js"></script> <script type="text/javascript" src="proof-of-work_test.js"></script>
<script type="text/javascript" src="libloki-protocol_test.js"></script>
<script type="text/javascript" src="service_nodes_test.js"></script> <script type="text/javascript" src="service_nodes_test.js"></script>
<script type="text/javascript" src="storage_test.js"></script>
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. --> <!-- 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( --> <!-- NOTE: blanket doesn't support modern syntax and will choke until we find a replacement. :0( -->

@ -1,103 +0,0 @@
/* 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),
};
textsecure.storage.put('signedKeyId', 2);
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);
});
});

@ -8,7 +8,8 @@ const {
greaterThan, greaterThan,
} = pow; } = pow;
describe('Proof of Work Worker', () => { describe('Proof of Work', () => {
describe('#incrementNonce', () => {
it('should increment a Uint8Array nonce correctly', () => { it('should increment a Uint8Array nonce correctly', () => {
const arr1Before = new Uint8Array([0,0,0,0,0,0,0,0]); const arr1Before = new Uint8Array([0,0,0,0,0,0,0,0]);
const arr1After = incrementNonce(arr1Before); const arr1After = incrementNonce(arr1Before);
@ -22,7 +23,7 @@ describe('Proof of Work Worker', () => {
assert.strictEqual(arr1After[7], 1); assert.strictEqual(arr1After[7], 1);
}); });
it('should increment a Uint8Array nonce correctly', () => { it('should increment a Uint8Array nonce correctly in a loop', () => {
let arr = new Uint8Array([0,0,0,0,0,0,0,0]); 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])); 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]); arr = new Uint8Array([0,0,0,0,0,0,0,0]);
@ -33,7 +34,9 @@ describe('Proof of Work Worker', () => {
arr = new Uint8Array([255,255,255,255,255,255,255,255]); 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])); assert.deepEqual(incrementNonce(arr), new Uint8Array([0,0,0,0,0,0,0,0]));
}); });
});
describe('#calcTarget', () => {
it('should calculate a correct difficulty target', () => { it('should calculate a correct difficulty target', () => {
// These values will need to be updated if we adjust the difficulty settings // These values will need to be updated if we adjust the difficulty settings
let payloadLen = 625; let payloadLen = 625;
@ -47,7 +50,9 @@ describe('Proof of Work Worker', () => {
actualTarget = calcTarget(ttl, payloadLen, 10); actualTarget = calcTarget(ttl, payloadLen, 10);
assert.deepEqual(actualTarget, expectedTarget); assert.deepEqual(actualTarget, expectedTarget);
}); });
});
describe('#greaterThan', () => {
it('should correclty compare two Uint8Arrays', () => { it('should correclty compare two Uint8Arrays', () => {
let arr1 = new Uint8Array([0,0,0,0,0,0,0,0,0,1]); 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]); let arr2 = new Uint8Array([0,0,0,0,0,0,0,0,0,1]);
@ -65,7 +70,9 @@ describe('Proof of Work Worker', () => {
arr2 = new Uint8Array([0,0]); arr2 = new Uint8Array([0,0]);
assert.isFalse(greaterThan(arr1, arr2)) assert.isFalse(greaterThan(arr1, arr2))
}); });
});
describe('#bufferToBase64', () => {
it('should correclty convert a Uint8Array to a base64 string', () => { it('should correclty convert a Uint8Array to a base64 string', () => {
let arr = new Uint8Array([1,2,3]); let arr = new Uint8Array([1,2,3]);
let expected = 'AQID'; let expected = 'AQID';
@ -77,7 +84,9 @@ describe('Proof of Work Worker', () => {
expected = ''; expected = '';
assert.strictEqual(bufferToBase64(arr), expected); assert.strictEqual(bufferToBase64(arr), expected);
}); });
});
describe('#bigIntToUint8Array', () => {
it('should correclty convert a BigInteger to a Uint8Array', () => { it('should correclty convert a BigInteger to a Uint8Array', () => {
let bigInt = JSBI.BigInt(Number.MAX_SAFE_INTEGER); let bigInt = JSBI.BigInt(Number.MAX_SAFE_INTEGER);
let expected = new Uint8Array([0, 31, 255, 255, 255, 255, 255, 255]); let expected = new Uint8Array([0, 31, 255, 255, 255, 255, 255, 255]);
@ -93,3 +102,4 @@ describe('Proof of Work Worker', () => {
assert.deepEqual(bigIntToUint8Array(bigInt), expected); assert.deepEqual(bigIntToUint8Array(bigInt), expected);
}); });
}); });
});

@ -1,40 +1,50 @@
/* global libloki, chai */ /* global libloki, assert */
describe('ServiceNodes', () => { describe('ServiceNodes', () => {
describe('#consolidateLists', () => { describe('#consolidateLists', () => {
it('should throw when provided a non-iterable list', () => { it('should throw when provided a non-iterable list', () => {
chai.expect(() => libloki.serviceNodes.consolidateLists(null, 1)).to.throw(); assert.throws(() => libloki.serviceNodes.consolidateLists(null, 1), Error);
}); });
it('should throw when provided a non-iterable item in the list', () => { it('should throw when provided a non-iterable item in the list', () => {
chai.expect(() => libloki.serviceNodes.consolidateLists([1, 2, 3], 1)).to.throw(); assert.throws(() => libloki.serviceNodes.consolidateLists([1, 2, 3], 1), Error);
}); });
it('should throw when provided a non-number threshold', () => { it('should throw when provided a non-number threshold', () => {
chai.expect(() => libloki.serviceNodes.consolidateLists([], 'a')).to.throw(); assert.throws(
() => libloki.serviceNodes.consolidateLists([], 'a'),
'Provided threshold is not a number'
);
}); });
it('should return an empty array when the input is an empty array', () => { it('should return an empty array when the input is an empty array', () => {
const result = libloki.serviceNodes.consolidateLists([]); const result = libloki.serviceNodes.consolidateLists([]);
chai.expect(result).to.deep.equal([]); assert.deepEqual(result, []);
}); });
it('should return the input when only 1 list is provided', () => { it('should return the input when only 1 list is provided', () => {
const result = libloki.serviceNodes.consolidateLists([['a', 'b', 'c']]); const result = libloki.serviceNodes.consolidateLists([['a', 'b', 'c']]);
chai.expect(result).to.deep.equal(['a', 'b', 'c']); assert.deepEqual(result, ['a', 'b', 'c']);
}); });
it('should return the union of all lists when threshold is 0', () => { it('should return the union of all lists when threshold is 0', () => {
const result = libloki.serviceNodes.consolidateLists([ const result = libloki.serviceNodes.consolidateLists([
['a', 'b', 'c', 'h'], ['a', 'b', 'c', 'h'],
['d', 'e', 'f', 'g'], ['d', 'e', 'f', 'g'],
['g', 'h'], ['g', 'h'],
], 0); ], 0);
chai.expect(result.sort()).to.deep.equal(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']); assert.deepEqual(result.sort(), ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']);
}); });
it('should return the intersection of all lists when threshold is 1', () => { it('should return the intersection of all lists when threshold is 1', () => {
const result = libloki.serviceNodes.consolidateLists([ const result = libloki.serviceNodes.consolidateLists([
['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd'],
['a', 'e', 'f', 'g'], ['a', 'e', 'f', 'g'],
['a', 'h'], ['a', 'h'],
], 1); ], 1);
chai.expect(result).to.deep.equal(['a']); assert.deepEqual(result, ['a']);
}); });
it('should return the elements that have an occurence >= the provided threshold', () => { it('should return the elements that have an occurence >= the provided threshold', () => {
const result = libloki.serviceNodes.consolidateLists([ const result = libloki.serviceNodes.consolidateLists([
['a', 'b', 'c', 'd', 'e', 'f', 'g'], ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
@ -42,8 +52,9 @@ describe('ServiceNodes', () => {
['a', 'b', 'c', 'd', 'e', 'f', 'g'], ['a', 'b', 'c', 'd', 'e', 'f', 'g'],
['a', 'b', 'c', 'd', 'e', 'g', 'h'], ['a', 'b', 'c', 'd', 'e', 'g', 'h'],
], 3/4); ], 3/4);
chai.expect(result).to.deep.equal(['a', 'b', 'c', 'd', 'e', 'f', 'g']); assert.deepEqual(result, ['a', 'b', 'c', 'd', 'e', 'f', 'g']);
}); });
it('should work with sets as well', () => { it('should work with sets as well', () => {
const result = libloki.serviceNodes.consolidateLists(new Set([ const result = libloki.serviceNodes.consolidateLists(new Set([
new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']), new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']),
@ -51,7 +62,7 @@ describe('ServiceNodes', () => {
new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']), new Set(['a', 'b', 'c', 'd', 'e', 'f', 'g']),
new Set(['a', 'b', 'c', 'd', 'e', 'g', 'h']), new Set(['a', 'b', 'c', 'd', 'e', 'g', 'h']),
]), 3/4); ]), 3/4);
chai.expect(result).to.deep.equal(['a', 'b', 'c', 'd', 'e', 'f', 'g']); assert.deepEqual(result, ['a', 'b', 'c', 'd', 'e', 'f', 'g']);
}); });
}); });
}); });

@ -0,0 +1,72 @@
/* global libsignal, libloki, textsecure, StringView */
'use strict';
describe('Storage', () => {
let testKey;
const store = textsecure.storage.protocol;
describe('#getPreKeyBundleForContact', () => {
beforeEach(async () => {
clearDatabase();
testKey = {
pubKey: libsignal.crypto.getRandomBytes(33),
privKey: libsignal.crypto.getRandomBytes(32),
};
textsecure.storage.put('signedKeyId', 2);
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.storage.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.storage.getPreKeyBundleForContact(pubKeyString);
const bundle2 = await libloki.storage.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.storage.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);
});
});
});

@ -605,7 +605,7 @@ MessageReceiver.prototype.extend({
textsecure.storage.protocol textsecure.storage.protocol
); );
const fallBackSessionCipher = new libloki.FallBackSessionCipher( const fallBackSessionCipher = new libloki.crypto.FallBackSessionCipher(
address address
); );
@ -1238,7 +1238,7 @@ MessageReceiver.prototype.extend({
); );
} }
await libloki.saveContactPreKeyBundle({ await libloki.storage.saveContactPreKeyBundle({
pubKey, pubKey,
preKeyId, preKeyId,
signedKeyId, signedKeyId,

@ -121,7 +121,7 @@ OutgoingMessage.prototype = {
} }
return builder.processPreKey(device).then(async () => { return builder.processPreKey(device).then(async () => {
// TODO: only remove the keys that were used above! // TODO: only remove the keys that were used above!
await window.libloki.removeContactPreKeyBundle(number); await window.libloki.storage.removeContactPreKeyBundle(number);
return true; return true;
} }
).catch(error => { ).catch(error => {
@ -288,7 +288,7 @@ OutgoingMessage.prototype = {
const address = new libsignal.SignalProtocolAddress(number, deviceId); const address = new libsignal.SignalProtocolAddress(number, deviceId);
const ourKey = textsecure.storage.user.getNumber(); const ourKey = textsecure.storage.user.getNumber();
const options = {}; const options = {};
const fallBackCipher = new libloki.FallBackSessionCipher(address); const fallBackCipher = new libloki.crypto.FallBackSessionCipher(address);
// Check if we need to attach the preKeys // Check if we need to attach the preKeys
let sessionCipher; let sessionCipher;
@ -297,7 +297,7 @@ OutgoingMessage.prototype = {
const isEndSession = flags === textsecure.protobuf.DataMessage.Flags.END_SESSION; const isEndSession = flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
if (isFriendRequest || isEndSession) { if (isFriendRequest || isEndSession) {
// Encrypt them with the fallback // Encrypt them with the fallback
const pkb = await libloki.getPreKeyBundleForContact(number); const pkb = await libloki.storage.getPreKeyBundleForContact(number);
const preKeyBundleMessage = new textsecure.protobuf.PreKeyBundleMessage(pkb); const preKeyBundleMessage = new textsecure.protobuf.PreKeyBundleMessage(pkb);
this.message.preKeyBundleMessage = preKeyBundleMessage; this.message.preKeyBundleMessage = preKeyBundleMessage;
window.log.info('attaching prekeys to outgoing message'); window.log.info('attaching prekeys to outgoing message');

Loading…
Cancel
Save