Merge pull request #1134 from msgmaxim/group_tests

Medium groups: Part 2
pull/1156/head
Maxim Shishmarev 5 years ago committed by GitHub
commit 46275995e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2921,5 +2921,8 @@
"example": "10"
}
}
},
"useSenderKeys": {
"message": "Use Sender Keys"
}
}

@ -28,8 +28,10 @@ describe('Closed groups', function() {
await app.client.element(ConversationPage.globeButtonSection).click();
await app.client.element(ConversationPage.createClosedGroupButton).click();
const useSenderKeys = false;
// create group and add new friend
await common.addFriendToNewClosedGroup(app, app2);
await common.addFriendToNewClosedGroup([app, app2], useSenderKeys);
// send a message from app and validate it is received on app2
const textMessage = common.generateSendMessageText();

@ -20,6 +20,13 @@ chai.should();
chai.use(chaiAsPromised);
chai.config.includeStack = true;
// From https://github.com/chaijs/chai/issues/200
chai.use((_chai, _) => {
_chai.Assertion.addMethod('withMessage', msg => {
_.flag(this, 'message', msg);
});
});
const STUB_SNODE_SERVER_PORT = 3000;
const ENABLE_LOG = false;
@ -242,23 +249,8 @@ module.exports = {
);
},
async startAppsAsFriends() {
const app1Props = {
mnemonic: this.TEST_MNEMONIC1,
displayName: this.TEST_DISPLAY_NAME1,
stubSnode: true,
};
const app2Props = {
mnemonic: this.TEST_MNEMONIC2,
displayName: this.TEST_DISPLAY_NAME2,
stubSnode: true,
};
const [app1, app2] = await Promise.all([
this.startAndStub(app1Props),
this.startAndStubN(app2Props, 2),
]);
async makeFriends(app1, client2) {
const [app2, pubkey2] = client2;
/** add each other as friends */
const textMessage = this.generateSendMessageText();
@ -266,11 +258,7 @@ module.exports = {
await app1.client.element(ConversationPage.contactsButtonSection).click();
await app1.client.element(ConversationPage.addContactButton).click();
await this.setValueWrapper(
app1,
ConversationPage.sessionIDInput,
this.TEST_PUBKEY2
);
await this.setValueWrapper(app1, ConversationPage.sessionIDInput, pubkey2);
await app1.client.element(ConversationPage.nextButton).click();
await app1.client.waitForExist(
ConversationPage.sendFriendRequestTextarea,
@ -317,33 +305,70 @@ module.exports = {
ConversationPage.acceptedFriendRequestMessage,
5000
);
},
async startAppsAsFriends() {
const app1Props = {
mnemonic: this.TEST_MNEMONIC1,
displayName: this.TEST_DISPLAY_NAME1,
stubSnode: true,
};
const app2Props = {
mnemonic: this.TEST_MNEMONIC2,
displayName: this.TEST_DISPLAY_NAME2,
stubSnode: true,
};
const [app1, app2] = await Promise.all([
this.startAndStub(app1Props),
this.startAndStubN(app2Props, 2),
]);
await this.makeFriends(app1, [app2, this.TEST_PUBKEY2]);
return [app1, app2];
},
async addFriendToNewClosedGroup(app, app2) {
async addFriendToNewClosedGroup(members, useSenderKeys) {
const [app, ...others] = members;
await this.setValueWrapper(
app,
ConversationPage.closedGroupNameTextarea,
this.VALID_CLOSED_GROUP_NAME1
);
await app.client
.element(ConversationPage.closedGroupNameTextarea)
.getValue()
.should.eventually.equal(this.VALID_CLOSED_GROUP_NAME1);
await app.client
.element(ConversationPage.createClosedGroupMemberItem)
.isVisible().should.eventually.be.true;
// This assumes that app does not have any other friends
for (let i = 0; i < others.length; i += 1) {
// eslint-disable-next-line no-await-in-loop
await app.client
.element(ConversationPage.createClosedGroupMemberItem(i))
.isVisible().should.eventually.be.true;
// eslint-disable-next-line no-await-in-loop
await app.client
.element(ConversationPage.createClosedGroupMemberItem(i))
.click();
}
// select the first friend as a member of the groups being created
await app.client
.element(ConversationPage.createClosedGroupMemberItem)
.click();
await app.client
.element(ConversationPage.createClosedGroupMemberItemSelected)
.isVisible().should.eventually.be.true;
if (useSenderKeys) {
// Select Sender Keys
await app.client
.element(ConversationPage.createClosedGroupSealedSenderToggle)
.click();
}
// trigger the creation of the group
await app.client
.element(ConversationPage.validateCreationClosedGroupButton)
@ -356,8 +381,9 @@ module.exports = {
await app.client.isExisting(
ConversationPage.headerTitleGroupName(this.VALID_CLOSED_GROUP_NAME1)
).should.eventually.be.true;
await app.client.element(ConversationPage.headerTitleMembers(2)).isVisible()
.should.eventually.be.true;
await app.client
.element(ConversationPage.headerTitleMembers(members.length))
.isVisible().should.eventually.be.true;
// validate overlay is closed
await app.client
@ -376,25 +402,29 @@ module.exports = {
)
).should.eventually.be.true;
// next check app2 has been invited and has the group in its conversations
await app2.client.waitForExist(
ConversationPage.rowOpenGroupConversationName(
this.VALID_CLOSED_GROUP_NAME1
),
6000
await Promise.all(
others.map(async otherApp => {
// next check that other members have been invited and have the group in their conversations
await otherApp.client.waitForExist(
ConversationPage.rowOpenGroupConversationName(
this.VALID_CLOSED_GROUP_NAME1
),
6000
);
// open the closed group conversation on otherApp
await otherApp.client
.element(ConversationPage.conversationButtonSection)
.click();
await this.timeout(500);
await otherApp.client
.element(
ConversationPage.rowOpenGroupConversationName(
this.VALID_CLOSED_GROUP_NAME1
)
)
.click();
})
);
// open the closed group conversation on app2
await app2.client
.element(ConversationPage.conversationButtonSection)
.click();
await this.timeout(500);
await app2.client
.element(
ConversationPage.rowOpenGroupConversationName(
this.VALID_CLOSED_GROUP_NAME1
)
)
.click();
},
async linkApp2ToApp(app1, app2) {
@ -543,6 +573,10 @@ module.exports = {
app1.webContents.executeJavaScript(
'window.LokiMessageAPI = window.StubMessageAPI;'
);
app1.webContents.executeJavaScript(
'window.LokiSnodeAPI = window.StubLokiSnodeAPI;'
);
},
logsContainsString: async (app1, str) => {
@ -557,35 +591,45 @@ module.exports = {
const { query } = url.parse(request.url, true);
const { pubkey, data, timestamp } = query;
if (pubkey) {
if (request.method === 'POST') {
if (ENABLE_LOG) {
console.warn('POST', [data, timestamp]);
}
if (!pubkey) {
console.warn('NO PUBKEY');
response.writeHead(400, { 'Content-Type': 'text/html' });
response.end();
}
let ori = this.messages[pubkey];
if (!this.messages[pubkey]) {
ori = [];
}
if (request.method === 'POST') {
if (ENABLE_LOG) {
console.warn(
'POST',
pubkey.substr(2, 3),
data.substr(4, 10),
timestamp
);
}
this.messages[pubkey] = [...ori, { data, timestamp }];
let ori = this.messages[pubkey];
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end();
} else {
const retrievedMessages = { messages: this.messages[pubkey] };
if (ENABLE_LOG) {
console.warn('GET', pubkey, retrievedMessages);
}
if (this.messages[pubkey]) {
response.writeHead(200, { 'Content-Type': 'application/json' });
response.write(JSON.stringify(retrievedMessages));
this.messages[pubkey] = [];
}
response.end();
if (!this.messages[pubkey]) {
ori = [];
}
this.messages[pubkey] = [...ori, { data, timestamp }];
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end();
} else {
const retrievedMessages = { messages: this.messages[pubkey] || [] };
if (ENABLE_LOG) {
const messages = retrievedMessages.messages.map(m =>
m.data.substr(4, 10)
);
console.warn('GET', pubkey.substr(2, 3), messages);
}
response.writeHead(200, { 'Content-Type': 'application/json' });
response.write(JSON.stringify(retrievedMessages));
response.end();
}
response.end();
});
this.stubSnode.listen(STUB_SNODE_SERVER_PORT);
} else {

@ -13,6 +13,7 @@ require('./link_device_test');
require('./closed_group_test');
require('./message_functions_test');
require('./settings_test');
require('./sender_keys_test');
before(async () => {
// start the app once before all tests to get the platform-dependent

@ -32,7 +32,7 @@ describe('Message Functions', function() {
await app.client.element(ConversationPage.createClosedGroupButton).click();
// create group and add new friend
await common.addFriendToNewClosedGroup(app, app2);
await common.addFriendToNewClosedGroup([app, app2]);
// send attachment from app1 to closed group
const fileLocation = path.join(__dirname, 'test_attachment');

@ -14,6 +14,8 @@ module.exports = {
inputWithId: id => `//input[contains(@id, '${id}')]`,
textAreaWithPlaceholder: placeholder =>
`//textarea[contains(@placeholder, "${placeholder}")]`,
textAreaWithClass: classname =>
`//textarea[contains(@class, "${classname}")]`,
byId: id => `//*[@id="${id}"]`,
divWithClass: classname => `//div[contains(@class, "${classname}")]`,
divWithClassAndText: (classname, text) =>

@ -4,7 +4,7 @@ module.exports = {
// conversation view
sessionLoader: commonPage.divWithClass('session-loader'),
leftPaneOverlay: commonPage.divWithClass('module-left-pane-overlay'),
sendMessageTextarea: commonPage.textAreaWithPlaceholder('Type your message'),
sendMessageTextarea: commonPage.textAreaWithClass('send-message'),
sendFriendRequestTextarea: commonPage.textAreaWithPlaceholder(
'Send your first message'
),
@ -63,7 +63,11 @@ module.exports = {
closedGroupNameTextarea: commonPage.textAreaWithPlaceholder(
'Enter a group name'
),
createClosedGroupMemberItem: commonPage.divWithClass('session-member-item'),
createClosedGroupMemberItem: idx =>
commonPage.divWithClass(`session-member-item-${idx}`),
createClosedGroupSealedSenderToggle: commonPage.divWithClass(
'session-toggle'
),
createClosedGroupMemberItemSelected: commonPage.divWithClass(
'session-member-item selected'
),

@ -0,0 +1,159 @@
/* eslint-disable func-names */
/* eslint-disable import/no-extraneous-dependencies */
const { afterEach, beforeEach, describe, it } = require('mocha');
const common = require('./common');
const ConversationPage = require('./page-objects/conversation.page');
async function generateAndSendMessage(app) {
// send a message from app and validate it is received on app2
const textMessage = common.generateSendMessageText();
await app.client
.element(ConversationPage.sendMessageTextarea)
.setValue(textMessage);
await app.client
.element(ConversationPage.sendMessageTextarea)
.getValue()
.should.eventually.equal(textMessage);
// send the message
await app.client.keys('Enter');
// validate that the message has been added to the message list view
await app.client.waitForExist(
ConversationPage.existingSendMessageText(textMessage),
2000
);
return textMessage;
}
async function makeFriendsPlusMessage(app, [app2, pubkey]) {
await common.makeFriends(app, [app2, pubkey]);
// Send something back so that `app` can see our name
const text = await generateAndSendMessage(app2);
await app.client.waitForExist(
ConversationPage.existingReceivedMessageText(text),
8000
);
// Click away so we can call this function again
await app.client.element(ConversationPage.globeButtonSection).click();
}
async function testTwoMembers() {
const [app, app2] = await common.startAppsAsFriends();
await app.client.element(ConversationPage.globeButtonSection).click();
await app.client.element(ConversationPage.createClosedGroupButton).click();
const useSenderKeys = true;
// create group and add new friend
await common.addFriendToNewClosedGroup([app, app2], useSenderKeys);
const text1 = await generateAndSendMessage(app);
// validate that the message has been added to the message list view
await app2.client.waitForExist(
ConversationPage.existingReceivedMessageText(text1),
5000
);
// Send a message back:
const text2 = await generateAndSendMessage(app2);
// TODO: fix this. We can send messages back manually, not sure
// why this test fails
await app.client.waitForExist(
ConversationPage.existingReceivedMessageText(text2),
10000
);
}
async function testThreeMembers() {
// 1. Make three clients A, B, C
const app1Props = {
mnemonic: common.TEST_MNEMONIC1,
displayName: common.TEST_DISPLAY_NAME1,
stubSnode: true,
};
const app2Props = {
mnemonic: common.TEST_MNEMONIC2,
displayName: common.TEST_DISPLAY_NAME2,
stubSnode: true,
};
const app3Props = {
mnemonic: common.TEST_MNEMONIC3,
displayName: common.TEST_DISPLAY_NAME3,
stubSnode: true,
};
const [app1, app2, app3] = await Promise.all([
common.startAndStub(app1Props),
common.startAndStubN(app2Props, 2),
common.startAndStubN(app3Props, 3),
]);
// 2. Make A friends with B and C (B and C are not friends)
await makeFriendsPlusMessage(app1, [app2, common.TEST_PUBKEY2]);
await makeFriendsPlusMessage(app1, [app3, common.TEST_PUBKEY3]);
const useSenderKeys = true;
await app1.client.element(ConversationPage.globeButtonSection).click();
await app1.client.element(ConversationPage.createClosedGroupButton).click();
// 3. Add all three to the group
await common.addFriendToNewClosedGroup([app1, app2, app3], useSenderKeys);
// 4. Test that all members can see the message from app1
const text1 = await generateAndSendMessage(app1);
await app2.client.waitForExist(
ConversationPage.existingReceivedMessageText(text1),
5000
);
await app3.client.waitForExist(
ConversationPage.existingReceivedMessageText(text1),
5000
);
// TODO: test that B and C can send messages to the group
// const text2 = await generateAndSendMessage(app3);
// await app2.client.waitForExist(
// ConversationPage.existingReceivedMessageText(text2),
// 5000
// );
}
describe('senderkeys', function() {
let app;
this.timeout(600000);
this.slow(40000);
beforeEach(async () => {
await common.killallElectron();
await common.stopStubSnodeServer();
});
afterEach(async () => {
await common.stopApp(app);
await common.killallElectron();
await common.stopStubSnodeServer();
});
it('Two member group', testTwoMembers);
it('Three member group: test session requests', testThreeMembers);
});

@ -0,0 +1,10 @@
/* global log */
class StubLokiSnodeAPI {
// eslint-disable-next-line class-methods-use-this
async refreshSwarmNodesForPubKey(pubKey) {
log.info('refreshSwarmNodesForPubkey: ', pubKey);
}
}
module.exports = StubLokiSnodeAPI;

@ -1,4 +1,4 @@
/* global clearTimeout, dcodeIO, Buffer, TextDecoder, process */
/* global clearTimeout, dcodeIO, Buffer, TextDecoder, process, log */
const nodeFetch = require('node-fetch');
class StubMessageAPI {
@ -26,6 +26,35 @@ class StubMessageAPI {
);
}
async pollForGroupId(groupId, onMessages) {
const get = {
method: 'GET',
};
const res = await nodeFetch(
`${this.baseUrl}/messages?pubkey=${groupId}`,
get
);
try {
const json = await res.json();
const modifiedMessages = json.messages.map(m => {
// eslint-disable-next-line no-param-reassign
m.conversationId = groupId;
return m;
});
onMessages(modifiedMessages || []);
} catch (e) {
log.error('invalid json for GROUP', e);
onMessages([]);
}
setTimeout(() => {
this.pollForGroupId(groupId, onMessages);
}, 1000);
}
async startLongPolling(numConnections, stopPolling, callback) {
const ourPubkey = this.ourKey;
@ -36,10 +65,15 @@ class StubMessageAPI {
`${this.baseUrl}/messages?pubkey=${ourPubkey}`,
get
);
const json = await res.json();
// console.warn('STUBBED polling messages ', json.messages);
callback(json.messages || []);
try {
const json = await res.json();
callback(json.messages || []);
} catch (e) {
log.error('invalid json: ', e);
callback([]);
}
// console.warn('STUBBED polling messages ', json.messages);
}
}

@ -638,23 +638,21 @@
window.doUpdateGroup = async (groupId, groupName, members, avatar) => {
const ourKey = textsecure.storage.user.getNumber();
const ev = new Event('message');
ev.confirm = () => {};
ev.data = {
source: ourKey,
timestamp: Date.now(),
message: {
group: {
id: groupId,
type: textsecure.protobuf.GroupContext.Type.UPDATE,
name: groupName,
members,
avatar: null, // TODO
},
const ev = {
groupDetails: {
id: groupId,
name: groupName,
members,
active: true,
expireTimer: 0,
avatar: '',
is_medium_group: true,
},
confirm: () => {},
};
await onGroupReceived(ev);
const convo = await ConversationController.getOrCreateAndWait(
groupId,
'group'
@ -719,15 +717,42 @@
const recipients = _.union(convo.get('members'), members);
await onMessageReceived(ev);
convo.updateGroup({
groupId,
groupName,
const isMediumGroup = convo.isMediumGroup();
const updateObj = {
id: groupId,
name: groupName,
avatar: nullAvatar,
recipients,
members,
is_medium_group: isMediumGroup,
options,
});
};
// Send own sender keys and group secret key
if (isMediumGroup) {
const { chainKey, keyIdx } = await window.SenderKeyAPI.getSenderKeys(
groupId,
ourKey
);
updateObj.senderKey = {
chainKey: StringView.arrayBufferToHex(chainKey),
keyIdx,
};
const groupIdentity = await window.Signal.Data.getIdentityKeyById(
groupId
);
const secretKeyHex = StringView.hexToArrayBuffer(
groupIdentity.secretKey
);
updateObj.secretKey = secretKeyHex;
}
convo.updateGroup(updateObj);
};
window.createMediumSizeGroup = async (groupName, members) => {
@ -746,66 +771,73 @@
identityKeys.privKey
);
// Constructing a "create group" message
const proto = new textsecure.protobuf.DataMessage();
const groupUpdate = new textsecure.protobuf.MediumGroupUpdate();
const primary = window.storage.get('primaryDevicePubKey');
groupUpdate.groupId = groupId;
groupUpdate.groupSecretKey = groupSecretKeyHex;
groupUpdate.senderKey = senderKey;
groupUpdate.members = [ourIdentity, ...members];
groupUpdate.groupName = groupName;
proto.mediumGroupUpdate = groupUpdate;
const allMembers = [primary, ...members];
await window.Signal.Data.createOrUpdateIdentityKey({
id: groupId,
secretKey: groupSecretKeyHex,
});
const convo = await window.ConversationController.getOrCreateAndWait(
const ev = {
groupDetails: {
id: groupId,
name: groupName,
members: allMembers,
recipients: allMembers,
active: true,
expireTimer: 0,
avatar: '',
secretKey: identityKeys.privKey,
senderKey,
is_medium_group: true,
},
confirm: () => {},
};
await onGroupReceived(ev);
const convo = await ConversationController.getOrCreateAndWait(
groupId,
Message.GROUP
'group'
);
convo.set('is_medium_group', true);
convo.set('active_at', Date.now());
convo.set('name', groupName);
convo.updateGroupAdmins([primary]);
convo.updateGroup(ev.groupDetails);
convo.setFriendRequestStatus(
window.friends.friendRequestStatusEnum.friends
);
appView.openConversation(groupId, {});
// Subscribe to this group id
messageReceiver.pollForAdditionalId(groupId);
// TODO: include ourselves so that our lined devices work as well!
await textsecure.messaging.updateMediumGroup(members, proto);
};
window.doCreateGroup = async (groupName, members) => {
const keypair = await libsignal.KeyHelper.generateIdentityKeyPair();
const groupId = StringView.arrayBufferToHex(keypair.pubKey);
const ev = new Event('group');
const primaryDeviceKey =
window.storage.get('primaryDevicePubKey') ||
textsecure.storage.user.getNumber();
const allMembers = [primaryDeviceKey, ...members];
ev.groupDetails = {
id: groupId,
name: groupName,
members: allMembers,
recipients: allMembers,
active: true,
expireTimer: 0,
avatar: '',
const ev = {
groupDetails: {
id: groupId,
name: groupName,
members: allMembers,
recipients: allMembers,
active: true,
expireTimer: 0,
avatar: '',
},
confirm: () => {},
};
ev.confirm = () => {};
await onGroupReceived(ev);
const convo = await ConversationController.getOrCreateAndWait(
@ -1899,6 +1931,7 @@
members: details.members,
color: details.color,
type: 'group',
is_medium_group: details.is_medium_group || false,
};
if (details.active) {

@ -14,7 +14,8 @@
clipboard,
BlockedNumberController,
lokiPublicChatAPI,
JobQueue
JobQueue,
StringView
*/
/* eslint-disable more/no-then */
@ -235,6 +236,9 @@
isBlocked() {
return BlockedNumberController.isBlocked(this.id);
},
isMediumGroup() {
return this.get('is_medium_group');
},
block() {
BlockedNumberController.block(this.id);
this.trigger('change');
@ -1781,7 +1785,7 @@
let dest = destination;
let numbers = groupNumbers;
if (this.get('is_medium_group')) {
if (this.isMediumGroup()) {
dest = this.id;
numbers = [destination];
options.isMediumGroup = true;
@ -2271,6 +2275,12 @@
}
},
async saveChangesToDB() {
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
},
async updateGroup(providedGroupUpdate) {
let groupUpdate = providedGroupUpdate;
@ -2289,15 +2299,44 @@
group_update: groupUpdate,
});
const id = await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message,
});
message.set({ id });
const messageId = await window.Signal.Data.saveMessage(
message.attributes,
{
Message: Whisper.Message,
}
);
message.set({ id: messageId });
const options = this.getSendOptions();
if (groupUpdate.is_medium_group) {
// Constructing a "create group" message
const proto = new textsecure.protobuf.DataMessage();
const mgUpdate = new textsecure.protobuf.MediumGroupUpdate();
const { id, name, secretKey, senderKey, members } = groupUpdate;
mgUpdate.type = textsecure.protobuf.MediumGroupUpdate.Type.NEW_GROUP;
mgUpdate.groupId = id;
mgUpdate.groupSecretKey = secretKey;
mgUpdate.senderKey = new textsecure.protobuf.SenderKey(senderKey);
mgUpdate.members = members.map(pkHex =>
StringView.hexToArrayBuffer(pkHex)
);
mgUpdate.groupName = name;
mgUpdate.admins = this.get('groupAdmins');
proto.mediumGroupUpdate = mgUpdate;
message.send(
this.wrapSend(textsecure.messaging.updateMediumGroup(members, proto))
);
return;
}
message.send(
this.wrapSend(
textsecure.messaging.updateGroup(
textsecure.messaging.sendGroupUpdate(
this.id,
this.get('name'),
this.get('avatar'),
@ -2313,7 +2352,7 @@
sendGroupInfo(recipients) {
if (this.isClosedGroup()) {
const options = this.getSendOptions();
textsecure.messaging.updateGroup(
textsecure.messaging.sendGroupUpdate(
this.id,
this.get('name'),
this.get('avatar'),
@ -2327,6 +2366,15 @@
async leaveGroup() {
const now = Date.now();
if (this.isMediumGroup()) {
// NOTE: we should probably remove sender keys for groupId,
// and its secret key, but it is low priority
// TODO: need to reset everyone's sender keys
window.lokiMessageAPI.stopPollingForGroup(this.id);
}
if (this.get('type') === 'group') {
const groupNumbers = this.getRecipients();
this.set({ left: true });

@ -1477,7 +1477,8 @@
if (!this.isFriendRequest()) {
const c = this.getConversation();
// Don't bother sending sync messages to public chats
if (c && !c.isPublic()) {
// or groups with sender keys
if (c && !c.isPublic() && !c.isMediumGroup()) {
this.sendSyncMessage();
}
}

@ -74,6 +74,8 @@ class LokiMessageAPI {
this.jobQueue = new window.JobQueue();
this.sendingData = {};
this.ourKey = ourKey;
// stop polling for a group if its id is no longer found here
this.groupIdsToPoll = {};
}
async sendMessage(pubKey, data, messageTimeStamp, ttl, options = {}) {
@ -325,7 +327,7 @@ class LokiMessageAPI {
);
// eslint-disable-next-line no-constant-condition
while (true) {
while (this.groupIdsToPoll[groupId]) {
try {
let messages = await _retrieveNextMessages(node, groupId);
@ -384,6 +386,13 @@ class LokiMessageAPI {
async pollForGroupId(groupId, onMessages) {
log.info(`Starting to poll for group id: ${groupId}`);
if (this.groupIdsToPoll[groupId]) {
log.warn(`Already polling for group id: ${groupId}`);
return;
}
this.groupIdsToPoll[groupId] = true;
// Get nodes for groupId
const nodes = await lokiSnodeAPI.refreshSwarmNodesForPubKey(groupId);
@ -394,6 +403,16 @@ class LokiMessageAPI {
);
}
async stopPollingForGroup(groupId) {
if (!this.groupIdsToPoll[groupId]) {
log.warn(`Already not polling for group id: ${groupId}`);
return;
}
log.warn(`Stop polling for group id: ${groupId}`);
delete this.groupIdsToPoll[groupId];
}
async _openRetrieveConnection(pSwarmPool, stopPollingPromise, onMessages) {
const swarmPool = pSwarmPool; // lint
let stopPollingResult = false;
@ -515,9 +534,7 @@ class LokiMessageAPI {
// Start polling for medium size groups as well (they might be in different swarms)
{
const convos = window
.getConversations()
.filter(c => c.get('is_medium_group'));
const convos = window.getConversations().filter(c => c.isMediumGroup());
const self = this;

@ -5,7 +5,8 @@
dcodeIO,
libloki,
log,
crypto
crypto,
textsecure
*/
/* eslint-disable more/no-then */
@ -39,9 +40,7 @@ async function saveSenderKeysInner(
}
// Save somebody else's key
async function saveSenderKeys(groupId, senderIdentity, chainKey) {
// New key, so index 0
const keyIdx = 0;
async function saveSenderKeys(groupId, senderIdentity, chainKey, keyIdx) {
const messageKeys = {};
await saveSenderKeysInner(
groupId,
@ -133,7 +132,7 @@ async function advanceRatchet(groupId, senderIdentity, idx) {
log.error(
`Could not find ratchet for groupId ${groupId} sender: ${senderIdentity}`
);
return null;
throw new textsecure.SenderKeyMissing(senderIdentity);
}
// Normally keyIdx will be 1 behind, in which case we stepRatchet one time only
@ -178,7 +177,10 @@ async function advanceRatchet(groupId, senderIdentity, idx) {
curMessageKey = messageKey;
break;
} else if (nextKeyIdx > idx) {
log.error('Developer error: nextKeyIdx > idx');
log.error(
`Could not decrypt for an older ratchet step: (${nextKeyIdx})nextKeyIdx > (${idx})idx`
);
throw new Error(`Cannot revert ratchet for group ${groupId}!`);
} else {
// Store keys for skipped nextKeyIdx, we might need them to decrypt
// messages that arrive out-of-order
@ -289,9 +291,16 @@ async function encryptWithSenderKeyInner(plaintext, groupId, ourIdentity) {
return { ciphertext, keyIdx };
}
async function getSenderKeys(groupId, senderIdentity) {
const { chainKey, keyIdx } = await loadChainKey(groupId, senderIdentity);
return { chainKey, keyIdx };
}
module.exports = {
createSenderKeyForGroup,
encryptWithSenderKey,
decryptWithSenderKey,
saveSenderKeys,
getSenderKeys,
};

@ -21,6 +21,7 @@
const debugFlags = DebugFlagsEnum.ALL;
const debugLogFn = (...args) => {
// eslint-disable-next-line no-constant-condition
if (true) {
// process.env.NODE_ENV.includes('test-integration') ||
window.console.warn(...args);
@ -220,6 +221,7 @@
});
return syncMessage;
}
function createGroupSyncProtoMessage(sessionGroup) {
// We are getting a single open group here

@ -263,6 +263,19 @@
}
}
function SenderKeyMissing(senderIdentity) {
this.name = 'SenderKeyMissing';
this.senderIdentity = senderIdentity;
Error.call(this, this.name);
// Maintains proper stack trace, where our error was thrown (only available on V8)
// via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
if (Error.captureStackTrace) {
Error.captureStackTrace(this);
}
}
window.textsecure.UnregisteredUserError = UnregisteredUserError;
window.textsecure.SendMessageNetworkError = SendMessageNetworkError;
window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError;
@ -282,4 +295,5 @@
window.textsecure.TimestampError = TimestampError;
window.textsecure.PublicChatError = PublicChatError;
window.textsecure.PublicTokenError = PublicTokenError;
window.textsecure.SenderKeyMissing = SenderKeyMissing;
})();

@ -682,7 +682,7 @@ MessageReceiver.prototype.extend({
const { senderIdentity } = envelope;
const {
ciphertext: ciphertext2,
ciphertext: outerCiphertext,
ephemeralKey,
} = textsecure.protobuf.MediumGroupContent.decode(ciphertextObj);
@ -692,16 +692,16 @@ MessageReceiver.prototype.extend({
'hex'
).toArrayBuffer();
const res = await libloki.crypto.decryptForPubkey(
const mediumGroupCiphertext = await libloki.crypto.decryptForPubkey(
secretKey,
ephemKey,
ciphertext2.toArrayBuffer()
outerCiphertext.toArrayBuffer()
);
const {
ciphertext,
keyIdx,
} = textsecure.protobuf.MediumGroupCiphertext.decode(res);
} = textsecure.protobuf.MediumGroupCiphertext.decode(mediumGroupCiphertext);
const plaintext = await window.SenderKeyAPI.decryptWithSenderKey(
ciphertext.toArrayBuffer(),
@ -849,6 +849,22 @@ MessageReceiver.prototype.extend({
return promise
.then(plaintext => this.postDecrypt(envelope, plaintext))
.catch(error => {
if (error && error instanceof textsecure.SenderKeyMissing) {
const groupId = envelope.source;
const { senderIdentity } = error;
log.info(
'Requesting missing key for identity: ',
senderIdentity,
'groupId: ',
groupId
);
textsecure.messaging.requestSenderKeys(senderIdentity, groupId);
return;
}
let errorToThrow = error;
const noSession =
@ -876,7 +892,7 @@ MessageReceiver.prototype.extend({
ev.confirm = this.removeFromCache.bind(this, envelope);
const returnError = () => Promise.reject(errorToThrow);
return this.dispatchAndWait(ev).then(returnError, returnError);
this.dispatchAndWait(ev).then(returnError, returnError);
});
},
async decryptPreKeyWhisperMessage(ciphertext, sessionCipher, address) {
@ -900,7 +916,7 @@ MessageReceiver.prototype.extend({
},
// handle a SYNC message for a message
// sent by another device
handleSentMessage(envelope, sentContainer, msg) {
async handleSentMessage(envelope, sentContainer, msg) {
const {
destination,
timestamp,
@ -908,41 +924,63 @@ MessageReceiver.prototype.extend({
unidentifiedStatus,
} = sentContainer;
let p = Promise.resolve();
// eslint-disable-next-line no-bitwise
if (msg.flags & textsecure.protobuf.DataMessage.Flags.END_SESSION) {
p = this.handleEndSession(destination);
await this.handleEndSession(destination);
}
return p.then(() =>
this.processDecrypted(envelope, msg).then(message => {
const primaryDevicePubKey = window.storage.get('primaryDevicePubKey');
// handle profileKey and avatar updates
if (envelope.source === primaryDevicePubKey) {
const { profileKey, profile } = message;
const primaryConversation = ConversationController.get(
primaryDevicePubKey
);
if (profile) {
this.updateProfile(primaryConversation, profile, profileKey);
}
}
if (msg.mediumGroupUpdate) {
await this.handleMediumGroupUpdate(envelope, msg.mediumGroupUpdate);
return;
}
const ev = new Event('sent');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
destination,
timestamp: timestamp.toNumber(),
device: envelope.sourceDevice,
unidentifiedStatus,
message,
};
if (expirationStartTimestamp) {
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
}
return this.dispatchAndWait(ev);
})
const message = await this.processDecrypted(envelope, msg);
const groupId = message.group && message.group.id;
const isBlocked = this.isGroupBlocked(groupId);
const primaryDevicePubKey = window.storage.get('primaryDevicePubKey');
const isMe =
envelope.source === textsecure.storage.user.getNumber() ||
envelope.source === primaryDevicePubKey;
const isLeavingGroup = Boolean(
message.group &&
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
);
if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
window.log.warn(
`Message ${this.getEnvelopeId(
envelope
)} ignored; destined for blocked group`
);
this.removeFromCache(envelope);
return;
}
// handle profileKey and avatar updates
if (envelope.source === primaryDevicePubKey) {
const { profileKey, profile } = message;
const primaryConversation = ConversationController.get(
primaryDevicePubKey
);
if (profile) {
this.updateProfile(primaryConversation, profile, profileKey);
}
}
const ev = new Event('sent');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
destination,
timestamp: timestamp.toNumber(),
device: envelope.sourceDevice,
unidentifiedStatus,
message,
};
if (expirationStartTimestamp) {
ev.data.expirationStartTimestamp = expirationStartTimestamp.toNumber();
}
this.dispatchAndWait(ev);
},
async handleLokiAddressMessage(envelope) {
window.log.warn('Ignoring a Loki address message');
@ -1163,96 +1201,180 @@ MessageReceiver.prototype.extend({
},
async handleMediumGroupUpdate(envelope, groupUpdate) {
const {
groupId,
groupSecretKey,
senderKey,
members,
groupName,
} = groupUpdate;
const { type, groupId } = groupUpdate;
const convoExists = window.ConversationController.get(groupId, 'group');
const ourIdentity = await textsecure.storage.user.getNumber();
const senderIdentity = envelope.source;
if (convoExists) {
// If the group already exists, check that `members` is empty,
// and if so, it is sender key message
if (
type === textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY_REQUEST
) {
log.debug('[sender key] sender key request from:', senderIdentity);
// TODO: introduce TYPE into this message instead?
if (!members || !members.length) {
log.info('[sender key] got a new sender key from:', envelope.source);
const proto = new textsecure.protobuf.DataMessage();
// We probably don't need to await here
await window.SenderKeyAPI.saveSenderKeys(
groupId,
envelope.source,
senderKey
);
// We reuse the same message type for sender keys
const update = new textsecure.protobuf.MediumGroupUpdate();
const { chainKey, keyIdx } = await window.SenderKeyAPI.getSenderKeys(
groupId,
ourIdentity
);
update.type = textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY;
update.groupId = groupId;
update.senderKey = new textsecure.protobuf.SenderKey({
chainKey: StringView.arrayBufferToHex(chainKey),
keyIdx,
});
proto.mediumGroupUpdate = update;
textsecure.messaging.updateMediumGroup([senderIdentity], proto);
this.removeFromCache(envelope);
} else if (type === textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY) {
const { senderKey } = groupUpdate;
log.debug('[sender key] got a new sender key from:', senderIdentity);
await window.SenderKeyAPI.saveSenderKeys(
groupId,
senderIdentity,
senderKey.chainKey,
senderKey.keyIdx
);
this.removeFromCache(envelope);
return;
this.removeFromCache(envelope);
} else if (type === textsecure.protobuf.MediumGroupUpdate.Type.NEW_GROUP) {
const maybeConvo = await window.ConversationController.get(groupId);
const groupExists = !!maybeConvo;
const {
members: membersBinary,
groupSecretKey,
groupName,
senderKey,
admins,
} = groupUpdate;
const members = membersBinary.map(pk =>
StringView.arrayBufferToHex(pk.toArrayBuffer())
);
const convo = groupExists
? maybeConvo
: await window.ConversationController.getOrCreateAndWait(
groupId,
'group'
);
{
// Add group update message
const now = Date.now();
const message = convo.messageCollection.add({
conversationId: convo.id,
type: 'incoming',
sent_at: now,
received_at: now,
group_update: {
name: groupName,
members,
},
});
const messageId = await window.Signal.Data.saveMessage(
message.attributes,
{
Message: Whisper.Message,
}
);
message.set({ id: messageId });
}
log.error(`Conversation for groupId ${groupId} already exists`);
}
if (groupExists) {
// ***** Updating the group *****
log.info('Received a group update for medium group:', groupId);
const convo = await window.ConversationController.getOrCreateAndWait(
groupId,
'group'
);
convo.set('is_medium_group', true);
convo.set('active_at', Date.now());
convo.set('name', groupName);
// Check that the sender is admin (make sure it words with multidevice)
const isAdmin = convo.get('groupAdmins').includes(senderIdentity);
await window.Signal.Data.createOrUpdateIdentityKey({
id: groupId,
secretKey: groupSecretKey,
});
if (!isAdmin) {
log.warn('Rejected attempt to update a group by non-admin');
this.removeFromCache(envelope);
return;
}
// Save sender's key
await window.SenderKeyAPI.saveSenderKeys(
groupId,
envelope.source,
senderKey
);
convo.set('name', groupName);
convo.set('members', members);
// TODO: Check that we are even a part of this group?
const ourIdentity = await textsecure.storage.user.getNumber();
// TODO: check that we are still in the group (when we enable deleting members)
convo.saveChangesToDB();
const ownSenderKey = await window.SenderKeyAPI.createSenderKeyForGroup(
groupId,
ourIdentity
);
// Update other fields. Add a corresponding "update" message to the conversation
} else {
// ***** Creating a new group *****
log.info('Received a new medium group:', groupId);
{
// TODO: Send own key to every member
// TODO: Check that we are even a part of this group?
const otherMembers = _.without(members, ourIdentity);
convo.set('is_medium_group', true);
convo.set('active_at', Date.now());
convo.set('name', groupName);
convo.set('groupAdmins', admins);
const proto = new textsecure.protobuf.DataMessage();
convo.setFriendRequestStatus(
window.friends.friendRequestStatusEnum.friends
);
// We reuse the same message type for sender keys
const update = new textsecure.protobuf.MediumGroupUpdate();
update.groupId = groupId;
update.senderKey = ownSenderKey;
const secretKeyHex = StringView.arrayBufferToHex(
groupSecretKey.toArrayBuffer()
);
proto.mediumGroupUpdate = update;
await window.Signal.Data.createOrUpdateIdentityKey({
id: groupId,
secretKey: secretKeyHex,
});
// TODO: send to our linked devices too?
// Save sender's key
await window.SenderKeyAPI.saveSenderKeys(
groupId,
envelope.source,
senderKey.chainKey,
senderKey.keyIdx
);
// Don't need to await here
const ownSenderKey = await window.SenderKeyAPI.createSenderKeyForGroup(
groupId,
ourIdentity
);
// TODO: Some of the members might not have a session with us, so
// we should send a session request
{
// Send own key to every member
const otherMembers = _.without(members, ourIdentity);
textsecure.messaging.updateMediumGroup(otherMembers, proto);
}
const proto = new textsecure.protobuf.DataMessage();
// Subscribe to this group
this.pollForAdditionalId(groupId);
// We reuse the same message type for sender keys
const update = new textsecure.protobuf.MediumGroupUpdate();
update.type = textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY;
update.groupId = groupId;
update.senderKey = new textsecure.protobuf.SenderKey({
chainKey: ownSenderKey,
keyIdx: 0,
});
// All further messages (maybe rather than 'control' messages) should come to this group's swarm
proto.mediumGroupUpdate = update;
this.removeFromCache(envelope);
textsecure.messaging.updateMediumGroup(otherMembers, proto);
}
// Subscribe to this group
this.pollForAdditionalId(groupId);
}
this.removeFromCache(envelope);
}
},
async handleDataMessage(envelope, msg) {
window.log.info('data message from', this.getEnvelopeId(envelope));
@ -1308,12 +1430,40 @@ MessageReceiver.prototype.extend({
!_.isEmpty(message.body) &&
friendRequestStatusNoneOrExpired;
// Build a 'message' event i.e. a received message event
const ev = new Event('message');
const source = envelope.senderIdentity || senderPubKey;
const isOwnDevice = async pubkey => {
const primaryDevice = window.storage.get('primaryDevicePubKey');
const secondaryDevices = await window.libloki.storage.getPairedDevicesFor(
primaryDevice
);
const allDevices = [primaryDevice, ...secondaryDevices];
return allDevices.includes(pubkey);
};
const ownDevice = await isOwnDevice(source);
let ev;
if (conversation.isMediumGroup() && ownDevice) {
// Data messages for medium groups don't arrive as sync messages. Instead,
// linked devices poll for group messages independently, thus they need
// to recognise some of those messages at their own.
ev = new Event('sent');
} else {
ev = new Event('message');
}
if (envelope.senderIdentity) {
message.group = {
id: envelope.source,
};
}
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
friendRequest: isFriendRequest,
source: senderPubKey,
source,
sourceDevice: envelope.sourceDevice,
timestamp: envelope.timestamp.toNumber(),
receivedAt: envelope.receivedAt,
@ -1331,6 +1481,7 @@ MessageReceiver.prototype.extend({
contact,
preview,
groupInvitation,
mediumGroupUpdate,
}) {
return (
!flags &&
@ -1340,7 +1491,8 @@ MessageReceiver.prototype.extend({
_.isEmpty(quote) &&
_.isEmpty(contact) &&
_.isEmpty(preview) &&
_.isEmpty(groupInvitation)
_.isEmpty(groupInvitation) &&
_.isEmpty(mediumGroupUpdate)
);
},
handleLegacyMessage(envelope) {

@ -167,6 +167,7 @@ function OutgoingMessage(
isMediumGroup,
publicSendData,
debugMessageType,
autoSession,
} =
options || {};
this.numberInfo = numberInfo;
@ -182,6 +183,7 @@ function OutgoingMessage(
this.online = online;
this.messageType = messageType || 'outgoing';
this.debugMessageType = debugMessageType;
this.autoSession = autoSession || false;
}
OutgoingMessage.prototype = {

@ -430,7 +430,7 @@ MessageSender.prototype = {
let keysFound = false;
// If we don't have a session but we already have prekeys to
// start communication then we should use them
if (!haveSession && !options.isPublic) {
if (!haveSession && !options.isPublic && !options.isMediumGroup) {
keysFound = await hasKeys(number);
}
@ -460,7 +460,7 @@ MessageSender.prototype = {
message.dataMessage.group
);
// If it was a message to a group then we need to send a session request
if (isGroupMessage) {
if (isGroupMessage || options.autoSession) {
const sessionRequestMessage = textsecure.OutgoingMessage.buildSessionRequestMessage(
number
);
@ -710,7 +710,11 @@ MessageSender.prototype = {
}
// We only want to sync across closed groups that we haven't left
const sessionGroups = conversations.filter(
c => c.isClosedGroup() && !c.get('left') && c.isFriend()
c =>
c.isClosedGroup() &&
!c.get('left') &&
c.isFriend() &&
!c.isMediumGroup()
);
if (sessionGroups.length === 0) {
window.console.info('No closed group to sync.');
@ -975,7 +979,12 @@ MessageSender.prototype = {
});
},
sendGroupProto(providedNumbers, proto, timestamp = Date.now(), options = {}) {
async sendGroupProto(
providedNumbers,
proto,
timestamp = Date.now(),
options = {}
) {
// We always assume that only primary device is a member in the group
const primaryDeviceKey =
window.storage.get('primaryDevicePubKey') ||
@ -1014,12 +1023,13 @@ MessageSender.prototype = {
);
});
return sendPromise.then(result => {
// Sync the group message to our other devices
const encoded = textsecure.protobuf.DataMessage.encode(proto);
this.sendSyncMessage(encoded, timestamp, null, null, [], [], options);
return result;
});
const result = await sendPromise;
// Sync the group message to our other devices
const encoded = textsecure.protobuf.DataMessage.encode(proto);
this.sendSyncMessage(encoded, timestamp, null, null, [], [], options);
return result;
},
async getMessageProto(
@ -1201,12 +1211,18 @@ MessageSender.prototype = {
},
async updateMediumGroup(members, groupUpdateProto) {
// Automatically request session if not found (updates use pairwise sessions)
const autoSession = true;
await this.sendGroupProto(members, groupUpdateProto, Date.now(), {
isPublic: false,
autoSession,
});
return true;
},
async updateGroup(
async sendGroupUpdate(
groupId,
name,
avatar,
@ -1282,6 +1298,16 @@ MessageSender.prototype = {
return this.sendGroupProto(groupNumbers, proto, Date.now(), options);
},
requestSenderKeys(sender, groupId) {
const proto = new textsecure.protobuf.DataMessage();
const update = new textsecure.protobuf.MediumGroupUpdate();
update.type = textsecure.protobuf.MediumGroupUpdate.Type.SENDER_KEY_REQUEST;
update.groupId = groupId;
proto.mediumGroupUpdate = update;
textsecure.messaging.updateMediumGroup([sender], proto);
},
leaveGroup(groupId, groupNumbers, options) {
const proto = new textsecure.protobuf.DataMessage();
proto.group = new textsecure.protobuf.GroupContext();
@ -1385,12 +1411,13 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) {
this.resetSession = sender.resetSession.bind(sender);
this.sendMessageToGroup = sender.sendMessageToGroup.bind(sender);
this.sendTypingMessage = sender.sendTypingMessage.bind(sender);
this.updateGroup = sender.updateGroup.bind(sender);
this.sendGroupUpdate = sender.sendGroupUpdate.bind(sender);
this.updateMediumGroup = sender.updateMediumGroup.bind(sender);
this.addNumberToGroup = sender.addNumberToGroup.bind(sender);
this.setGroupName = sender.setGroupName.bind(sender);
this.setGroupAvatar = sender.setGroupAvatar.bind(sender);
this.requestGroupInfo = sender.requestGroupInfo.bind(sender);
this.requestSenderKeys = sender.requestSenderKeys.bind(sender);
this.leaveGroup = sender.leaveGroup.bind(sender);
this.sendSyncMessage = sender.sendSyncMessage.bind(sender);
this.getProfile = sender.getProfile.bind(sender);

@ -37,6 +37,7 @@
"test-electron": "yarn grunt test",
"test-integration": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js",
"test-integration-parts": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'registration' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'openGroup' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'addFriends' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'linkDevice' && ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'closedGroup'",
"test-medium-groups": "ELECTRON_DISABLE_SANDBOX=1 mocha --exit --timeout 10000 integration_test/integration_test.js --grep 'senderkeys'",
"test-node": "mocha --recursive --exit test/app test/modules ts/test libloki/test/node",
"eslint": "eslint --cache .",
"eslint-fix": "eslint --fix .",

@ -342,6 +342,7 @@ window.LokiMessageAPI = require('./js/modules/loki_message_api');
if (process.env.USE_STUBBED_NETWORK) {
window.StubMessageAPI = require('./integration_test/stubs/stub_message_api');
window.StubAppDotNetApi = require('./integration_test/stubs/stub_app_dot_net_api');
window.StubLokiSnodeAPI = require('./integration_test/stubs/stub_loki_snode_api');
}
window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api');
@ -430,6 +431,7 @@ window.lokiFeatureFlags = {
useSnodeProxy: !process.env.USE_STUBBED_NETWORK,
useOnionRequests: true,
useFileOnionRequests: false,
enableSenderKeys: false,
onionRequestHops: 1,
};
@ -456,7 +458,7 @@ if (
};
/* eslint-enable global-require, import/no-extraneous-dependencies */
window.lokiFeatureFlags = {};
window.lokiSnodeAPI = {}; // no need stub out each function here
window.lokiSnodeAPI = new window.StubLokiSnodeAPI(); // no need stub out each function here
}
if (config.environment.includes('test-integration')) {
window.lokiFeatureFlags = {

@ -51,17 +51,27 @@ message MediumGroupContent {
optional bytes ephemeralKey = 2;
}
message SenderKey {
optional string chainKey = 1;
optional uint32 keyIdx = 2;
}
message MediumGroupUpdate {
enum Type {
NEW_GROUP = 0; // groupId, groupName, groupSecretKey, members, senderKey
GROUP_INFO = 1; // groupId, groupName, members, senderKey
SENDER_KEY_REQUEST = 2; // groupId
SENDER_KEY = 3; // groupId, SenderKey
}
optional string groupName = 1;
optional string groupId = 2; // should this be bytes?
optional string groupSecretKey = 3;
optional string senderKey = 4;
repeated string members = 5;
}
message SenderKeyUpdate {
optional string groupId = 1;
optional string senderKey = 2;
optional bytes groupSecretKey = 3;
optional SenderKey senderKey = 4;
repeated bytes members = 5;
repeated string admins = 6;
optional Type type = 7;
}
message LokiAddressMessage {
@ -424,4 +434,5 @@ message GroupDetails {
optional string color = 7;
optional bool blocked = 8;
repeated string admins = 9;
optional bool is_medium_group = 10;
}

@ -190,7 +190,7 @@ textarea {
&.brand {
min-width: 165px;
height: 45px;
line-height: 40px;
align-items: center;
padding: 0px $session-margin-lg;
font-size: $session-font-md;
font-family: $session-font-accent;
@ -674,6 +674,17 @@ label {
}
}
.sealed-sender-toggle {
display: flex;
padding: 6px;
}
.sender-keys-description {
display: flex;
align-items: center;
padding-left: 10px;
}
.create-group-dialog .session-modal__body {
display: flex;
flex-direction: column;

@ -108,6 +108,7 @@ function joinChannelStateManager(
async function createClosedGroup(
groupName: string,
groupMembers: Array<ContactType>,
senderKeys: boolean,
onSuccess: any
) {
// Validate groupName and groupMembers length
@ -145,7 +146,12 @@ async function createClosedGroup(
}
const groupMemberIds = groupMembers.map(m => m.id);
await window.doCreateGroup(groupName, groupMemberIds);
if (senderKeys) {
await window.createMediumSizeGroup(groupName, groupMemberIds);
} else {
await window.doCreateGroup(groupName, groupMemberIds);
}
if (onSuccess) {
onSuccess();

@ -8,6 +8,7 @@ declare global {
interface Window {
Lodash: any;
doCreateGroup: any;
createMediumSizeGroup: any;
SMALL_GROUP_SIZE_LIMIT: number;
}
}

@ -108,9 +108,11 @@ export class InviteFriendsDialog extends React.Component<Props, State> {
private renderMemberList() {
const members = this.state.friendList;
return members.map((member: ContactType) => (
return members.map((member: ContactType, index: number) => (
<SessionMemberListItem
member={member}
key={index}
index={index}
isSelected={false}
onSelect={(selectedMember: ContactType) => {
this.onMemberClicked(selectedMember);

@ -147,9 +147,10 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
private renderMemberList() {
const members = this.state.friendList;
return members.map((member: ContactType) => (
return members.map((member: ContactType, index: number) => (
<SessionMemberListItem
member={member}
index={index}
isSelected={!member.checkmarked}
onSelect={this.onMemberClicked}
onUnselect={this.onMemberClicked}

@ -325,8 +325,9 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
}}
onButtonClick={async (
groupName: string,
groupMembers: Array<ContactType>
) => this.onCreateClosedGroup(groupName, groupMembers)}
groupMembers: Array<ContactType>,
senderKeys: boolean
) => this.onCreateClosedGroup(groupName, groupMembers, senderKeys)}
searchTerm={searchTerm}
updateSearch={this.updateSearchBound}
showSpinner={loading}
@ -480,16 +481,22 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
private async onCreateClosedGroup(
groupName: string,
groupMembers: Array<ContactType>
groupMembers: Array<ContactType>,
senderKeys: boolean
) {
await MainViewController.createClosedGroup(groupName, groupMembers, () => {
this.handleToggleOverlay(undefined);
window.pushToast({
title: window.i18n('closedGroupCreatedToastTitle'),
type: 'success',
});
});
await MainViewController.createClosedGroup(
groupName,
groupMembers,
senderKeys,
() => {
this.handleToggleOverlay(undefined);
window.pushToast({
title: window.i18n('closedGroupCreatedToastTitle'),
type: 'success',
});
}
);
}
private handleNewSessionButtonClick() {

@ -123,6 +123,7 @@ export class LeftPaneSectionHeader extends React.Component<Props, State> {
count={notificationCount}
size={NotificationCountSize.ON_HEADER}
onClick={this.props.buttonClicked}
key="notificationCount"
/>
);
}

@ -1,6 +1,7 @@
import React from 'react';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
import { SessionToggle } from './SessionToggle';
import { SessionIdEditable } from './SessionIdEditable';
import { UserSearchDropdown } from './UserSearchDropdown';
import { ContactType, SessionMemberListItem } from './SessionMemberListItem';
@ -12,6 +13,7 @@ import {
} from './SessionButton';
import { SessionSpinner } from './SessionSpinner';
import { PillDivider } from './PillDivider';
import classNames from 'classnames';
export enum SessionClosableOverlayType {
Contact = 'contact',
@ -35,6 +37,7 @@ interface Props {
interface State {
groupName: string;
selectedMembers: Array<ContactType>;
senderKeys: boolean;
}
export class SessionClosableOverlay extends React.Component<Props, State> {
@ -46,6 +49,7 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
this.state = {
groupName: '',
selectedMembers: [],
senderKeys: false,
};
this.inputRef = React.createRef();
@ -150,7 +154,7 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
default:
}
const { groupName, selectedMembers } = this.state;
const { groupName, selectedMembers, senderKeys } = this.state;
const ourSessionID = window.textsecure.storage.user.getNumber();
const contacts = this.getContacts();
@ -245,21 +249,44 @@ export class SessionClosableOverlay extends React.Component<Props, State> {
/>
)}
{isClosedGroupView &&
window.lokiFeatureFlags.enableSenderKeys && (
<div className="sealed-sender-toggle">
<SessionToggle
active={Boolean(false)}
onClick={() => {
const value = this.state.senderKeys;
this.setState({ senderKeys: !value });
}}
/>
<span
className={classNames(
'session-settings-item__description',
'sender-keys-description'
)}
>
{window.i18n('useSenderKeys')}
</span>
</div>
)}
<SessionButton
buttonColor={SessionButtonColor.Green}
buttonType={SessionButtonType.BrandOutline}
text={buttonText}
disabled={noContactsForClosedGroup}
onClick={() => onButtonClick(groupName, selectedMembers)}
onClick={() => onButtonClick(groupName, selectedMembers, senderKeys)}
/>
</div>
);
}
private renderMemberList(members: any) {
return members.map((member: ContactType) => (
return members.map((member: ContactType, index: number) => (
<SessionMemberListItem
member={member}
index={index}
isSelected={false}
key={member.id}
onSelect={(selectedMember: ContactType) => {

@ -18,6 +18,7 @@ export interface ContactType {
interface Props {
member: ContactType;
index: number; // index in the list
isSelected: boolean;
onSelect?: any;
onUnselect?: any;
@ -54,7 +55,11 @@ export class SessionMemberListItem extends React.Component<Props, State> {
return (
<div
className={classNames('session-member-item', isSelected && 'selected')}
className={classNames(
`session-member-item-${this.props.index}`,
'session-member-item',
isSelected && 'selected'
)}
onClick={this.handleSelectionAction}
role="button"
>

Loading…
Cancel
Save