add SessionProtocol test

pull/1166/head
Audric Ackermann 5 years ago
parent 15f71cb9c8
commit dc00923f17
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -1,6 +1,11 @@
import { SessionResetMessage } from '../messages/outgoing'; import { SessionResetMessage } from '../messages/outgoing';
// import { MessageSender } from '../sending'; // import { MessageSender } from '../sending';
import { createOrUpdateItem, getItemById } from '../../../js/modules/data'; import { createOrUpdateItem, getItemById } from '../../../js/modules/data';
import { libloki, libsignal, textsecure } from '../../window';
import { MessageSender } from '../sending';
import { RawMessage } from '../types/RawMessage';
import { EncryptionType } from '../types/EncryptionType';
import { TextEncoder } from 'util';
interface StringToNumberMap { interface StringToNumberMap {
[key: string]: number; [key: string]: number;
@ -8,17 +13,15 @@ interface StringToNumberMap {
// tslint:disable: function-name // tslint:disable: function-name
// tslint:disable: no-unnecessary-class // tslint:disable: no-unnecessary-class
export class SessionProtocol { export class SessionProtocol {
private static dbLoaded: Boolean = false; private static dbLoaded: Boolean = false;
/** /**
* This map olds the sent session timestamps, i.e. session requests message effectively sent to the recipient. * This map olds the sent session timestamps, i.e. session requests message effectively sent to the recipient.
* It is backed by a database entry so it's loaded from db on startup. * It is backed by a database entry so it's loaded from db on startup.
* This map should not be used directly, but instead through * This map should not be used directly, but instead through
* `updateSendSessionTimestamp()`, `getSendSessionRequest()` or `hasSendSessionRequest()` * `updateSendSessionTimestamp()`, or `hasSendSessionRequest()`
*/ */
private static sentSessionsTimestamp: StringToNumberMap; private static sentSessionsTimestamp: StringToNumberMap;
/** /**
* This map olds the processed session timestamps, i.e. when we received a session request and handled it. * This map olds the processed session timestamps, i.e. when we received a session request and handled it.
* It is backed by a database entry so it's loaded from db on startup. * It is backed by a database entry so it's loaded from db on startup.
@ -34,13 +37,24 @@ export class SessionProtocol {
*/ */
private static readonly pendingSendSessionsTimestamp: Set<string> = new Set(); private static readonly pendingSendSessionsTimestamp: Set<string> = new Set();
public static getSentSessionsTimestamp(): Readonly<StringToNumberMap> {
return SessionProtocol.sentSessionsTimestamp;
}
public static getProcessedSessionsTimestamp(): Readonly<StringToNumberMap> {
return SessionProtocol.processedSessionsTimestamp;
}
public static getPendingSendSessionTimestamp(): Readonly<Set<string>> {
return SessionProtocol.pendingSendSessionsTimestamp;
}
/** Returns true if we already have a session with that device */ /** Returns true if we already have a session with that device */
public static async hasSession(device: string): Promise<boolean> { public static async hasSession(device: string): Promise<boolean> {
// Session does not use the concept of a deviceId, thus it's always 1 // Session does not use the concept of a deviceId, thus it's always 1
const address = new window.libsignal.SignalProtocolAddress(device, 1); const address = new libsignal.SignalProtocolAddress(device, 1);
const sessionCipher = new window.libsignal.SessionCipher( const sessionCipher = new libsignal.SessionCipher(
window.textsecure.storage.protocol, textsecure.storage.protocol,
address address
); );
@ -52,7 +66,9 @@ export class SessionProtocol {
* if a session request to that device is right now being sent. * if a session request to that device is right now being sent.
*/ */
public static async hasSentSessionRequest(device: string): Promise<boolean> { public static async hasSentSessionRequest(device: string): Promise<boolean> {
const pendingSend = SessionProtocol.pendingSendSessionsTimestamp.has(device); const pendingSend = SessionProtocol.pendingSendSessionsTimestamp.has(
device
);
const hasSent = await SessionProtocol._hasSentSessionRequest(device); const hasSent = await SessionProtocol._hasSentSessionRequest(device);
return pendingSend || hasSent; return pendingSend || hasSent;
@ -67,13 +83,17 @@ export class SessionProtocol {
public static async sendSessionRequestIfNeeded( public static async sendSessionRequestIfNeeded(
device: string device: string
): Promise<void> { ): Promise<void> {
if (SessionProtocol.hasSession(device) || SessionProtocol.hasSentSessionRequest(device)) { if (
(await SessionProtocol.hasSession(device)) ||
(await SessionProtocol.hasSentSessionRequest(device))
) {
return Promise.resolve(); return Promise.resolve();
} }
const preKeyBundle = await window.libloki.storage.getPreKeyBundleForContact( const preKeyBundle = await libloki.storage.getPreKeyBundleForContact(
device device
); );
const sessionReset = new SessionResetMessage({ const sessionReset = new SessionResetMessage({
preKeyBundle, preKeyBundle,
timestamp: Date.now(), timestamp: Date.now(),
@ -92,17 +112,30 @@ export class SessionProtocol {
// mark the session as being pending send with current timestamp // mark the session as being pending send with current timestamp
// so we know we already triggered a new session with that device // so we know we already triggered a new session with that device
SessionProtocol.pendingSendSessionsTimestamp.add(device); SessionProtocol.pendingSendSessionsTimestamp.add(device);
// const rawMessage = toRawMessage(message);
// // TODO: Send out the request via MessageSender // FIXME to remove
function toRawMessage(m: any): RawMessage {
// try { return {
// await MessageSender.send(rawMessage); identifier: 'identifier',
// await SessionProtocolupdateSentSessionTimestamp(device, timestamp); plainTextBuffer: new TextEncoder().encode('jk'),
// } catch (e) { timestamp: Date.now(),
// window.console.log('Failed to send session request to', device); device: 'device',
// } finally { ttl: 10,
// SessionProtocolpendingSendSessionsTimestamp.delete(device); encryption: EncryptionType.SessionReset,
// } };
}
try {
// TODO: Send out the request via MessageSender
const rawMessage = toRawMessage(message);
await MessageSender.send(rawMessage);
await SessionProtocol.updateSentSessionTimestamp(device, timestamp);
} catch (e) {
console.log('Failed to send session request to:', device);
console.log('e:', e);
} finally {
SessionProtocol.pendingSendSessionsTimestamp.delete(device);
}
} }
/** /**
@ -117,7 +150,8 @@ export class SessionProtocol {
device: string, device: string,
messageTimestamp: number messageTimestamp: number
): Promise<boolean> { ): Promise<boolean> {
const existingSentTimestamp = (await SessionProtocol.getSentSessionRequest(device)) || 0; const existingSentTimestamp =
(await SessionProtocol.getSentSessionRequest(device)) || 0;
const existingProcessedTimestamp = const existingProcessedTimestamp =
(await SessionProtocol.getProcessedSessionRequest(device)) || 0; (await SessionProtocol.getProcessedSessionRequest(device)) || 0;
@ -137,24 +171,19 @@ export class SessionProtocol {
SessionProtocol.processedSessionsTimestamp = {}; SessionProtocol.processedSessionsTimestamp = {};
} }
/** /**
* We only need to fetch once from the database, because we are the only one writing to it * We only need to fetch once from the database, because we are the only one writing to it
*/ */
private static async fetchFromDBIfNeeded(): Promise<void> { private static async fetchFromDBIfNeeded(): Promise<void> {
if (!SessionProtocol.dbLoaded) { if (!SessionProtocol.dbLoaded) {
const sentItem = await getItemById( const sentItem = await getItemById('sentSessionsTimestamp');
'sentSessionsTimestamp'
);
if (sentItem) { if (sentItem) {
SessionProtocol.sentSessionsTimestamp = sentItem.value; SessionProtocol.sentSessionsTimestamp = sentItem.value;
} else { } else {
SessionProtocol.sentSessionsTimestamp = {}; SessionProtocol.sentSessionsTimestamp = {};
} }
const processedItem = await getItemById( const processedItem = await getItemById('processedSessionsTimestamp');
'processedSessionsTimestamp'
);
if (processedItem) { if (processedItem) {
SessionProtocol.processedSessionsTimestamp = processedItem.value; SessionProtocol.processedSessionsTimestamp = processedItem.value;
} else { } else {
@ -190,11 +219,11 @@ export class SessionProtocol {
timestamp: number | undefined, timestamp: number | undefined,
map: StringToNumberMap map: StringToNumberMap
): Promise<boolean> { ): Promise<boolean> {
await SessionProtocol.fetchFromDBIfNeeded();
if (!timestamp) { if (!timestamp) {
if (!!map[device]) { if (!!map[device]) {
delete map.device; // tslint:disable-next-line: no-dynamic-delete
// FIXME double check how are args handle in ts (by ref/value) delete map[device];
return true; return true;
} }
@ -214,7 +243,14 @@ export class SessionProtocol {
device: string, device: string,
timestamp: number | undefined timestamp: number | undefined
): Promise<void> { ): Promise<void> {
if (SessionProtocol.updateSessionTimestamp(device, timestamp, SessionProtocol.sentSessionsTimestamp)) { await SessionProtocol.fetchFromDBIfNeeded();
if (
SessionProtocol.updateSessionTimestamp(
device,
timestamp,
SessionProtocol.sentSessionsTimestamp
)
) {
await SessionProtocol.writeToDBSentSessions(); await SessionProtocol.writeToDBSentSessions();
} }
} }
@ -226,7 +262,14 @@ export class SessionProtocol {
device: string, device: string,
timestamp: number | undefined timestamp: number | undefined
): Promise<void> { ): Promise<void> {
if (SessionProtocol.updateSessionTimestamp(device, timestamp, SessionProtocol.processedSessionsTimestamp)) { await SessionProtocol.fetchFromDBIfNeeded();
if (
SessionProtocol.updateSessionTimestamp(
device,
timestamp,
SessionProtocol.processedSessionsTimestamp
)
) {
await SessionProtocol.writeToDBProcessedSessions(); await SessionProtocol.writeToDBProcessedSessions();
} }
} }
@ -246,16 +289,24 @@ export class SessionProtocol {
private static async getSentSessionRequest( private static async getSentSessionRequest(
device: string device: string
): Promise<number | undefined> { ): Promise<number | undefined> {
return SessionProtocol.getSessionRequest(device, SessionProtocol.sentSessionsTimestamp); return SessionProtocol.getSessionRequest(
device,
SessionProtocol.sentSessionsTimestamp
);
} }
private static async getProcessedSessionRequest( private static async getProcessedSessionRequest(
device: string device: string
): Promise<number | undefined> { ): Promise<number | undefined> {
return SessionProtocol.getSessionRequest(device, SessionProtocol.processedSessionsTimestamp); return SessionProtocol.getSessionRequest(
device,
SessionProtocol.processedSessionsTimestamp
);
} }
private static async _hasSentSessionRequest(device: string): Promise<boolean> { private static async _hasSentSessionRequest(
device: string
): Promise<boolean> {
await SessionProtocol.fetchFromDBIfNeeded(); await SessionProtocol.fetchFromDBIfNeeded();
return !!SessionProtocol.sentSessionsTimestamp[device]; return !!SessionProtocol.sentSessionsTimestamp[device];

@ -1,4 +1,4 @@
import {SessionProtocol} from './SessionProtocol'; import { SessionProtocol } from './SessionProtocol';
import * as MultiDeviceProtocol from './MultiDeviceProtocol'; import * as MultiDeviceProtocol from './MultiDeviceProtocol';
export { SessionProtocol, MultiDeviceProtocol }; export { SessionProtocol, MultiDeviceProtocol };

@ -7,6 +7,7 @@ import { Stubs, TestUtils } from '../../test-utils';
import { UserUtil } from '../../../util'; import { UserUtil } from '../../../util';
import { SignalService } from '../../../protobuf'; import { SignalService } from '../../../protobuf';
// tslint:disable-next-line: max-func-body-length
describe('MessageEncrypter', () => { describe('MessageEncrypter', () => {
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
const ourNumber = 'ourNumber'; const ourNumber = 'ourNumber';

@ -1,9 +1,340 @@
// import { expect } from 'chai'; import { expect } from 'chai';
// import { SessionProtocol } from '../../../session/protocols'; import { SessionProtocol } from '../../../session/protocols';
import * as sinon from 'sinon';
import { Stubs, TestUtils, timeout } from '../../test-utils';
import { UserUtil } from '../../../util';
import { SessionResetMessage } from '../../../session/messages/outgoing';
import { TextEncoder } from 'util';
import { MessageSender } from '../../../session/sending';
// describe('SessionProtocol', () => { // tslint:disable-next-line: max-func-body-length
// it('has ', () => { describe('SessionProtocol', () => {
const sandbox = sinon.createSandbox();
const ourNumber = 'ourNumber';
let getItemById: sinon.SinonStub;
let send: sinon.SinonStub;
// }); const resetMessage: SessionResetMessage = new SessionResetMessage({
timestamp: Date.now(),
preKeyBundle: {
identityKey: new TextEncoder().encode('identityKey'),
deviceId: 1,
preKeyId: 2,
signedKeyId: 3,
preKey: new TextEncoder().encode('preKey'),
signedKey: new TextEncoder().encode('signedKey'),
signature: new TextEncoder().encode('signature'),
},
});
// }); beforeEach(() => {
TestUtils.stubWindow('libsignal', {
SignalProtocolAddress: sandbox.stub(),
SessionCipher: Stubs.SessionCipherStub,
} as any);
TestUtils.stubWindow('libloki', {
storage: {
getPreKeyBundleForContact: sandbox.stub(),
},
});
TestUtils.stubWindow('textsecure', {
storage: {
protocol: sandbox.stub(),
},
});
TestUtils.stubData('createOrUpdateItem');
getItemById = TestUtils.stubData('getItemById').resolves({ value: {} });
sandbox.stub(UserUtil, 'getCurrentDevicePubKey').resolves(ourNumber);
send = sandbox.stub(MessageSender, 'send' as any);
SessionProtocol.reset();
});
afterEach(() => {
sandbox.restore();
TestUtils.restoreStubs();
});
describe('db fetch', () => {
it('protocol: should fetch from DB `sentSessionsTimestamp` and `processedSessionsTimestamp`', async () => {
await SessionProtocol.hasSentSessionRequest('test');
expect(getItemById.calledWith('sentSessionsTimestamp'));
expect(getItemById.calledWith('processedSessionsTimestamp'));
expect(getItemById.callCount).to.equal(2);
});
it('protocol: should fetch only once', async () => {
await SessionProtocol.hasSentSessionRequest('test');
await SessionProtocol.hasSentSessionRequest('test');
await SessionProtocol.hasSentSessionRequest('test');
await SessionProtocol.hasSentSessionRequest('test');
expect(getItemById.calledWith('sentSessionsTimestamp'));
expect(getItemById.calledWith('processedSessionsTimestamp'));
expect(getItemById.callCount).to.equal(2);
});
});
describe('sendSessionRequest', () => {
beforeEach(async () => {
// trigger a sessionReset
await SessionProtocol.sendSessionRequest(resetMessage, 'deviceID');
});
it('protocol: sendSessionRequest should add the deviceID to the sentMap', async () => {
expect(SessionProtocol.getSentSessionsTimestamp())
.to.have.property('deviceID')
.to.be.approximately(Date.now(), 100);
});
it('protocol: sendSessionRequest should not have pendingSend set after', async () => {
expect(
SessionProtocol.getPendingSendSessionTimestamp()
).to.not.have.property('deviceID');
});
});
describe('onSessionEstablished', () => {
beforeEach(async () => {
// add an existing entry in the sentMap
await SessionProtocol.sendSessionRequest(resetMessage, 'deviceid');
});
it('protocol: onSessionEstablished should remove the device in sentTimestamps', async () => {
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
'deviceid'
);
await SessionProtocol.onSessionEstablished('deviceid');
expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property(
'deviceid'
);
});
it('protocol: onSessionEstablished should remove the device in sentTimestamps and ONLY that one', async () => {
// add a second item to the map
await SessionProtocol.sendSessionRequest(resetMessage, 'deviceid2');
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
'deviceid'
);
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
'deviceid2'
);
await SessionProtocol.onSessionEstablished('deviceid');
expect(SessionProtocol.getSentSessionsTimestamp()).to.not.have.property(
'deviceid'
);
expect(SessionProtocol.getSentSessionsTimestamp()).to.have.property(
'deviceid2'
);
});
});
describe('hasSentSessionRequest', () => {
it('protocol: hasSentSessionRequest returns false if a message was not sent to that device', async () => {
const hasSent = await SessionProtocol.hasSentSessionRequest('deviceid');
expect(hasSent).to.be.equal(
false,
'hasSent should be false for `deviceid`'
);
});
it('protocol: hasSentSessionRequest returns true if a message is already sent for that device', async () => {
// add an existing entry in the sentMap
await SessionProtocol.sendSessionRequest(resetMessage, 'deviceid');
const hasSent = await SessionProtocol.hasSentSessionRequest('deviceid');
expect(hasSent).to.be.equal(
true,
'hasSent should be true for `deviceid`'
);
});
// TODO add a test to validate that pending is filled when message is triggered and not yet sent
});
describe('sendSessionRequestIfNeeded', () => {
it('protocol: sendSessionRequestIfNeeded should send a new sessionMessage ', async () => {
// not called before, so the message reset sending should be triggered
await SessionProtocol.sendSessionRequestIfNeeded('deviceid');
expect(send.callCount).to.be.equal(
1,
'MessageSender.send() should have been called'
);
// check that the map is updated with that ID
const hasSent = await SessionProtocol.hasSentSessionRequest('deviceid');
expect(hasSent).to.be.equal(
true,
'hasSent should be true for `deviceid`'
);
});
it('protocol: sendSessionRequestIfNeeded should NOT send a new sessionMessage on second try ', async () => {
await SessionProtocol.sendSessionRequestIfNeeded('deviceid');
expect(send.callCount).to.be.equal(
1,
'MessageSender.send() should have been called'
);
// check that the map is updated with that ID
const hasSent = await SessionProtocol.hasSentSessionRequest('deviceid');
expect(hasSent).to.be.equal(
true,
'hasSent should be true for `deviceid`'
);
send.resetHistory();
// trigger a second call, Message.send().calledCount should still be 1
await SessionProtocol.sendSessionRequestIfNeeded('deviceid');
expect(send.callCount).to.be.equal(
0,
'MessageSender.send() should NOT have been called a second time'
);
});
});
it('hasSession: returns false as it is stubbed', () => {
expect(SessionProtocol.hasSession('deviceID')).to.be.equal(
false,
'hasSession() should be false as it is stubbed'
);
});
describe('onSessionRequestProcessed', () => {
it('protocol: onSessionRequestProcessed should insert a new item in the processedMap ', async () => {
// trigger the requestProcessed and check the map is updated
await SessionProtocol.onSessionRequestProcessed('deviceid');
expect(SessionProtocol.getProcessedSessionsTimestamp())
.to.have.property('deviceid')
.to.be.approximately(Date.now(), 5);
});
it('protocol: onSessionRequestProcessed should update an existing item in the processedMap ', async () => {
// trigger the requestProcessed and check the map is updated
// then trigger it a second time, and expect a change in the processed timestamp
await SessionProtocol.onSessionRequestProcessed('deviceid');
expect(SessionProtocol.getProcessedSessionsTimestamp())
.to.have.property('deviceid')
.to.be.approximately(Date.now(), 5);
await timeout(5);
const oldTimestamp = SessionProtocol.getProcessedSessionsTimestamp()
.deviceid;
await SessionProtocol.onSessionRequestProcessed('deviceid');
expect(SessionProtocol.getProcessedSessionsTimestamp())
.to.have.property('deviceid')
.to.be.approximately(Date.now(), 5)
.to.not.be.equal(oldTimestamp);
});
});
describe('shouldProcessSessionRequest', () => {
it('protocol: shouldProcessSessionRequest returns true if timestamp is more recent than processed timestamp', async () => {
await SessionProtocol.onSessionRequestProcessed('deviceid'); // adds a Date.now() entry
expect(
SessionProtocol.shouldProcessSessionRequest(
'deviceid',
Date.now() + 1000
)
).to.be.eventually.equal(
true,
'shouldProcessSessionRequest should return true when existingProcessed is less recent'
);
});
it('protocol: shouldProcessSessionRequest returns true if there is no processed timestamp yet for this device', async () => {
expect(
SessionProtocol.shouldProcessSessionRequest('deviceid', 100)
).to.be.eventually.equal(
true,
'shouldProcessSessionRequest should return false when existingProcessed is empty for this device'
);
});
it('protocol: shouldProcessSessionRequest returns false if timestamp is less recent than current processed timestamp', async () => {
await SessionProtocol.onSessionRequestProcessed('deviceid'); // adds a Date.now() entry
expect(
SessionProtocol.shouldProcessSessionRequest('deviceid', 100)
).to.be.eventually.equal(
false,
'shouldProcessSessionRequest should return false when existingProcessed is more recent'
);
});
it('protocol: shouldProcessSessionRequest returns false if timestamp is less recent than current sent timestamp', async () => {
await SessionProtocol.sendSessionRequest(resetMessage, 'deviceid'); // adds a Date.now() entry
expect(
SessionProtocol.shouldProcessSessionRequest('deviceid', 100)
).to.be.eventually.equal(
false,
'shouldProcessSessionRequest should return false when existingSent is more recent'
);
});
it('protocol: shouldProcessSessionRequest returns true if timestamp is more recent than current sent timestamp', async () => {
await SessionProtocol.sendSessionRequest(resetMessage, 'deviceid'); // adds a Date.now() entry
expect(
SessionProtocol.shouldProcessSessionRequest(
'deviceid',
Date.now() + 1000
)
).to.be.eventually.equal(
true,
'shouldProcessSessionRequest should return true when existingSent is less recent'
);
});
it('protocol: shouldProcessSessionRequest returns true if there is no sent timestamp', async () => {
expect(
SessionProtocol.shouldProcessSessionRequest('deviceid', 100)
).to.be.eventually.equal(
true,
'shouldProcessSessionRequest should return true as there is no sent timestamp'
);
});
it('protocol: shouldProcessSessionRequest returns false if there is a more recent sent but a less recent processed', async () => {
await SessionProtocol.sendSessionRequest(resetMessage, 'deviceid'); // adds a Date.now() entry
await timeout(100);
await SessionProtocol.onSessionRequestProcessed('deviceid'); // adds a Date.now() entry 100ms after
expect(
SessionProtocol.shouldProcessSessionRequest('deviceid', Date.now() - 50)
).to.be.eventually.equal(
false,
'shouldProcessSessionRequest should return false if there is a more recent sent but a less recent processed'
);
});
it('protocol: shouldProcessSessionRequest returns false if there is a more recent processed but a less recent sent', async () => {
await SessionProtocol.onSessionRequestProcessed('deviceid'); // adds a Date.now() entry
await timeout(100);
await SessionProtocol.sendSessionRequest(resetMessage, 'deviceid'); // adds a Date.now() entry 100ms after
expect(
SessionProtocol.shouldProcessSessionRequest('deviceid', Date.now() - 50)
).to.be.eventually.equal(
false,
'shouldProcessSessionRequest should return false if there is a more recent processed but a less recent sent'
);
});
it('protocol: shouldProcessSessionRequest returns true if both sent and processed timestamp are older', async () => {
await SessionProtocol.onSessionRequestProcessed('deviceid'); // adds a Date.now() entry
await SessionProtocol.sendSessionRequest(resetMessage, 'deviceid'); // adds a Date.now() entry
expect(
SessionProtocol.shouldProcessSessionRequest(
'deviceid',
Date.now() + 1000
)
).to.be.eventually.equal(
true,
'shouldProcessSessionRequest should return true if there if both processed and sent are set but are older'
);
});
});
});

@ -17,4 +17,8 @@ export class SessionCipherStub {
body: Buffer.from(buffer).toString('binary'), body: Buffer.from(buffer).toString('binary'),
}; };
} }
public async hasOpenSession(): Promise<Boolean> {
return false;
}
} }

Loading…
Cancel
Save