Made changes to how messages are sent.

Instead of blocking the message queue when we don't have a session, we instead just send out a session request and send the queued messages using fallback encryption.
This means that users will be able to message right away without having to wait.
The only down side is that all messages sent before sessions are established will be using the weaker encryption.

This change also means we have to detach session requests from envelope type (which is a good thing) and thus now a message is a session request if it contains a preKeyBundle.
pull/1233/head
Mikunj 5 years ago
parent 4381d0135f
commit 646973e330

@ -50,7 +50,7 @@ describe('Message Syncing', function() {
// Linking Alice2 to Alice1
// alice2 should trigger auto FR with bob1 as it's one of her friend
// and alice2 should trigger a SESSION_REQUEST with bob1 as he is in a closed group with her
// and alice2 should trigger a FALLBACK_MESSAGE with bob1 as he is in a closed group with her
await common.linkApp2ToApp(Alice1, Alice2, common.TEST_PUBKEY1);
await common.timeout(25000);
@ -119,7 +119,7 @@ describe('Message Syncing', function() {
// once autoFR is auto-accepted, alice2 trigger contact sync
await common.logsContains(
bob1Logs,
`Received SESSION_REQUEST from source: ${alice2Pubkey}`,
`Received FALLBACK_MESSAGE from source: ${alice2Pubkey}`,
1
);
await common.logsContains(

@ -8,7 +8,7 @@
const DebugFlagsEnum = {
GROUP_SYNC_MESSAGES: 1,
CONTACT_SYNC_MESSAGES: 2,
SESSION_REQUEST_MESSAGES: 8,
FALLBACK_MESSAGES: 8,
SESSION_MESSAGE_SENDING: 16,
SESSION_BACKGROUND_MESSAGE: 32,
GROUP_REQUEST_INFO: 64,

@ -153,7 +153,7 @@
ivAndCiphertext
).toString('binary');
return {
type: textsecure.protobuf.Envelope.Type.SESSION_REQUEST,
type: textsecure.protobuf.Envelope.Type.FALLBACK_MESSAGE,
body: binaryIvAndCiphertext,
registrationId: undefined,
};

@ -19,12 +19,12 @@ describe('Crypto', () => {
fallbackCipher = new libloki.crypto.FallBackSessionCipher(address);
});
it('should encrypt fallback cipher messages as friend requests', async () => {
it('should encrypt fallback cipher messages as fallback messages', async () => {
const buffer = new ArrayBuffer(10);
const { type } = await fallbackCipher.encrypt(buffer);
assert.strictEqual(
type,
textsecure.protobuf.Envelope.Type.SESSION_REQUEST
textsecure.protobuf.Envelope.Type.FALLBACK_MESSAGE
);
});

@ -325,8 +325,7 @@ OutgoingMessage.prototype = {
// END_SESSION means Session reset message
const isEndSession =
flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
const isSessionRequest =
flags === textsecure.protobuf.DataMessage.Flags.SESSION_REQUEST;
const isSessionRequest = false;
if (enableFallBackEncryption || isEndSession) {
// Encrypt them with the fallback

@ -13,7 +13,7 @@ message Envelope {
RECEIPT = 5;
UNIDENTIFIED_SENDER = 6;
MEDIUM_GROUP_CIPHERTEXT = 7;
SESSION_REQUEST = 101; // contains prekeys and is using simple encryption
FALLBACK_MESSAGE = 101; // contains prekeys and is using simple encryption
}
optional Type type = 1;

@ -183,9 +183,9 @@ async function decryptUnidentifiedSender(
}
// We might have substituted the type based on decrypted content
if (type === SignalService.Envelope.Type.SESSION_REQUEST) {
if (type === SignalService.Envelope.Type.FALLBACK_MESSAGE) {
// eslint-disable-next-line no-param-reassign
envelope.type = SignalService.Envelope.Type.SESSION_REQUEST;
envelope.type = SignalService.Envelope.Type.FALLBACK_MESSAGE;
}
const blocked = await isBlocked(sender.getName());
@ -227,8 +227,8 @@ async function doDecrypt(
return lokiSessionCipher.decryptWhisperMessage(ciphertext).then(unpad);
case SignalService.Envelope.Type.MEDIUM_GROUP_CIPHERTEXT:
return decryptForMediumGroup(envelope, ciphertext);
case SignalService.Envelope.Type.SESSION_REQUEST: {
window.log.info('session-request message from ', envelope.source);
case SignalService.Envelope.Type.FALLBACK_MESSAGE: {
window.log.info('fallback message from ', envelope.source);
const fallBackSessionCipher = new libloki.crypto.FallBackSessionCipher(
address
@ -344,13 +344,13 @@ export async function innerHandleContentMessage(
const content = SignalService.Content.decode(new Uint8Array(plaintext));
const { SESSION_REQUEST } = SignalService.Envelope.Type;
const { FALLBACK_MESSAGE } = SignalService.Envelope.Type;
await ConversationController.getOrCreateAndWait(envelope.source, 'private');
if (envelope.type === SESSION_REQUEST) {
await handleSessionRequestMessage(envelope, content);
} else {
if (content.preKeyBundleMessage) {
await handleSessionRequestMessage(envelope, content.preKeyBundleMessage);
} else if (envelope.type !== FALLBACK_MESSAGE) {
const device = new PubKey(envelope.source);
await SessionProtocol.onSessionEstablished(device);

@ -25,10 +25,9 @@ export async function handleEndSession(number: string): Promise<void> {
export async function handleSessionRequestMessage(
envelope: EnvelopePlus,
content: SignalService.Content
preKeyBundleMessage: SignalService.IPreKeyBundleMessage
) {
const { libsignal, libloki, StringView, textsecure, dcodeIO, log } = window;
const { preKeyBundleMessage } = content;
window.console.log(
`Received SESSION_REQUEST from source: ${envelope.source}`

@ -53,7 +53,7 @@ export async function encrypt(
}
let innerCipherText: CipherTextObject;
if (encryptionType === EncryptionType.SessionRequest) {
if (encryptionType === EncryptionType.Fallback) {
const cipher = new window.libloki.crypto.FallBackSessionCipher(address);
innerCipherText = await cipher.encrypt(plainText.buffer);
} else {

@ -144,7 +144,7 @@ export class SessionProtocol {
SessionProtocol.pendingSendSessionsTimestamp.add(pubkey.key);
try {
const rawMessage = MessageUtils.toRawMessage(pubkey, message);
const rawMessage = await MessageUtils.toRawMessage(pubkey, message);
await MessageSender.send(rawMessage);
await SessionProtocol.updateSentSessionTimestamp(pubkey.key, timestamp);
} catch (e) {

@ -130,10 +130,9 @@ export class MessageQueue implements MessageQueueInterface {
const isMediumGroup = GroupUtils.isMediumGroup(device);
const hasSession = await SessionProtocol.hasSession(device);
// If we don't have a session then try and establish one and then continue sending messages
if (!isMediumGroup && !hasSession) {
await SessionProtocol.sendSessionRequestIfNeeded(device);
return;
}
const jobQueue = this.getJobQueue(device);

@ -41,7 +41,7 @@ export class PendingMessageCache {
message: ContentMessage
): Promise<RawMessage> {
await this.loadFromDBIfNeeded();
const rawMessage = MessageUtils.toRawMessage(device, message);
const rawMessage = await MessageUtils.toRawMessage(device, message);
// Does it exist in cache already?
if (this.find(rawMessage)) {

@ -1,5 +1,5 @@
export enum EncryptionType {
Signal,
SessionRequest,
Fallback,
MediumGroup,
}

@ -1,18 +1,30 @@
import { RawMessage } from '../types/RawMessage';
import { ContentMessage, SessionRequestMessage } from '../messages/outgoing';
import {
ContentMessage,
MediumGroupMessage,
SessionRequestMessage,
} from '../messages/outgoing';
import { EncryptionType, PubKey } from '../types';
import { SessionProtocol } from '../protocols';
export function toRawMessage(
export async function toRawMessage(
device: PubKey,
message: ContentMessage
): RawMessage {
): Promise<RawMessage> {
const timestamp = message.timestamp;
const ttl = message.ttl();
const plainTextBuffer = message.plainTextBuffer();
const encryption =
message instanceof SessionRequestMessage
? EncryptionType.SessionRequest
: EncryptionType.Signal;
let encryption: EncryptionType;
if (message instanceof MediumGroupMessage) {
encryption = EncryptionType.MediumGroup;
} else if (message instanceof SessionRequestMessage) {
encryption = EncryptionType.Fallback;
} else {
// If we don't have a session yet then send using fallback encryption until we have a session
const hasSession = await SessionProtocol.hasSession(device);
encryption = hasSession ? EncryptionType.Signal : EncryptionType.Fallback;
}
// tslint:disable-next-line: no-unnecessary-local-variable
const rawMessage: RawMessage = {

@ -66,11 +66,7 @@ describe('MessageEncrypter', () => {
Stubs.FallBackSessionCipherStub.prototype,
'encrypt'
);
await MessageEncrypter.encrypt(
'1',
data,
EncryptionType.SessionRequest
);
await MessageEncrypter.encrypt('1', data, EncryptionType.Fallback);
expect(spy.called).to.equal(
true,
'FallbackSessionCipher.encrypt should be called.'
@ -83,11 +79,7 @@ describe('MessageEncrypter', () => {
Stubs.FallBackSessionCipherStub.prototype,
'encrypt'
);
await MessageEncrypter.encrypt(
'1',
data,
EncryptionType.SessionRequest
);
await MessageEncrypter.encrypt('1', data, EncryptionType.Fallback);
const paddedData = MessageEncrypter.padPlainTextBuffer(data);
const firstArgument = new Uint8Array(spy.args[0][0]);
@ -99,7 +91,7 @@ describe('MessageEncrypter', () => {
const result = await MessageEncrypter.encrypt(
'1',
data,
EncryptionType.SessionRequest
EncryptionType.Fallback
);
expect(result.envelopeType).to.deep.equal(
SignalService.Envelope.Type.UNIDENTIFIED_SENDER
@ -144,7 +136,7 @@ describe('MessageEncrypter', () => {
describe('Sealed Sender', () => {
it('should pass the correct values to SecretSessionCipher encrypt', async () => {
const types = [EncryptionType.SessionRequest, EncryptionType.Signal];
const types = [EncryptionType.Fallback, EncryptionType.Signal];
for (const type of types) {
const spy = sandbox.spy(
Stubs.SecretSessionCipherStub.prototype,

@ -1,59 +0,0 @@
import { expect } from 'chai';
import { beforeEach } from 'mocha';
import {
DeviceUnlinkMessage,
SessionRequestMessage,
} from '../../../session/messages/outgoing';
import { SignalService } from '../../../protobuf';
import { toRawMessage } from '../../../session/utils/Messages';
import { EncryptionType, PubKey, RawMessage } from '../../../session/types';
import { TestUtils } from '../../test-utils';
import { TextEncoder } from 'util';
describe('toRawMessage', () => {
let message: DeviceUnlinkMessage;
const pubkey: PubKey = TestUtils.generateFakePubKey();
let raw: RawMessage;
beforeEach(() => {
const timestamp = Date.now();
message = new DeviceUnlinkMessage({ timestamp });
raw = toRawMessage(pubkey, message);
});
it('copied fields are set', () => {
expect(raw).to.have.property('ttl', message.ttl());
expect(raw)
.to.have.property('plainTextBuffer')
.to.be.deep.equal(message.plainTextBuffer());
expect(raw).to.have.property('timestamp', message.timestamp);
expect(raw).to.have.property('identifier', message.identifier);
expect(raw).to.have.property('device', pubkey.key);
});
it('encryption is set to SESSION_REQUEST if message is of instance SessionRequestMessage', () => {
const preKeyBundle = {
deviceId: 123456,
preKeyId: 654321,
signedKeyId: 111111,
preKey: new TextEncoder().encode('preKey'),
signature: new TextEncoder().encode('signature'),
signedKey: new TextEncoder().encode('signedKey'),
identityKey: new TextEncoder().encode('identityKey'),
};
const sessionRequest = new SessionRequestMessage({
timestamp: Date.now(),
preKeyBundle,
});
const sessionRequestRaw = toRawMessage(pubkey, sessionRequest);
expect(sessionRequestRaw).to.have.property(
'encryption',
EncryptionType.SessionRequest
);
});
it('encryption is set to Signal if message is not of instance SessionRequestMessage', () => {
expect(raw).to.have.property('encryption', EncryptionType.Signal);
});
});

@ -83,7 +83,7 @@ describe('MessageQueue', () => {
});
describe('processPending', () => {
it('will send session request message if no session', async () => {
it('will send session request if no session and not sending to medium group', async () => {
hasSessionStub.resolves(false);
isMediumGroupStub.returns(false);
@ -97,48 +97,33 @@ describe('MessageQueue', () => {
await expect(stubCallPromise).to.be.fulfilled;
});
it('will send message if session exists', async () => {
hasSessionStub.resolves(true);
isMediumGroupStub.returns(false);
sendStub.resolves();
it('will not send session request if sending to medium group', async () => {
hasSessionStub.resolves(false);
isMediumGroupStub.returns(true);
const device = TestUtils.generateFakePubKey();
await pendingMessageCache.add(device, TestUtils.generateChatMessage());
const successPromise = PromiseUtils.waitForTask(done => {
messageQueueStub.events.once('success', done);
});
await messageQueueStub.processPending(device);
await expect(successPromise).to.be.fulfilled;
expect(sendSessionRequestIfNeededStub.called).to.equal(
false,
'Session request triggered when we have a session.'
);
expect(sendSessionRequestIfNeededStub.callCount).to.equal(0);
});
it('will send message if sending to medium group', async () => {
isMediumGroupStub.returns(true);
sendStub.resolves();
it('will send messages', async () => {
for (const hasSession of [true, false]) {
hasSessionStub.resolves(hasSession);
const device = TestUtils.generateFakePubKey();
await pendingMessageCache.add(device, TestUtils.generateChatMessage());
const successPromise = PromiseUtils.waitForTask(done => {
messageQueueStub.events.once('success', done);
});
const device = TestUtils.generateFakePubKey();
await pendingMessageCache.add(device, TestUtils.generateChatMessage());
await messageQueueStub.processPending(device);
await expect(successPromise).to.be.fulfilled;
expect(sendSessionRequestIfNeededStub.called).to.equal(
false,
'Session request triggered on medium group'
);
const successPromise = PromiseUtils.waitForTask(done => {
messageQueueStub.events.once('success', done);
});
await messageQueueStub.processPending(device);
await expect(successPromise).to.be.fulfilled;
}
});
it('should remove message from cache', async () => {
hasSessionStub.resolves(true);
isMediumGroupStub.returns(false);
const events = ['success', 'fail'];
for (const event of events) {
@ -166,8 +151,6 @@ describe('MessageQueue', () => {
describe('events', () => {
it('should send a success event if message was sent', async () => {
hasSessionStub.resolves(true);
isMediumGroupStub.returns(false);
sendStub.resolves();
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
@ -188,7 +171,6 @@ describe('MessageQueue', () => {
it('should send a fail event if something went wrong while sending', async () => {
hasSessionStub.resolves(true);
isMediumGroupStub.returns(false);
sendStub.throws(new Error('failure'));
const spy = sandbox.spy();

@ -1,8 +1,10 @@
import { expect } from 'chai';
import * as sinon from 'sinon';
import * as _ from 'lodash';
import { MessageUtils } from '../../../session/utils';
import { TestUtils } from '../../../test/test-utils';
import { PendingMessageCache } from '../../../session/sending/PendingMessageCache';
import { SessionProtocol } from '../../../session/protocols';
// Equivalent to Data.StorageItem
interface StorageItem {
@ -11,6 +13,7 @@ interface StorageItem {
}
describe('PendingMessageCache', () => {
const sandbox = sinon.createSandbox();
// Initialize new stubbed cache
let data: StorageItem;
let pendingMessageCacheStub: PendingMessageCache;
@ -36,9 +39,12 @@ describe('PendingMessageCache', () => {
});
pendingMessageCacheStub = new PendingMessageCache();
sandbox.stub(SessionProtocol, 'hasSession').resolves(true);
});
afterEach(() => {
sandbox.restore();
TestUtils.restoreStubs();
});
@ -53,7 +59,7 @@ describe('PendingMessageCache', () => {
it('can add to cache', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = MessageUtils.toRawMessage(device, message);
const rawMessage = await MessageUtils.toRawMessage(device, message);
await pendingMessageCacheStub.add(device, message);
@ -86,7 +92,7 @@ describe('PendingMessageCache', () => {
it('can remove from cache', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = MessageUtils.toRawMessage(device, message);
const rawMessage = await MessageUtils.toRawMessage(device, message);
await pendingMessageCacheStub.add(device, message);
@ -105,7 +111,7 @@ describe('PendingMessageCache', () => {
it('should only remove messages with different timestamp and device', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = MessageUtils.toRawMessage(device, message);
const rawMessage = await MessageUtils.toRawMessage(device, message);
await pendingMessageCacheStub.add(device, message);
await TestUtils.timeout(5);
@ -195,7 +201,7 @@ describe('PendingMessageCache', () => {
it('can find nothing when empty', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = MessageUtils.toRawMessage(device, message);
const rawMessage = await MessageUtils.toRawMessage(device, message);
const foundMessage = pendingMessageCacheStub.find(rawMessage);
expect(foundMessage, 'a message was found in empty cache').to.be.undefined;
@ -204,7 +210,7 @@ describe('PendingMessageCache', () => {
it('can find message in cache', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = MessageUtils.toRawMessage(device, message);
const rawMessage = await MessageUtils.toRawMessage(device, message);
await pendingMessageCacheStub.add(device, message);

@ -1,7 +1,14 @@
import chai from 'chai';
import * as sinon from 'sinon';
import crypto from 'crypto';
import { TestUtils } from '../../test-utils/';
import { MessageUtils } from '../../../session/utils/';
import { PubKey } from '../../../session/types/';
import { EncryptionType, PubKey } from '../../../session/types/';
import { SessionProtocol } from '../../../session/protocols';
import {
MediumGroupChatMessage,
SessionRequestMessage,
} from '../../../session/messages/outgoing';
// tslint:disable-next-line: no-require-imports no-var-requires
const chaiAsPromised = require('chai-as-promised');
@ -10,12 +17,26 @@ chai.use(chaiAsPromised);
const { expect } = chai;
describe('Message Utils', () => {
const sandbox = sinon.createSandbox();
afterEach(() => {
sandbox.restore();
});
describe('toRawMessage', () => {
let hasSessionStub: sinon.SinonStub<[PubKey], Promise<boolean>>;
beforeEach(() => {
hasSessionStub = sandbox
.stub(SessionProtocol, 'hasSession')
.resolves(true);
});
it('can convert to raw message', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = MessageUtils.toRawMessage(device, message);
const rawMessage = await MessageUtils.toRawMessage(device, message);
expect(Object.keys(rawMessage)).to.have.length(6);
expect(rawMessage.identifier).to.exist;
@ -24,13 +45,21 @@ describe('Message Utils', () => {
expect(rawMessage.plainTextBuffer).to.exist;
expect(rawMessage.timestamp).to.exist;
expect(rawMessage.ttl).to.exist;
expect(rawMessage.identifier).to.equal(message.identifier);
expect(rawMessage.device).to.equal(device.key);
expect(rawMessage.plainTextBuffer).to.deep.equal(
message.plainTextBuffer()
);
expect(rawMessage.timestamp).to.equal(message.timestamp);
expect(rawMessage.ttl).to.equal(message.ttl());
});
it('should generate valid plainTextBuffer', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = MessageUtils.toRawMessage(device, message);
const rawMessage = await MessageUtils.toRawMessage(device, message);
const rawBuffer = rawMessage.plainTextBuffer;
const rawBufferJSON = JSON.stringify(rawBuffer);
@ -50,7 +79,7 @@ describe('Message Utils', () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = MessageUtils.toRawMessage(device, message);
const rawMessage = await MessageUtils.toRawMessage(device, message);
const derivedPubKey = PubKey.from(rawMessage.device);
expect(derivedPubKey).to.exist;
@ -59,5 +88,63 @@ describe('Message Utils', () => {
'pubkey of message was not converted correctly'
);
});
it('should set encryption to MediumGroup if a MediumGroupMessage is passed in', async () => {
hasSessionStub.resolves(true);
const device = TestUtils.generateFakePubKey();
const groupId = TestUtils.generateFakePubKey();
const chatMessage = TestUtils.generateChatMessage();
const message = new MediumGroupChatMessage({ chatMessage, groupId });
const rawMessage = await MessageUtils.toRawMessage(device, message);
expect(rawMessage.encryption).to.equal(EncryptionType.MediumGroup);
});
it('should set encryption to Fallback if a SessionRequestMessage is passed in', async () => {
hasSessionStub.resolves(true);
const device = TestUtils.generateFakePubKey();
const preKeyBundle = {
deviceId: 123456,
preKeyId: 654321,
signedKeyId: 111111,
preKey: crypto.randomBytes(16),
signature: crypto.randomBytes(16),
signedKey: crypto.randomBytes(16),
identityKey: crypto.randomBytes(16),
};
const sessionRequest = new SessionRequestMessage({
timestamp: Date.now(),
preKeyBundle,
});
const rawMessage = await MessageUtils.toRawMessage(
device,
sessionRequest
);
expect(rawMessage.encryption).to.equal(EncryptionType.Fallback);
});
it('should set encryption to Fallback on other messages if we do not have a session', async () => {
hasSessionStub.resolves(false);
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = await MessageUtils.toRawMessage(device, message);
expect(rawMessage.encryption).to.equal(EncryptionType.Fallback);
});
it('should set encryption to Signal on other messages if we have a session', async () => {
hasSessionStub.resolves(true);
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = await MessageUtils.toRawMessage(device, message);
expect(rawMessage.encryption).to.equal(EncryptionType.Signal);
});
});
});

@ -5,7 +5,7 @@ import { StringUtils } from '../../../../session/utils';
export class FallBackSessionCipherStub {
public async encrypt(buffer: ArrayBuffer): Promise<CipherTextObject> {
return {
type: SignalService.Envelope.Type.SESSION_REQUEST,
type: SignalService.Envelope.Type.FALLBACK_MESSAGE,
body: StringUtils.decode(buffer, 'binary'),
};
}

Loading…
Cancel
Save