Merge branch 'clearnet' of https://github.com/loki-project/session-desktop into clearnet

pull/1002/head
Vincent 5 years ago
commit 8152e98636

@ -63,7 +63,7 @@ module.exports = {
// high value as a buffer to let Prettier control the line length: // high value as a buffer to let Prettier control the line length:
code: 999, code: 999,
// We still want to limit comments as before: // We still want to limit comments as before:
comments: 90, comments: 150,
ignoreUrls: true, ignoreUrls: true,
ignoreRegExpLiterals: true, ignoreRegExpLiterals: true,
}, },

@ -3,11 +3,12 @@
## Summary ## Summary
Session integrates directly with [Loki Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users IP Addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper). Session integrates directly with [Loki Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users IP Addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper).
<br/><br/>
![DesktopSession](https://i.imgur.com/ZnHvYjo.jpg) ![DesktopSession](https://i.imgur.com/ZnHvYjo.jpg)
## Want to Contribute? Found a Bug or Have a feature request? ## Want to Contribute? Found a Bug or Have a feature request?
Please search for any [existing issues](https://github.com/loki-project/session-desktop/issues) that describe your bugs in order to avoid duplicate submissions. Submissions can be made by making a pull request to our development branch. If you don't know where to start contributing , try reading the Github issues page for ideas. Please search for any [existing issues](https://github.com/loki-project/session-desktop/issues) that describe your bugs in order to avoid duplicate submissions. <br><br>Submissions can be made by making a pull request to our development branch. If you don't know where to start contributing, try reading the Github issues page for ideas.
## Build instruction ## Build instruction
@ -15,10 +16,7 @@ Build instructions can be found in [BUILDING.md](BUILDING.md).
## License ## License
Copyright 2011 Whisper Systems Copyright 2011 Whisper Systems<br/>
Copyright 2013-2017 Open Whisper Systems<br/>
Copyright 2013-2017 Open Whisper Systems Copyright 2019-2020 The Loki Project<br/>
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html<br/>
Copyright 2019-2020 The Loki Project
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html

@ -2204,12 +2204,16 @@
"message": "Edit Profile", "message": "Edit Profile",
"description": "Button action that the user can click to edit their profile" "description": "Button action that the user can click to edit their profile"
}, },
"editGroupName": {
"message": "Edit group name",
"description": "Button action that the user can click to edit a group name"
},
"createGroupDialogTitle": { "createGroupDialogTitle": {
"message": "Creating a Private Group Chat", "message": "Creating a Closed Group",
"description": "Title for the dialog box used to create a new private group" "description": "Title for the dialog box used to create a new private group"
}, },
"updateGroupDialogTitle": { "updateGroupDialogTitle": {
"message": "Updating a Private Group Chat", "message": "Updating a Closed Group",
"description": "description":
"Title for the dialog box used to update an existing private group" "Title for the dialog box used to update an existing private group"
}, },
@ -2529,6 +2533,9 @@
"noFriendsToAdd": { "noFriendsToAdd": {
"message": "No friends to add" "message": "No friends to add"
}, },
"noMembersInThisGroup": {
"message": "No other members in this group"
},
"noModeratorsToRemove": { "noModeratorsToRemove": {
"message": "no moderators to remove" "message": "no moderators to remove"
}, },

@ -1135,9 +1135,14 @@
} }
}); });
Whisper.events.on('updateGroup', async groupConvo => { Whisper.events.on('updateGroupName', async groupConvo => {
if (appView) { if (appView) {
appView.showUpdateGroupDialog(groupConvo); appView.showUpdateGroupNameDialog(groupConvo);
}
});
Whisper.events.on('updateGroupMembers', async groupConvo => {
if (appView) {
appView.showUpdateGroupMembersDialog(groupConvo);
} }
}); });

@ -475,7 +475,6 @@ SecretSessionCipher.prototype = {
// private byte[] decrypt(UnidentifiedSenderMessageContent message) // private byte[] decrypt(UnidentifiedSenderMessageContent message)
_decryptWithUnidentifiedSenderMessage(message) { _decryptWithUnidentifiedSenderMessage(message) {
const { SessionCipher } = this;
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
const sender = new libsignal.SignalProtocolAddress( const sender = new libsignal.SignalProtocolAddress(
@ -485,12 +484,12 @@ SecretSessionCipher.prototype = {
switch (message.type) { switch (message.type) {
case CiphertextMessage.WHISPER_TYPE: case CiphertextMessage.WHISPER_TYPE:
return new SessionCipher( return new libloki.crypto.LokiSessionCipher(
signalProtocolStore, signalProtocolStore,
sender sender
).decryptWhisperMessage(message.content); ).decryptWhisperMessage(message.content);
case CiphertextMessage.PREKEY_TYPE: case CiphertextMessage.PREKEY_TYPE:
return new SessionCipher( return new libloki.crypto.LokiSessionCipher(
signalProtocolStore, signalProtocolStore,
sender sender
).decryptPreKeyWhisperMessage(message.content); ).decryptPreKeyWhisperMessage(message.content);

@ -34,8 +34,8 @@ const {
ConversationHeader, ConversationHeader,
} = require('../../ts/components/conversation/ConversationHeader'); } = require('../../ts/components/conversation/ConversationHeader');
const { const {
SessionChannelSettings, SessionGroupSettings,
} = require('../../ts/components/session/SessionChannelSettings'); } = require('../../ts/components/session/SessionGroupSettings');
const { const {
EmbeddedContact, EmbeddedContact,
} = require('../../ts/components/conversation/EmbeddedContact'); } = require('../../ts/components/conversation/EmbeddedContact');
@ -93,8 +93,11 @@ const {
} = require('../../ts/components/session/SessionRegistrationView'); } = require('../../ts/components/session/SessionRegistrationView');
const { const {
UpdateGroupDialog, UpdateGroupNameDialog,
} = require('../../ts/components/conversation/UpdateGroupDialog'); } = require('../../ts/components/conversation/UpdateGroupNameDialog');
const {
UpdateGroupMembersDialog,
} = require('../../ts/components/conversation/UpdateGroupMembersDialog');
const { const {
InviteFriendsDialog, InviteFriendsDialog,
} = require('../../ts/components/conversation/InviteFriendsDialog'); } = require('../../ts/components/conversation/InviteFriendsDialog');
@ -278,7 +281,7 @@ exports.setup = (options = {}) => {
ContactListItem, ContactListItem,
ContactName, ContactName,
ConversationHeader, ConversationHeader,
SessionChannelSettings, SessionGroupSettings,
SettingsView, SettingsView,
EmbeddedContact, EmbeddedContact,
Emojify, Emojify,
@ -293,7 +296,8 @@ exports.setup = (options = {}) => {
DevicePairingDialog, DevicePairingDialog,
SessionRegistrationView, SessionRegistrationView,
ConfirmDialog, ConfirmDialog,
UpdateGroupDialog, UpdateGroupNameDialog,
UpdateGroupMembersDialog,
InviteFriendsDialog, InviteFriendsDialog,
AddModeratorsDialog, AddModeratorsDialog,
RemoveModeratorsDialog, RemoveModeratorsDialog,

@ -228,10 +228,15 @@
const dialog = new Whisper.CreateGroupDialogView(); const dialog = new Whisper.CreateGroupDialogView();
this.el.append(dialog.el); this.el.append(dialog.el);
}, },
showUpdateGroupDialog(groupConvo) { showUpdateGroupNameDialog(groupConvo) {
const dialog = new Whisper.UpdateGroupDialogView(groupConvo); const dialog = new Whisper.UpdateGroupNameDialogView(groupConvo);
this.el.prepend(dialog.el); this.el.append(dialog.el);
}, },
showUpdateGroupMembersDialog(groupConvo) {
const dialog = new Whisper.UpdateGroupMembersDialogView(groupConvo);
this.el.append(dialog.el);
},
showSessionRestoreConfirmation(options) { showSessionRestoreConfirmation(options) {
const dialog = new Whisper.ConfirmSessionResetView(options); const dialog = new Whisper.ConfirmSessionResetView(options);
this.el.append(dialog.el); this.el.append(dialog.el);

@ -244,11 +244,6 @@
onMoveToInbox: () => { onMoveToInbox: () => {
this.model.setArchived(false); this.model.setArchived(false);
}, },
onUpdateGroup: () => {
window.Whisper.events.trigger('updateGroup', this.model);
},
onLeaveGroup: () => { onLeaveGroup: () => {
window.Whisper.events.trigger('leaveGroup', this.model); window.Whisper.events.trigger('leaveGroup', this.model);
}, },
@ -276,7 +271,8 @@
}, },
}; };
}; };
const getGroupSettingsProp = () => { const getGroupSettingsProps = () => {
const ourPK = window.textsecure.storage.user.getNumber();
const members = this.model.get('members') || []; const members = this.model.get('members') || [];
return { return {
@ -288,6 +284,7 @@
avatarPath: this.model.getAvatarPath(), avatarPath: this.model.getAvatarPath(),
isGroup: !this.model.isPrivate(), isGroup: !this.model.isPrivate(),
isPublic: this.model.isPublic(), isPublic: this.model.isPublic(),
isAdmin: this.model.get('groupAdmins').includes(ourPK),
isRss: this.model.isRss(), isRss: this.model.isRss(),
memberCount: members.length, memberCount: members.length,
@ -303,8 +300,11 @@
this.hideConversationRight(); this.hideConversationRight();
}, },
onUpdateGroup: () => { onUpdateGroupName: () => {
window.Whisper.events.trigger('updateGroup', this.model); window.Whisper.events.trigger('updateGroupName', this.model);
},
onUpdateGroupMembers: () => {
window.Whisper.events.trigger('updateGroupMembers', this.model);
}, },
onLeaveGroup: () => { onLeaveGroup: () => {
@ -355,12 +355,15 @@
if (!this.groupSettings) { if (!this.groupSettings) {
this.groupSettings = new Whisper.ReactWrapperView({ this.groupSettings = new Whisper.ReactWrapperView({
className: 'group-settings', className: 'group-settings',
Component: window.Signal.Components.SessionChannelSettings, Component: window.Signal.Components.SessionGroupSettings,
props: getGroupSettingsProp(this.model), props: getGroupSettingsProps(this.model),
}); });
this.$('.conversation-content-right').append(this.groupSettings.el); this.$('.conversation-content-right').append(this.groupSettings.el);
this.updateGroupSettingsPanel = () =>
this.groupSettings.update(getGroupSettingsProps(this.model));
this.listenTo(this.model, 'change', this.updateGroupSettingsPanel);
} else { } else {
this.groupSettings.update(getGroupSettingsProp(this.model)); this.groupSettings.update(getGroupSettingsProps(this.model));
} }
this.showConversationRight(); this.showConversationRight();

@ -47,13 +47,13 @@
}, },
}); });
Whisper.UpdateGroupDialogView = Whisper.View.extend({ Whisper.UpdateGroupNameDialogView = Whisper.View.extend({
className: 'loki-dialog modal', className: 'loki-dialog modal',
initialize(groupConvo) { initialize(groupConvo) {
this.groupName = groupConvo.get('name'); this.groupName = groupConvo.get('name');
this.conversation = groupConvo; this.conversation = groupConvo;
this.titleText = `${i18n('updateGroupDialogTitle')}: ${this.groupName}`; this.titleText = i18n('updateGroupDialogTitle');
this.okText = i18n('ok'); this.okText = i18n('ok');
this.cancelText = i18n('cancel'); this.cancelText = i18n('cancel');
this.close = this.close.bind(this); this.close = this.close.bind(this);
@ -106,7 +106,90 @@
render() { render() {
this.dialogView = new Whisper.ReactWrapperView({ this.dialogView = new Whisper.ReactWrapperView({
className: 'create-group-dialog', className: 'create-group-dialog',
Component: window.Signal.Components.UpdateGroupDialog, Component: window.Signal.Components.UpdateGroupNameDialog,
props: {
titleText: this.titleText,
groupName: this.groupName,
okText: this.okText,
isPublic: this.isPublic,
cancelText: this.cancelText,
existingMembers: this.existingMembers,
isAdmin: this.isAdmin,
onClose: this.close,
onSubmit: this.onSubmit,
},
});
this.$el.append(this.dialogView.el);
return this;
},
onSubmit(newGroupName, members) {
const groupId = this.conversation.get('id');
window.doUpdateGroup(groupId, newGroupName, members);
},
close() {
this.remove();
},
});
Whisper.UpdateGroupMembersDialogView = Whisper.View.extend({
className: 'loki-dialog modal',
initialize(groupConvo) {
this.groupName = groupConvo.get('name');
this.conversation = groupConvo;
this.titleText = i18n('updateGroupDialogTitle');
this.okText = i18n('ok');
this.cancelText = i18n('cancel');
this.close = this.close.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.isPublic = groupConvo.isPublic();
const ourPK = textsecure.storage.user.getNumber();
this.isAdmin = groupConvo.get('groupAdmins').includes(ourPK);
const convos = window.getConversations().models.filter(d => !!d);
let existingMembers = groupConvo.get('members') || [];
// Show a contact if they are our friend or if they are a member
const friendsAndMembers = convos.filter(
d => existingMembers.includes(d.id) && d.isPrivate() && !d.isMe()
);
this.friendsAndMembers = _.uniq(friendsAndMembers, true, d => d.id);
// at least make sure it's an array
if (!Array.isArray(existingMembers)) {
existingMembers = [];
}
this.existingMembers = existingMembers;
// public chat settings overrides
if (this.isPublic) {
// fix the title
this.titleText = `${i18n('updatePublicGroupDialogTitle')}: ${
this.groupName
}`;
// I'd much prefer to integrate mods with groupAdmins
// but lets discuss first...
this.isAdmin = groupConvo.isModerator(
window.storage.get('primaryDevicePubKey')
);
// zero out friendList for now
this.friendsAndMembers = [];
this.existingMembers = [];
}
this.$el.focus();
this.render();
},
render() {
this.dialogView = new Whisper.ReactWrapperView({
className: 'create-group-dialog',
Component: window.Signal.Components.UpdateGroupMembersDialog,
props: { props: {
titleText: this.titleText, titleText: this.titleText,
groupName: this.groupName, groupName: this.groupName,
@ -124,12 +207,12 @@
this.$el.append(this.dialogView.el); this.$el.append(this.dialogView.el);
return this; return this;
}, },
onSubmit(newGroupName, newMembers) { onSubmit(groupName, newMembers) {
const ourPK = textsecure.storage.user.getNumber(); const ourPK = textsecure.storage.user.getNumber();
const allMembers = window.Lodash.concat(newMembers, [ourPK]); const allMembers = window.Lodash.concat(newMembers, [ourPK]);
const groupId = this.conversation.get('id'); const groupId = this.conversation.get('id');
window.doUpdateGroup(groupId, newGroupName, allMembers); window.doUpdateGroup(groupId, groupName, allMembers);
}, },
close() { close() {
this.remove(); this.remove();

@ -204,10 +204,10 @@
}); });
}, },
showFileSizeError() { showFileSizeError(limit, units) {
window.pushToast({ window.pushToast({
title: i18n('fileSizeWarning'), title: i18n('fileSizeWarning'),
description: `Max size: ${this.model.limit} ${this.model.units}`, description: `Max size: ${limit} ${units}`,
type: 'error', type: 'error',
id: 'fileSizeWarning', id: 'fileSizeWarning',
}); });
@ -339,7 +339,7 @@
contentType, contentType,
file, file,
}); });
let limitKb = 1000000; let limitKb = 10000;
const blobType = const blobType =
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0]; file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
@ -348,19 +348,19 @@
limitKb = 6000; limitKb = 6000;
break; break;
case 'gif': case 'gif':
limitKb = 25000; limitKb = 10000;
break; break;
case 'audio': case 'audio':
limitKb = 100000; limitKb = 10000;
break; break;
case 'video': case 'video':
limitKb = 100000; limitKb = 10000;
break; break;
default: default:
limitKb = 100000; limitKb = 10000;
break; break;
} }
if ((blob.size / 1024).toFixed(4) >= limitKb) { if ((blob.file.size / 1024).toFixed(4) >= limitKb) {
const units = ['kB', 'MB', 'GB']; const units = ['kB', 'MB', 'GB'];
let u = -1; let u = -1;
let limit = limitKb * 1000; let limit = limitKb * 1000;
@ -368,7 +368,7 @@
limit /= 1000; limit /= 1000;
u += 1; u += 1;
} while (limit >= 1000 && u < units.length - 1); } while (limit >= 1000 && u < units.length - 1);
this.showFileSizeError(); this.showFileSizeError(limit, units[u]);
return; return;
} }
} catch (error) { } catch (error) {

@ -1,4 +1,4 @@
/* global Whisper */ /* global Whisper, _ */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function() {
@ -22,6 +22,8 @@
this.chatName = convo.get('name'); this.chatName = convo.get('name');
this.chatServer = convo.get('server'); this.chatServer = convo.get('server');
this.channelId = convo.get('channelId'); this.channelId = convo.get('channelId');
this.isPublic = !!convo.cachedProps.isPublic;
this.convo = convo;
this.$el.focus(); this.$el.focus();
this.render(); this.render();
@ -45,14 +47,56 @@
this.remove(); this.remove();
}, },
submit(pubkeys) { submit(pubkeys) {
window.sendGroupInvitations( // public group chats
{ if (this.isPublic) {
address: this.chatServer, window.sendGroupInvitations(
name: this.chatName, {
channelId: this.channelId, address: this.chatServer,
}, name: this.chatName,
pubkeys channelId: this.channelId,
); },
pubkeys
);
} else {
// private group chats
const ourPK = window.textsecure.storage.user.getNumber();
let existingMembers = this.convo.get('members') || [];
// at least make sure it's an array
if (!Array.isArray(existingMembers)) {
existingMembers = [];
}
existingMembers = existingMembers.filter(d => !!d);
const newMembers = pubkeys.filter(d => !existingMembers.includes(d));
if (newMembers.length > 0) {
// Do not trigger an update if there is too many members
if (
newMembers.length + existingMembers.length >
window.SMALL_GROUP_SIZE_LIMIT
) {
const msg = `${window.i18n('maxGroupMembersError')} ${
window.SMALL_GROUP_SIZE_LIMIT
}`;
window.pushToast({
title: msg,
type: 'error',
id: 'tooManyMembers',
});
return;
}
const allMembers = window.Lodash.concat(existingMembers, newMembers, [
ourPK,
]);
const uniqMembers = _.uniq(allMembers, true, d => d);
const groupId = this.convo.get('id');
const groupName = this.convo.get('name');
window.doUpdateGroup(groupId, groupName, uniqMembers);
}
}
}, },
}); });
})(); })();

@ -324,6 +324,154 @@
GRANT: 2, GRANT: 2,
}); });
/**
* A wrapper around Signal's SessionCipher.
* This handles specific session reset logic that we need.
*/
class LokiSessionCipher {
constructor(storage, protocolAddress) {
this.storage = storage;
this.protocolAddress = protocolAddress;
this.sessionCipher = new libsignal.SessionCipher(
storage,
protocolAddress
);
this.TYPE = Object.freeze({
MESSAGE: 1,
PREKEY: 2,
});
}
decryptWhisperMessage(buffer, encoding) {
return this._decryptMessage(this.TYPE.MESSAGE, buffer, encoding);
}
decryptPreKeyWhisperMessage(buffer, encoding) {
return this._decryptMessage(this.TYPE.PREKEY, buffer, encoding);
}
async _decryptMessage(type, buffer, encoding) {
// Capture active session
const activeSessionBaseKey = await this._getCurrentSessionBaseKey();
if (type === this.TYPE.PREKEY && !activeSessionBaseKey) {
const wrapped = dcodeIO.ByteBuffer.wrap(buffer);
await window.libloki.storage.verifyFriendRequestAcceptPreKey(
this.protocolAddress.getName(),
wrapped
);
}
const decryptFunction =
type === this.TYPE.PREKEY
? this.sessionCipher.decryptPreKeyWhisperMessage
: this.sessionCipher.decryptWhisperMessage;
const result = await decryptFunction(buffer, encoding);
// Handle session reset
// This needs to be done synchronously so that the next time we decrypt a message,
// we have the correct session
try {
await this._handleSessionResetIfNeeded(activeSessionBaseKey);
} catch (e) {
window.log.info('Failed to handle session reset: ', e);
}
return result;
}
async _handleSessionResetIfNeeded(previousSessionBaseKey) {
if (!previousSessionBaseKey) {
return;
}
let conversation;
try {
conversation = await window.ConversationController.getOrCreateAndWait(
this.protocolAddress.getName(),
'private'
);
} catch (e) {
window.log.info(
'Error getting conversation: ',
this.protocolAddress.getName()
);
return;
}
if (conversation.isSessionResetOngoing()) {
const currentSessionBaseKey = await this._getCurrentSessionBaseKey();
if (currentSessionBaseKey !== previousSessionBaseKey) {
if (conversation.isSessionResetReceived()) {
// The other user used an old session to contact us; wait for them to switch to a new one.
await this._restoreSession(previousSessionBaseKey);
} else {
// Our session reset was successful; we initiated one and got a new session back from the other user.
await this._deleteAllSessionExcept(currentSessionBaseKey);
await conversation.onNewSessionAdopted();
}
} else if (conversation.isSessionResetReceived()) {
// Our session reset was successful; we received a message with the same session from the other user.
await this._deleteAllSessionExcept(previousSessionBaseKey);
await conversation.onNewSessionAdopted();
}
}
}
async _getCurrentSessionBaseKey() {
const record = await this.sessionCipher.getRecord(
this.protocolAddress.toString()
);
if (!record) {
return null;
}
const openSession = record.getOpenSession();
if (!openSession) {
return null;
}
const { baseKey } = openSession.indexInfo;
return baseKey;
}
async _restoreSession(sessionBaseKey) {
const record = await this.sessionCipher.getRecord(
this.protocolAddress.toString()
);
if (!record) {
return;
}
record.archiveCurrentState();
const sessionToRestore = record.sessions[sessionBaseKey];
if (!sessionToRestore) {
throw new Error(`Cannot find session with base key ${sessionBaseKey}`);
}
record.promoteState(sessionToRestore);
record.updateSessionState(sessionToRestore);
await this.storage.storeSession(
this.protocolAddress.toString(),
record.serialize()
);
}
async _deleteAllSessionExcept(sessionBaseKey) {
const record = await this.sessionCipher.getRecord(
this.protocolAddress.toString()
);
if (!record) {
return;
}
const sessionToKeep = record.sessions[sessionBaseKey];
record.sessions = {};
record.updateSessionState(sessionToKeep);
await this.storage.storeSession(
this.protocolAddress.toString(),
record.serialize()
);
}
}
window.libloki.crypto = { window.libloki.crypto = {
DHEncrypt, DHEncrypt,
DHDecrypt, DHDecrypt,
@ -336,6 +484,7 @@
verifyAuthorisation, verifyAuthorisation,
validateAuthorisation, validateAuthorisation,
PairingType, PairingType,
LokiSessionCipher,
// for testing // for testing
_LokiSnodeChannel: LokiSnodeChannel, _LokiSnodeChannel: LokiSnodeChannel,
_decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey, _decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey,

@ -667,58 +667,29 @@ MessageReceiver.prototype.extend({
async decrypt(envelope, ciphertext) { async decrypt(envelope, ciphertext) {
let promise; let promise;
// We don't have source at this point yet (with sealed sender)
// This needs a massive cleanup!
const address = new libsignal.SignalProtocolAddress(
envelope.source,
envelope.sourceDevice
);
const ourNumber = textsecure.storage.user.getNumber(); const ourNumber = textsecure.storage.user.getNumber();
const number = address.toString().split('.')[0];
const options = {};
// No limit on message keys if we're communicating with our other devices
if (ourNumber === number) {
options.messageKeysLimit = false;
}
// Will become obsolete
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address,
options
);
const me = { const me = {
number: ourNumber, number: ourNumber,
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10), deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
}; };
// Will become obsolete // Envelope.source will be null on UNIDENTIFIED_SENDER
const getCurrentSessionBaseKey = async () => { // Don't use it there!
const record = await sessionCipher.getRecord(address.toString()); const address = new libsignal.SignalProtocolAddress(
if (!record) { envelope.source,
return null; envelope.sourceDevice
} );
const openSession = record.getOpenSession();
if (!openSession) {
return null;
}
const { baseKey } = openSession.indexInfo;
return baseKey;
};
// Will become obsolete const lokiSessionCipher = new libloki.crypto.LokiSessionCipher(
const captureActiveSession = async () => { textsecure.storage.protocol,
this.activeSessionBaseKey = await getCurrentSessionBaseKey(sessionCipher); address
}; );
switch (envelope.type) { switch (envelope.type) {
case textsecure.protobuf.Envelope.Type.CIPHERTEXT: case textsecure.protobuf.Envelope.Type.CIPHERTEXT:
window.log.info('message from', this.getEnvelopeId(envelope)); window.log.info('message from', this.getEnvelopeId(envelope));
promise = captureActiveSession() promise = lokiSessionCipher
.then(() => sessionCipher.decryptWhisperMessage(ciphertext)) .decryptWhisperMessage(ciphertext)
.then(this.unpad); .then(this.unpad);
break; break;
case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: { case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: {
@ -735,25 +706,11 @@ MessageReceiver.prototype.extend({
} }
case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE: case textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE:
window.log.info('prekey message from', this.getEnvelopeId(envelope)); window.log.info('prekey message from', this.getEnvelopeId(envelope));
promise = captureActiveSession(sessionCipher).then(async () => { promise = this.decryptPreKeyWhisperMessage(
if (!this.activeSessionBaseKey) { ciphertext,
try { lokiSessionCipher,
const buffer = dcodeIO.ByteBuffer.wrap(ciphertext); address
await window.libloki.storage.verifyFriendRequestAcceptPreKey( );
envelope.source,
buffer
);
} catch (e) {
await this.removeFromCache(envelope);
throw e;
}
}
return this.decryptPreKeyWhisperMessage(
ciphertext,
sessionCipher,
address
);
});
break; break;
case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: { case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: {
window.log.info('received unidentified sender message'); window.log.info('received unidentified sender message');
@ -856,72 +813,6 @@ MessageReceiver.prototype.extend({
window.log.info('Error getting conversation: ', envelope.source); window.log.info('Error getting conversation: ', envelope.source);
} }
// lint hates anything after // (so /// is no good)
// *** BEGIN: session reset ***
// we have address in scope from parent scope
// seems to be the same input parameters
// going to comment out due to lint complaints
/*
const address = new libsignal.SignalProtocolAddress(
envelope.source,
envelope.sourceDevice
);
*/
const restoreActiveSession = async () => {
const record = await sessionCipher.getRecord(address.toString());
if (!record) {
return;
}
record.archiveCurrentState();
// NOTE: activeSessionBaseKey will be undefined here...
const sessionToRestore = record.sessions[this.activeSessionBaseKey];
record.promoteState(sessionToRestore);
record.updateSessionState(sessionToRestore);
await textsecure.storage.protocol.storeSession(
address.toString(),
record.serialize()
);
};
const deleteAllSessionExcept = async sessionBaseKey => {
const record = await sessionCipher.getRecord(address.toString());
if (!record) {
return;
}
const sessionToKeep = record.sessions[sessionBaseKey];
record.sessions = {};
record.updateSessionState(sessionToKeep);
await textsecure.storage.protocol.storeSession(
address.toString(),
record.serialize()
);
};
if (conversation.isSessionResetOngoing()) {
const currentSessionBaseKey = await getCurrentSessionBaseKey(
sessionCipher
);
if (
this.activeSessionBaseKey &&
currentSessionBaseKey !== this.activeSessionBaseKey
) {
if (conversation.isSessionResetReceived()) {
await restoreActiveSession();
} else {
await deleteAllSessionExcept(currentSessionBaseKey);
await conversation.onNewSessionAdopted();
}
} else if (conversation.isSessionResetReceived()) {
await deleteAllSessionExcept(this.activeSessionBaseKey);
await conversation.onNewSessionAdopted();
}
}
// lint hates anything after // (so /// is no good)
// *** END ***
// Type here can actually be UNIDENTIFIED_SENDER even if // Type here can actually be UNIDENTIFIED_SENDER even if
// the underlying message is FRIEND_REQUEST // the underlying message is FRIEND_REQUEST
if ( if (
@ -1470,6 +1361,7 @@ MessageReceiver.prototype.extend({
content.preKeyBundleMessage content.preKeyBundleMessage
); );
} }
if (content.lokiAddressMessage) { if (content.lokiAddressMessage) {
return this.handleLokiAddressMessage( return this.handleLokiAddressMessage(
envelope, envelope,
@ -1833,7 +1725,7 @@ MessageReceiver.prototype.extend({
textsecure.storage.protocol, textsecure.storage.protocol,
address address
); );
builder.processPreKey(device); await builder.processPreKey(device);
}) })
); );
await conversation.onSessionResetReceived(); await conversation.onSessionResetReceived();

@ -391,6 +391,8 @@ OutgoingMessage.prototype = {
: null; : null;
const isEndSession = const isEndSession =
flags === textsecure.protobuf.DataMessage.Flags.END_SESSION; flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
const isSessionRequest =
flags === textsecure.protobuf.DataMessage.Flags.SESSION_REQUEST;
const signalCipher = new libsignal.SessionCipher( const signalCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
address address
@ -485,6 +487,7 @@ OutgoingMessage.prototype = {
content, content,
pubKey: devicePubKey, pubKey: devicePubKey,
isFriendRequest: enableFallBackEncryption, isFriendRequest: enableFallBackEncryption,
isSessionRequest,
}; };
}) })
) )
@ -494,7 +497,12 @@ OutgoingMessage.prototype = {
if (!outgoingObject) { if (!outgoingObject) {
return; return;
} }
const destination = outgoingObject.pubKey; const {
pubKey: destination,
ttl,
isFriendRequest,
isSessionRequest,
} = outgoingObject;
try { try {
const socketMessage = await this.wrapInWebsocketMessage( const socketMessage = await this.wrapInWebsocketMessage(
outgoingObject outgoingObject
@ -503,9 +511,9 @@ OutgoingMessage.prototype = {
destination, destination,
socketMessage, socketMessage,
this.timestamp, this.timestamp,
outgoingObject.ttl ttl
); );
if (outgoingObject.isFriendRequest) { if (!this.isGroup && isFriendRequest && !isSessionRequest) {
const conversation = ConversationController.get(destination); const conversation = ConversationController.get(destination);
if (conversation) { if (conversation) {
// Redundant for primary device but marks secondary devices as pending // Redundant for primary device but marks secondary devices as pending

@ -1041,64 +1041,6 @@ MessageSender.prototype = {
silent, silent,
options options
).catch(logError('resetSession/sendToContact error:')); ).catch(logError('resetSession/sendToContact error:'));
/*
const deleteAllSessions = targetNumber =>
textsecure.storage.protocol.getDeviceIds(targetNumber).then(deviceIds =>
Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(
targetNumber,
deviceId
);
window.log.info('deleting sessions for', address.toString());
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.deleteAllSessionsForDevice();
})
)
);
const sendToContactPromise = deleteAllSessions(number)
.catch(logError('resetSession/deleteAllSessions1 error:'))
.then(() => {
window.log.info(
'finished closing local sessions, now sending to contact'
);
return this.sendIndividualProto(
number,
proto,
timestamp,
silent,
options
).catch(logError('resetSession/sendToContact error:'));
})
.then(() =>
deleteAllSessions(number).catch(
logError('resetSession/deleteAllSessions2 error:')
)
);
const myNumber = textsecure.storage.user.getNumber();
// We already sent the reset session to our other devices in the code above!
if (number === myNumber) {
return sendToContactPromise;
}
const buffer = proto.toArrayBuffer();
const sendSyncPromise = this.sendSyncMessage(
buffer,
timestamp,
number,
null,
[],
[],
options
).catch(logError('resetSession/sendSync error:'));
return Promise.all([sendToContact, sendSyncPromise]);
*/
}, },
async sendMessageToGroup( async sendMessageToGroup(

@ -202,7 +202,7 @@
"webpack": "4.4.1" "webpack": "4.4.1"
}, },
"engines": { "engines": {
"node": "10.13.0" "node": "^10.13.0"
}, },
"build": { "build": {
"appId": "com.loki-project.messenger-desktop", "appId": "com.loki-project.messenger-desktop",

@ -802,6 +802,15 @@ label {
} }
} }
.create-group-dialog .session-modal__body {
display: flex;
flex-direction: column;
.friend-selection-list {
width: unset;
}
}
.session-confirm { .session-confirm {
&-wrapper { &-wrapper {
.session-modal__body .session-modal__centered { .session-modal__body .session-modal__centered {

@ -32,7 +32,6 @@ interface Props {
phoneNumber: string; phoneNumber: string;
profileName?: string; profileName?: string;
color: string;
avatarPath?: string; avatarPath?: string;
isVerified: boolean; isVerified: boolean;
@ -88,7 +87,6 @@ interface Props {
onCopyPublicKey: () => void; onCopyPublicKey: () => void;
onUpdateGroup: () => void;
onLeaveGroup: () => void; onLeaveGroup: () => void;
onAddModerators: () => void; onAddModerators: () => void;
onRemoveModerators: () => void; onRemoveModerators: () => void;
@ -206,7 +204,6 @@ export class ConversationHeader extends React.Component<Props> {
public renderAvatar() { public renderAvatar() {
const { const {
avatarPath, avatarPath,
color,
i18n, i18n,
isGroup, isGroup,
isMe, isMe,
@ -223,7 +220,6 @@ export class ConversationHeader extends React.Component<Props> {
<span className="module-conversation-header__avatar"> <span className="module-conversation-header__avatar">
<Avatar <Avatar
avatarPath={avatarPath} avatarPath={avatarPath}
color={color}
conversationType={conversationType} conversationType={conversationType}
i18n={i18n} i18n={i18n}
noteToSelf={isMe} noteToSelf={isMe}
@ -305,7 +301,6 @@ export class ConversationHeader extends React.Component<Props> {
onDeleteMessages, onDeleteMessages,
onDeleteContact, onDeleteContact,
onCopyPublicKey, onCopyPublicKey,
onUpdateGroup,
onLeaveGroup, onLeaveGroup,
onAddModerators, onAddModerators,
onRemoveModerators, onRemoveModerators,
@ -323,9 +318,6 @@ export class ConversationHeader extends React.Component<Props> {
<MenuItem onClick={onCopyPublicKey}>{copyIdLabel}</MenuItem> <MenuItem onClick={onCopyPublicKey}>{copyIdLabel}</MenuItem>
) : null} ) : null}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem> <MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
{isPrivateGroup || amMod ? (
<MenuItem onClick={onUpdateGroup}>{i18n('updateGroup')}</MenuItem>
) : null}
{amMod ? ( {amMod ? (
<MenuItem onClick={onAddModerators}>{i18n('addModerators')}</MenuItem> <MenuItem onClick={onAddModerators}>{i18n('addModerators')}</MenuItem>
) : null} ) : null}

@ -2,7 +2,8 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Contact, MemberList } from './MemberList'; import { Contact, MemberList } from './MemberList';
import { SessionModal } from './../session/SessionModal'; import { SessionModal } from '../session/SessionModal';
import { SessionButton } from '../session/SessionButton';
interface Props { interface Props {
titleText: string; titleText: string;
@ -21,12 +22,11 @@ interface Props {
interface State { interface State {
friendList: Array<Contact>; friendList: Array<Contact>;
groupName: string;
errorDisplayed: boolean; errorDisplayed: boolean;
errorMessage: string; errorMessage: string;
} }
export class UpdateGroupDialog extends React.Component<Props, State> { export class UpdateGroupMembersDialog extends React.Component<Props, State> {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
@ -57,7 +57,6 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
this.state = { this.state = {
friendList: friends, friendList: friends,
groupName: this.props.groupName,
errorDisplayed: false, errorDisplayed: false,
errorMessage: 'placeholder', errorMessage: 'placeholder',
}; };
@ -70,13 +69,7 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
d => d.id d => d.id
); );
if (!this.state.groupName.trim()) { this.props.onSubmit(this.props.groupName, members);
this.onShowError(this.props.i18n('emptyGroupNameError'));
return;
}
this.props.onSubmit(this.state.groupName, members);
this.closeDialog(); this.closeDialog();
} }
@ -111,25 +104,16 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
); );
return ( return (
<SessionModal title={titleText} onClose={() => null} onOk={() => null}> <SessionModal
title={titleText}
// tslint:disable-next-line: no-void-expression
onClose={() => this.closeDialog()}
onOk={() => null}
>
<div className="spacer-md" /> <div className="spacer-md" />
<p className={errorMessageClasses}>{errorMsg}</p> <p className={errorMessageClasses}>{errorMsg}</p>
<div className="spacer-md" /> <div className="spacer-md" />
<input
type="text"
id="group-name"
className="group-name"
placeholder={this.props.i18n('groupNamePlaceholder')}
value={this.state.groupName}
disabled={!this.props.isAdmin}
onChange={this.onGroupNameChanged}
tabIndex={0}
required={true}
aria-required={true}
autoFocus={true}
/>
<div className="friend-selection-list"> <div className="friend-selection-list">
<MemberList <MemberList
members={this.state.friendList} members={this.state.friendList}
@ -139,15 +123,13 @@ export class UpdateGroupDialog extends React.Component<Props, State> {
/> />
</div> </div>
<p className={noFriendsClasses}>{`(${this.props.i18n( <p className={noFriendsClasses}>{`(${this.props.i18n(
'noFriendsToAdd' 'noMembersInThisGroup'
)})`}</p> )})`}</p>
<div className="buttons">
<button className="cancel" tabIndex={0} onClick={this.closeDialog}> <div className="session-modal__button-group">
{cancelText} <SessionButton text={okText} onClick={this.onClickOK} />
</button>
<button className="ok" tabIndex={0} onClick={this.onClickOK}> <SessionButton text={cancelText} onClick={this.closeDialog} />
{okText}
</button>
</div> </div>
</SessionModal> </SessionModal>
); );

@ -0,0 +1,148 @@
import React from 'react';
import classNames from 'classnames';
import { SessionModal } from '../session/SessionModal';
import { SessionButton } from '../session/SessionButton';
interface Props {
titleText: string;
groupName: string;
okText: string;
cancelText: string;
isAdmin: boolean;
i18n: any;
onSubmit: any;
onClose: any;
existingMembers: Array<String>;
}
interface State {
groupName: string;
errorDisplayed: boolean;
errorMessage: string;
}
export class UpdateGroupNameDialog extends React.Component<Props, State> {
constructor(props: any) {
super(props);
this.onClickOK = this.onClickOK.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.closeDialog = this.closeDialog.bind(this);
this.onGroupNameChanged = this.onGroupNameChanged.bind(this);
this.state = {
groupName: this.props.groupName,
errorDisplayed: false,
errorMessage: 'placeholder',
};
window.addEventListener('keyup', this.onKeyUp);
}
public onClickOK() {
if (!this.state.groupName.trim()) {
this.onShowError(this.props.i18n('emptyGroupNameError'));
return;
}
this.props.onSubmit(this.state.groupName, this.props.existingMembers);
this.closeDialog();
}
public render() {
const okText = this.props.okText;
const cancelText = this.props.cancelText;
let titleText;
titleText = `${this.props.titleText}`;
const errorMsg = this.state.errorMessage;
const errorMessageClasses = classNames(
'error-message',
this.state.errorDisplayed ? 'error-shown' : 'error-faded'
);
return (
<SessionModal
title={titleText}
// tslint:disable-next-line: no-void-expression
onClose={() => this.closeDialog()}
onOk={() => null}
>
<div className="spacer-md" />
<p className={errorMessageClasses}>{errorMsg}</p>
<div className="spacer-md" />
<input
type="text"
className="profile-name-input"
value={this.state.groupName}
placeholder={this.props.i18n('groupNamePlaceholder')}
onChange={this.onGroupNameChanged}
tabIndex={0}
required={true}
aria-required={true}
autoFocus={true}
disabled={!this.props.isAdmin}
/>
<div className="session-modal__button-group">
<SessionButton text={okText} onClick={this.onClickOK} />
<SessionButton text={cancelText} onClick={this.closeDialog} />
</div>
</SessionModal>
);
}
private onShowError(msg: string) {
if (this.state.errorDisplayed) {
return;
}
this.setState({
errorDisplayed: true,
errorMessage: msg,
});
setTimeout(() => {
this.setState({
errorDisplayed: false,
});
}, 3000);
}
private onKeyUp(event: any) {
switch (event.key) {
case 'Enter':
this.onClickOK();
break;
case 'Esc':
case 'Escape':
this.closeDialog();
break;
default:
}
}
private closeDialog() {
window.removeEventListener('keyup', this.onKeyUp);
this.props.onClose();
}
private onGroupNameChanged(event: any) {
event.persist();
this.setState(state => {
return {
...state,
groupName: event.target.value,
};
});
}
}

@ -19,15 +19,18 @@ interface Props {
avatarPath: string; avatarPath: string;
timerOptions: Array<TimerOption>; timerOptions: Array<TimerOption>;
isPublic: boolean; isPublic: boolean;
isAdmin: boolean;
onGoBack: () => void; onGoBack: () => void;
onInviteFriends: () => void; onInviteFriends: () => void;
onLeaveGroup: () => void; onLeaveGroup: () => void;
onUpdateGroupName: () => void;
onUpdateGroupMembers: () => void;
onShowLightBox: (options: any) => void; onShowLightBox: (options: any) => void;
onSetDisappearingMessages: (seconds: number) => void; onSetDisappearingMessages: (seconds: number) => void;
} }
export class SessionChannelSettings extends React.Component<Props, any> { export class SessionGroupSettings extends React.Component<Props, any> {
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
@ -207,6 +210,7 @@ export class SessionChannelSettings extends React.Component<Props, any> {
timerOptions, timerOptions,
onLeaveGroup, onLeaveGroup,
isPublic, isPublic,
isAdmin,
} = this.props; } = this.props;
const { documents, media, onItemClick } = this.state; const { documents, media, onItemClick } = this.state;
const showMemberCount = !!(memberCount && memberCount > 0); const showMemberCount = !!(memberCount && memberCount > 0);
@ -231,7 +235,7 @@ export class SessionChannelSettings extends React.Component<Props, any> {
{showMemberCount && ( {showMemberCount && (
<> <>
<div className="spacer-lg" /> <div className="spacer-lg" />
<div className="text-subtle"> <div role="button" className="text-subtle">
{window.i18n('members', memberCount)} {window.i18n('members', memberCount)}
</div> </div>
<div className="spacer-lg" /> <div className="spacer-lg" />
@ -241,7 +245,26 @@ export class SessionChannelSettings extends React.Component<Props, any> {
className="description" className="description"
placeholder={window.i18n('description')} placeholder={window.i18n('description')}
/> />
{!isPublic && (
<>
{isAdmin && (
<div
className="group-settings-item"
role="button"
onClick={this.props.onUpdateGroupName}
>
{window.i18n('editGroupName')}
</div>
)}
<div
className="group-settings-item"
role="button"
onClick={this.props.onUpdateGroupMembers}
>
{window.i18n('showMembers')}
</div>
</>
)}
<div className="group-settings-item"> <div className="group-settings-item">
{window.i18n('notifications')} {window.i18n('notifications')}
</div> </div>
@ -269,8 +292,16 @@ export class SessionChannelSettings extends React.Component<Props, any> {
} }
private renderHeader() { private renderHeader() {
const { id, onGoBack, onInviteFriends, avatarPath } = this.props; const {
const shouldShowInviteFriends = !this.props.isPublic; id,
onGoBack,
onInviteFriends,
avatarPath,
isAdmin,
isPublic,
} = this.props;
const showInviteFriends = isPublic || isAdmin;
return ( return (
<div className="group-settings-header"> <div className="group-settings-header">
@ -286,9 +317,8 @@ export class SessionChannelSettings extends React.Component<Props, any> {
conversationType="group" conversationType="group"
size={80} size={80}
/> />
<div className="invite-friends-container"> <div className="invite-friends-container">
{shouldShowInviteFriends && ( {showInviteFriends && (
<SessionIconButton <SessionIconButton
iconType={SessionIconType.AddUser} iconType={SessionIconType.AddUser}
iconSize={SessionIconSize.Medium} iconSize={SessionIconSize.Medium}
Loading…
Cancel
Save