Merge pull request #50 from Mikunj/fix/friend-request

Friend request fixes
pull/55/head
sachaaaaa 7 years ago committed by GitHub
commit dbdd52b4eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1590,6 +1590,10 @@
"message": "Friend request declined", "message": "Friend request declined",
"description": "Shown in the conversation history when the user declines a friend request" "description": "Shown in the conversation history when the user declines a friend request"
}, },
"friendRequestExpired": {
"message": "Friend request expired",
"description": "Shown in the conversation history when the users friend request expires"
},
"friendRequestNotificationTitle": { "friendRequestNotificationTitle": {
"message": "Friend request", "message": "Friend request",
"description": "Shown in a notification title when receiving a friend request" "description": "Shown in a notification title when receiving a friend request"

@ -572,12 +572,6 @@
} }
}); });
Whisper.events.on('showFriendRequest', friendRequest => {
if (appView) {
appView.showFriendRequest(friendRequest);
}
});
Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => { Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => {
try { try {
const conversation = ConversationController.get(pubKey); const conversation = ConversationController.get(pubKey);
@ -1261,7 +1255,7 @@
async function initIncomingMessage(data, options = {}) { async function initIncomingMessage(data, options = {}) {
const { isError } = options; const { isError } = options;
const message = new Whisper.Message({ let messageData = {
source: data.source, source: data.source,
sourceDevice: data.sourceDevice, sourceDevice: data.sourceDevice,
sent_at: data.timestamp, sent_at: data.timestamp,
@ -1270,7 +1264,19 @@
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived, unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
type: 'incoming', type: 'incoming',
unread: 1, unread: 1,
}); preKeyBundle: data.preKeyBundle || null,
};
if (data.type === 'friend-request') {
messageData = {
...messageData,
type: 'friend-request',
friendStatus: 'pending',
direction: 'incoming',
}
}
const message = new Whisper.Message(messageData);
// If we don't return early here, we can get into infinite error loops. So, no // If we don't return early here, we can get into infinite error loops. So, no
// delivery receipts for sealed sender errors. // delivery receipts for sealed sender errors.

@ -52,14 +52,35 @@
'blue_grey', 'blue_grey',
]; ];
/**
* A few key things that need to be known in this is the difference
* between isFriend() and isKeyExchangeCompleted().
*
* `isFriend` returns whether we have accepted the other user as a friend.
* - This is explicilty stored as a state in the conversation
*
* `isKeyExchangeCompleted` return whether we know for certain
* that both of our preKeyBundles have been exchanged.
* - This will be set when we receive a valid CIPHER or
* PREKEY_BUNDLE message from the other user.
* * Valid meaning we can decypher the message using the preKeys provided
* or the keys we have stored.
*
* `isFriend` will determine whether we should send a FRIEND_REQUEST message.
*
* `isKeyExchangeCompleted` will determine whether we keep
* sending preKeyBundle to the other user.
*/
Whisper.Conversation = Backbone.Model.extend({ Whisper.Conversation = Backbone.Model.extend({
storeName: 'conversations', storeName: 'conversations',
defaults() { defaults() {
return { return {
unreadCount: 0, unreadCount: 0,
verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT, verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT,
isFriend: false,
keyExchangeCompleted: false, keyExchangeCompleted: false,
friendRequestStatus: { allowSending: true, unlockTimestamp: null }, unlockTimestamp: null, // Timestamp used for expiring friend requests.
}; };
}, },
@ -115,7 +136,7 @@
this.updateLastMessage this.updateLastMessage
); );
this.on('newmessage', this.updateLastMessage); this.on('newmessage', this.onNewMessage);
this.on('change:profileKey', this.onChangeProfileKey); this.on('change:profileKey', this.onChangeProfileKey);
// Listening for out-of-band data updates // Listening for out-of-band data updates
@ -125,10 +146,6 @@
this.on('expiration-change', this.updateAndMerge); this.on('expiration-change', this.updateAndMerge);
this.on('expired', this.onExpired); this.on('expired', this.onExpired);
setTimeout(() => {
this.setFriendRequestTimer();
}, 0);
const sealedSender = this.get('sealedSender'); const sealedSender = this.get('sealedSender');
if (sealedSender === undefined) { if (sealedSender === undefined) {
this.set({ sealedSender: SEALED_SENDER.UNKNOWN }); this.set({ sealedSender: SEALED_SENDER.UNKNOWN });
@ -141,6 +158,7 @@
this.unset('lastMessageStatus'); this.unset('lastMessageStatus');
this.updateTextInputState(); this.updateTextInputState();
this.setFriendRequestExpiryTimeout();
}, },
isMe() { isMe() {
@ -209,6 +227,7 @@
await this.inProgressFetch; await this.inProgressFetch;
removeMessage(); removeMessage();
}, },
async onCalculatingPoW(pubKey, timestamp) { async onCalculatingPoW(pubKey, timestamp) {
if (this.id !== pubKey) return; if (this.id !== pubKey) return;
@ -222,7 +241,6 @@
if (setToExpire) model.setToExpire(); if (setToExpire) model.setToExpire();
return model; return model;
}, },
format() { format() {
const { format } = PhoneNumber; const { format } = PhoneNumber;
const regionCode = storage.get('regionCode'); const regionCode = storage.get('regionCode');
@ -457,48 +475,35 @@
return this.get('keyExchangeCompleted') || false; return this.get('keyExchangeCompleted') || false;
}, },
getFriendRequestStatus() { async setKeyExchangeCompleted(value) {
return this.get('friendRequestStatus'); // Only update the value if it's different
}, if (this.get('keyExchangeCompleted') === value) return;
waitingForFriendRequestApproval() {
const friendRequestStatus = this.getFriendRequestStatus();
if (!friendRequestStatus) {
return false;
}
return !friendRequestStatus.allowSending;
},
setFriendRequestTimer() {
const friendRequestStatus = this.getFriendRequestStatus();
if (friendRequestStatus) {
if (!friendRequestStatus.allowSending) {
const delay = Math.max(
friendRequestStatus.unlockTimestamp - Date.now(),
0
);
setTimeout(() => {
this.onFriendRequestTimedOut();
}, delay);
}
}
},
async onFriendRequestAccepted({ updateUnread }) {
// Make sure we don't keep incrementing the unread count
const unreadCount = !updateUnread || this.isKeyExchangeCompleted()
? {}
: { unreadCount: this.get('unreadCount') + 1 };
this.set({
friendRequestStatus: null,
keyExchangeCompleted: true,
...unreadCount,
});
this.set({ keyExchangeCompleted: value });
await window.Signal.Data.updateConversation(this.id, this.attributes, { await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation, Conversation: Whisper.Conversation,
}); });
},
async waitingForFriendRequestApproval() {
// Check if we have an incoming friend request
// Or any successful outgoing ones
const incoming = await this.getPendingFriendRequests('incoming');
const outgoing = await this.getPendingFriendRequests('outgoing');
const successfulOutgoing = outgoing.filter(o => !o.hasErrors());
return (incoming.length > 0 || successfulOutgoing.length > 0);
},
isFriend() {
return this.get('isFriend');
},
// Update any pending friend requests for the current user
async updateFriendRequestUI() {
// Enable the text inputs early // Enable the text inputs early
this.updateTextInputState(); this.updateTextInputState();
// We only update our friend requests if we have the user as a friend
if (!this.isFriend()) return;
// Update any pending outgoing messages // Update any pending outgoing messages
const pending = await this.getPendingFriendRequests('outgoing'); const pending = await this.getPendingFriendRequests('outgoing');
await Promise.all( await Promise.all(
@ -513,50 +518,82 @@
}) })
); );
// Update our local state
await this.updatePendingFriendRequests(); await this.updatePendingFriendRequests();
this.notifyFriendRequest(this.id, 'accepted') // Send the notification if we had an outgoing friend request
if (pending.length > 0)
this.notifyFriendRequest(this.id, 'accepted')
}, },
async onFriendRequestTimedOut() { async onFriendRequestAccepted() {
this.updateTextInputState(); if (!this.isFriend()) {
this.set({ isFriend: true });
const friendRequestStatus = this.getFriendRequestStatus();
if (friendRequestStatus) {
friendRequestStatus.allowSending = true;
this.set({ friendRequestStatus });
await window.Signal.Data.updateConversation(this.id, this.attributes, { await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation, Conversation: Whisper.Conversation,
}); });
} }
await this.updateFriendRequestUI();
}, },
async onFriendRequestSent() { async onFriendRequestTimeout() {
// Don't bother setting the friend request if we have already exchanged keys // Unset the timer
if (this.isKeyExchangeCompleted()) return; if (this.unlockTimer)
clearTimeout(this.unlockTimer);
const friendRequestLockDuration = 72; // hours this.unlockTimer = null;
let friendRequestStatus = this.getFriendRequestStatus(); // Set the unlock timestamp to null
if (!friendRequestStatus) { if (this.get('unlockTimestamp')) {
friendRequestStatus = {}; this.set({ unlockTimestamp: null });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
} }
friendRequestStatus.allowSending = false; // Change any pending outgoing friend requests to expired
const delayMs = 60 * 60 * 1000 * friendRequestLockDuration; const outgoing = await this.getPendingFriendRequests('outgoing');
friendRequestStatus.unlockTimestamp = Date.now() + delayMs; await Promise.all(
outgoing.map(async request => {
if (request.hasErrors()) return;
// Update the text input state request.set({ friendStatus: 'expired' });
this.updateTextInputState(); await window.Signal.Data.saveMessage(request.attributes, {
Message: Whisper.Message,
});
this.trigger('updateMessage', request);
})
);
this.set({ friendRequestStatus }); // Update the UI
await this.updatePendingFriendRequests();
await this.updateFriendRequestUI();
},
async onFriendRequestSent() {
// Check if we need to set the friend request expiry
const unlockTimestamp = this.get('unlockTimestamp');
if (!this.isFriend() && !unlockTimestamp) {
// Expire the messages after 72 hours
const hourLockDuration = 72;
const ms = 60 * 60 * 1000 * hourLockDuration;
this.set({ unlockTimestamp: Date.now() + ms });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
await window.Signal.Data.updateConversation(this.id, this.attributes, { this.setFriendRequestExpiryTimeout();
Conversation: Whisper.Conversation, }
});
setTimeout(() => { this.updateFriendRequestUI();
this.onFriendRequestTimedOut(); },
}, delayMs); setFriendRequestExpiryTimeout() {
const unlockTimestamp = this.get('unlockTimestamp');
if (unlockTimestamp && !this.unlockTimer) {
const delta = Math.max(unlockTimestamp - Date.now(), 0);
this.unlockTimer = setTimeout(() => {
this.onFriendRequestTimeout();
}, delta);
}
}, },
isUnverified() { isUnverified() {
if (this.isPrivate()) { if (this.isPrivate()) {
@ -708,91 +745,6 @@
existing.trigger('destroy'); existing.trigger('destroy');
} }
}, },
// This will add a message which will allow the user to reply to a friend request
async addFriendRequest(body, options = {}) {
const _options = {
friendStatus: 'pending',
direction: 'incoming',
preKeyBundle: null,
timestamp: null,
source: null,
sourceDevice: null,
received_at: null,
...options,
};
if (this.isMe()) {
window.log.info(
'refusing to send friend request to ourselves'
);
return;
}
const timestamp = _options.timestamp || this.get('timestamp') || Date.now();
window.log.info(
'adding friend request for',
this.ourNumber,
this.idForLogging(),
timestamp
);
this.lastMessageStatus = 'sending';
this.set({
active_at: Date.now(),
timestamp: Date.now(),
unreadCount: this.get('unreadCount') + 1,
});
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
// If we need to add new incoming friend requests
// Then we need to make sure we remove any pending requests that we may have
// This is to ensure that one user cannot spam us with multiple friend requests
if (_options.direction === 'incoming') {
const requests = await this.getPendingFriendRequests('incoming');
// Delete the old message if it's pending
await Promise.all(requests.map(request => this._removeMessage(request.id)));
// Trigger an update if we removed messages
if (requests.length > 0)
this.trigger('change');
}
// Add the new message
// eslint-disable-next-line camelcase
const received_at = _options.received_at || Date.now();
const message = {
conversationId: this.id,
type: 'friend-request',
sent_at: timestamp,
received_at,
unread: 1,
from: this.id,
to: this.ourNumber,
friendStatus: _options.friendStatus,
direction: _options.direction,
body,
preKeyBundle: _options.preKeyBundle,
source: _options.source,
sourceDevice: _options.sourceDevice,
};
const id = await window.Signal.Data.saveMessage(message, {
Message: Whisper.Message,
});
const whisperMessage = new Whisper.Message({
...message,
id,
});
this.trigger('newmessage', whisperMessage);
this.notify(whisperMessage);
},
async addVerifiedChange(verifiedChangeId, verified, providedOptions) { async addVerifiedChange(verifiedChangeId, verified, providedOptions) {
const options = providedOptions || {}; const options = providedOptions || {};
_.defaults(options, { local: true }); _.defaults(options, { local: true });
@ -1025,8 +977,8 @@
let messageWithSchema = null; let messageWithSchema = null;
// If we have exchanged keys then let the user send the message normally // If we are a friend then let the user send the message normally
if (this.isKeyExchangeCompleted()) { if (this.isFriend()) {
messageWithSchema = await upgradeMessageSchema({ messageWithSchema = await upgradeMessageSchema({
type: 'outgoing', type: 'outgoing',
body, body,
@ -1156,15 +1108,10 @@
}, },
async updateTextInputState() { async updateTextInputState() {
// Check if we need to disable the text field // Check if we need to disable the text field
if (!this.isKeyExchangeCompleted()) { if (!this.isFriend()) {
// Check if we have an incoming friend request // Disable the input if we're waiting for friend request approval
// Or any successful outgoing ones const waiting = await this.waitingForFriendRequestApproval();
const incoming = await this.getPendingFriendRequests('incoming'); if (waiting) {
const outgoing = await this.getPendingFriendRequests('outgoing');
const successfulOutgoing = outgoing.filter(o => !o.hasErrors());
// Disable the input
if (incoming.length > 0 || successfulOutgoing.length > 0) {
this.trigger('disable:input', true); this.trigger('disable:input', true);
this.trigger('change:placeholder', 'disabled'); this.trigger('change:placeholder', 'disabled');
return; return;
@ -1327,6 +1274,38 @@
}, },
}; };
}, },
async onNewMessage(message) {
if (message.get('type') === 'friend-request' && message.get('direction') === 'incoming') {
// We need to make sure we remove any pending requests that we may have
// This is to ensure that one user cannot spam us with multiple friend requests.
const incoming = await this.getPendingFriendRequests('incoming');
// Delete the old messages if it's pending
await Promise.all(
incoming
.filter(i => i.id !== message.id)
.map(request => this._removeMessage(request.id))
);
// If we have an outgoing friend request then
// we auto accept the incoming friend request
const outgoing = await this.getPendingFriendRequests('outgoing');
if (outgoing.length > 0) {
const current = this.messageCollection.find(i => i.id === message.id);
if (current) {
await current.acceptFriendRequest();
} else {
window.log.debug('onNewMessage: Failed to find incoming friend request');
}
}
// Trigger an update if we removed or updated messages
if (outgoing.length > 0 || incoming.length > 0)
this.trigger('change');
}
return this.updateLastMessage();
},
async updateLastMessage() { async updateLastMessage() {
if (!this.id) { if (!this.id) {
return; return;

@ -297,34 +297,42 @@
// It doesn't need anything right now! // It doesn't need anything right now!
return {}; return {};
}, },
getPropsForFriendRequest() {
const friendStatus = this.get('friendStatus') || 'pending'; async acceptFriendRequest() {
const direction = this.get('direction') || 'incoming'; if (this.get('friendStatus') !== 'pending') return;
const conversation = this.getConversation(); const conversation = this.getConversation();
const onAccept = async () => { this.set({ friendStatus: 'accepted' });
this.set({ friendStatus: 'accepted' }); await window.Signal.Data.saveMessage(this.attributes, {
await window.Signal.Data.saveMessage(this.attributes, { Message: Whisper.Message,
Message: Whisper.Message, });
});
window.Whisper.events.trigger('friendRequestUpdated', { window.Whisper.events.trigger('friendRequestUpdated', {
pubKey: conversation.id, pubKey: conversation.id,
...this.attributes, ...this.attributes,
}); });
}; },
async declineFriendRequest() {
if (this.get('friendStatus') !== 'pending') return;
const conversation = this.getConversation();
const onDecline = async () => { this.set({ friendStatus: 'declined' });
this.set({ friendStatus: 'declined' }); await window.Signal.Data.saveMessage(this.attributes, {
await window.Signal.Data.saveMessage(this.attributes, { Message: Whisper.Message,
Message: Whisper.Message, });
});
window.Whisper.events.trigger('friendRequestUpdated', { window.Whisper.events.trigger('friendRequestUpdated', {
pubKey: conversation.id, pubKey: conversation.id,
...this.attributes, ...this.attributes,
}); });
}; },
getPropsForFriendRequest() {
const friendStatus = this.get('friendStatus') || 'pending';
const direction = this.get('direction') || 'incoming';
const conversation = this.getConversation();
const onAccept = () => this.acceptFriendRequest();
const onDecline = () => this.declineFriendRequest()
const onDeleteConversation = async () => { const onDeleteConversation = async () => {
// Delete the whole conversation // Delete the whole conversation
@ -1240,6 +1248,7 @@
quote: dataMessage.quote, quote: dataMessage.quote,
schemaVersion: dataMessage.schemaVersion, schemaVersion: dataMessage.schemaVersion,
}); });
if (type === 'outgoing') { if (type === 'outgoing') {
const receipts = Whisper.DeliveryReceipts.forMessage( const receipts = Whisper.DeliveryReceipts.forMessage(
conversation, conversation,
@ -1303,7 +1312,7 @@
); );
} }
} }
if (type === 'incoming') { if (type === 'incoming' || type === 'friend-request') {
const readSync = Whisper.ReadSyncs.forMessage(message); const readSync = Whisper.ReadSyncs.forMessage(message);
if (readSync) { if (readSync) {
if ( if (

@ -178,15 +178,5 @@
}); });
} }
}, },
async showFriendRequest({ pubKey, message, preKeyBundle, options }) {
const controller = window.ConversationController;
const conversation = await controller.getOrCreateAndWait(pubKey, 'private');
if (conversation) {
conversation.addFriendRequest(message, {
preKeyBundle: preKeyBundle || null,
...options,
});
}
},
}); });
})(); })();

@ -70,16 +70,9 @@
}, },
template: $('#conversation').html(), template: $('#conversation').html(),
render_attributes() { render_attributes() {
let sendMessagePlaceholder = 'sendMessageFriendRequest';
const sendDisabled = this.model.waitingForFriendRequestApproval();
if (sendDisabled) {
sendMessagePlaceholder = 'sendMessageDisabled';
} else if (this.model.getFriendRequestStatus() === null) {
sendMessagePlaceholder = 'sendMessage';
}
return { return {
'disable-inputs': sendDisabled, 'disable-inputs': false,
'send-message': i18n(sendMessagePlaceholder), 'send-message': i18n('sendMessage'),
'android-length-warning': i18n('androidMessageLengthWarning'), 'android-length-warning': i18n('androidMessageLengthWarning'),
}; };
}, },
@ -145,6 +138,8 @@
this.render(); this.render();
this.model.updateTextInputState();
this.loadingScreen = new Whisper.ConversationLoadingScreen(); this.loadingScreen = new Whisper.ConversationLoadingScreen();
this.loadingScreen.render(); this.loadingScreen.render();
this.loadingScreen.$el.prependTo(this.$('.discussion-container')); this.loadingScreen.$el.prependTo(this.$('.discussion-container'));
@ -249,6 +244,8 @@
this.$('.send-message').blur(this.unfocusBottomBar.bind(this)); this.$('.send-message').blur(this.unfocusBottomBar.bind(this));
this.$emojiPanelContainer = this.$('.emoji-panel-container'); this.$emojiPanelContainer = this.$('.emoji-panel-container');
this.model.updateFriendRequestUI();
}, },
events: { events: {

@ -431,6 +431,7 @@ MessageReceiver.prototype.extend({
envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice; envelope.sourceDevice = envelope.sourceDevice || item.sourceDevice;
envelope.serverTimestamp = envelope.serverTimestamp =
envelope.serverTimestamp || item.serverTimestamp; envelope.serverTimestamp || item.serverTimestamp;
envelope.preKeyBundleMessage = envelope.preKeyBundleMessage || item.preKeyBundleMessage;
const { decrypted } = item; const { decrypted } = item;
if (decrypted) { if (decrypted) {
@ -447,6 +448,13 @@ MessageReceiver.prototype.extend({
payloadPlaintext payloadPlaintext
); );
} }
// Convert preKeys to array buffer
if (typeof envelope.preKeyBundleMessage === 'string') {
envelope.preKeyBundleMessage = await MessageReceiver.stringToArrayBuffer(
envelope.preKeyBundleMessage
);
}
this.queueDecryptedEnvelope(envelope, payloadPlaintext); this.queueDecryptedEnvelope(envelope, payloadPlaintext);
} else { } else {
this.queueEnvelope(envelope); this.queueEnvelope(envelope);
@ -672,7 +680,7 @@ MessageReceiver.prototype.extend({
return plaintext; return plaintext;
}, },
decrypt(envelope, ciphertext) { async decrypt(envelope, ciphertext) {
const { serverTrustRoot } = this; const { serverTrustRoot } = this;
let promise; let promise;
@ -699,6 +707,31 @@ MessageReceiver.prototype.extend({
textsecure.storage.protocol textsecure.storage.protocol
); );
const fallBackSessionCipher = new libloki.FallBackSessionCipher(
address
);
// Check if we have preKey bundles to decrypt
if (envelope.preKeyBundleMessage) {
const decryptedText = await fallBackSessionCipher.decrypt(envelope.preKeyBundleMessage.toArrayBuffer());
const unpadded = await this.unpad(decryptedText);
const decodedProto = textsecure.protobuf.PreKeyBundleMessage.decode(unpadded);
const decodedBundle = this.decodePreKeyBundleMessage(decodedProto);
// eslint-disable-next-line no-param-reassign
envelope.preKeyBundleMessage = decodedBundle;
// Save the preKey bundle if this is not a friend request.
// We don't automatically save on a friend request because
// we only want to save the preKeys when we click the accept button.
if (envelope.type !== textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) {
await this.handlePreKeyBundleMessage(
envelope.source,
envelope.preKeyBundleMessage
);
}
}
const me = { const me = {
number: ourNumber, number: ourNumber,
deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10), deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10),
@ -712,9 +745,6 @@ MessageReceiver.prototype.extend({
break; break;
case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: { case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: {
window.log.info('friend-request message from ', envelope.source); window.log.info('friend-request message from ', envelope.source);
const fallBackSessionCipher = new libloki.FallBackSessionCipher(
address
);
promise = fallBackSessionCipher.decrypt(ciphertext.toArrayBuffer()) promise = fallBackSessionCipher.decrypt(ciphertext.toArrayBuffer())
.then(this.unpad); .then(this.unpad);
break; break;
@ -894,7 +924,10 @@ MessageReceiver.prototype.extend({
}) })
); );
}, },
handleDataMessage(envelope, msg) { async handleFriendRequestMessage(envelope, msg) {
return this.handleDataMessage(envelope, msg, 'friend-request');
},
handleDataMessage(envelope, msg, type = 'data') {
window.log.info('data message from', this.getEnvelopeId(envelope)); window.log.info('data message from', this.getEnvelopeId(envelope));
let p = Promise.resolve(); let p = Promise.resolve();
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
@ -911,6 +944,13 @@ MessageReceiver.prototype.extend({
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
); );
if (type === 'friend-request' && isMe) {
window.log.info(
'refusing to add a friend request to ourselves'
);
throw new Error('Cannot add a friend request for ourselves!')
}
if (groupId && isBlocked && !(isMe && isLeavingGroup)) { if (groupId && isBlocked && !(isMe && isLeavingGroup)) {
window.log.warn( window.log.warn(
`Message ${this.getEnvelopeId( `Message ${this.getEnvelopeId(
@ -923,12 +963,14 @@ MessageReceiver.prototype.extend({
const ev = new Event('message'); const ev = new Event('message');
ev.confirm = this.removeFromCache.bind(this, envelope); ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = { ev.data = {
type,
source: envelope.source, source: envelope.source,
sourceDevice: envelope.sourceDevice, sourceDevice: envelope.sourceDevice,
timestamp: envelope.timestamp.toNumber(), timestamp: envelope.timestamp.toNumber(),
receivedAt: envelope.receivedAt, receivedAt: envelope.receivedAt,
unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived, unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived,
message, message,
preKeyBundle: envelope.preKeyBundleMessage || null,
}; };
return this.dispatchAndWait(ev); return this.dispatchAndWait(ev);
}) })
@ -956,19 +998,6 @@ MessageReceiver.prototype.extend({
return this.innerHandleContentMessage(envelope, plaintext); return this.innerHandleContentMessage(envelope, plaintext);
}); });
}, },
promptUserToAcceptFriendRequest(envelope, message, preKeyBundleMessage) {
window.Whisper.events.trigger('showFriendRequest', {
pubKey: envelope.source,
message,
preKeyBundle: this.decodePreKeyBundleMessage(preKeyBundleMessage),
options: {
source: envelope.source,
sourceDevice: envelope.sourceDevice,
timestamp: envelope.timestamp.toNumber(),
receivedAt: envelope.receivedAt,
},
});
},
// A handler function for when a friend request is accepted or declined // A handler function for when a friend request is accepted or declined
async onFriendRequestUpdate(pubKey, message) { async onFriendRequestUpdate(pubKey, message) {
if (!message || !message.direction || !message.friendStatus) return; if (!message || !message.direction || !message.friendStatus) return;
@ -977,14 +1006,12 @@ MessageReceiver.prototype.extend({
const conversation = window.ConversationController.get(pubKey); const conversation = window.ConversationController.get(pubKey);
if (conversation) { if (conversation) {
// Update the conversation friend request indicator // Update the conversation friend request indicator
conversation.updatePendingFriendRequests(); await conversation.updatePendingFriendRequests();
conversation.updateTextInputState(); await conversation.updateTextInputState();
} }
// Send our own prekeys as a response // If we accepted an incoming friend request then save the preKeyBundle
if (message.direction === 'incoming' && message.friendStatus === 'accepted') { if (message.direction === 'incoming' && message.friendStatus === 'accepted') {
libloki.sendEmptyMessageWithPreKeys(pubKey);
// Register the preKeys used for communication // Register the preKeys used for communication
if (message.preKeyBundle) { if (message.preKeyBundle) {
await this.handlePreKeyBundleMessage( await this.handlePreKeyBundleMessage(
@ -993,60 +1020,41 @@ MessageReceiver.prototype.extend({
); );
} }
await conversation.onFriendRequestAccepted({ updateUnread: false }); // Accept the friend request
if (conversation) {
await conversation.onFriendRequestAccepted();
}
// Send a reply back
libloki.sendEmptyMessageWithPreKeys(pubKey);
} }
window.log.info(`Friend request for ${pubKey} was ${message.friendStatus}`, message); window.log.info(`Friend request for ${pubKey} was ${message.friendStatus}`, message);
}, },
async innerHandleContentMessage(envelope, plaintext) { async innerHandleContentMessage(envelope, plaintext) {
const content = textsecure.protobuf.Content.decode(plaintext); const content = textsecure.protobuf.Content.decode(plaintext);
if (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) { let conversation;
let conversation; try {
try { conversation = window.ConversationController.get(envelope.source);
conversation = window.ConversationController.get(envelope.source); } catch (e) {
} catch (e) { window.log.info('Error getting conversation: ', envelope.source);
throw new Error('Error getting conversation for message.') }
}
// only prompt friend request if there is no conversation yet if (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) {
if (!conversation) { return this.handleFriendRequestMessage(envelope, content.dataMessage);
this.promptUserToAcceptFriendRequest( } else if (
envelope, envelope.type === textsecure.protobuf.Envelope.Type.CIPHERTEXT ||
content.dataMessage.body, // We also need to check for PREKEY_BUNDLE aswell if the session hasn't started.
content.preKeyBundleMessage // ref: libsignal-protocol.js:36120
); envelope.type === textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE
} else { ) {
const keyExchangeComplete = conversation.isKeyExchangeCompleted(); // We know for sure that keys are exchanged
if (conversation) {
// Check here if we received preKeys from the other user await conversation.setKeyExchangeCompleted(true);
// We are certain that other user accepted the friend request IF:
// - The message has a preKeyBundleMessage
// - We have an outgoing friend request that is pending
// The second check is crucial because it makes sure we don't save the preKeys of
// the incoming friend request (which is saved only when we press accept)
if (!keyExchangeComplete && content.preKeyBundleMessage) {
// Check for any outgoing friend requests
const pending = await conversation.getPendingFriendRequests('outgoing');
const successful = pending.filter(p => !p.hasErrors());
// Save the key only if we have an outgoing friend request
const savePreKey = (successful.length > 0);
// Save the pre key
if (savePreKey) {
await this.handlePreKeyBundleMessage(
envelope.source,
this.decodePreKeyBundleMessage(content.preKeyBundleMessage)
);
// Update the conversation // TODO: We should probably set this based on the PKB type
await conversation.onFriendRequestAccepted({ updateUnread: true }); await conversation.onFriendRequestAccepted();
}
}
} }
// Exit early since the friend request reply will be a regular empty message
return null;
} }
if (content.syncMessage) { if (content.syncMessage) {
@ -1060,6 +1068,8 @@ MessageReceiver.prototype.extend({
} else if (content.receiptMessage) { } else if (content.receiptMessage) {
return this.handleReceiptMessage(envelope, content.receiptMessage); return this.handleReceiptMessage(envelope, content.receiptMessage);
} }
if (envelope.preKeyBundleMessage) return null;
throw new Error('Unsupported content message'); throw new Error('Unsupported content message');
}, },
handleCallMessage(envelope) { handleCallMessage(envelope) {
@ -1279,6 +1289,8 @@ MessageReceiver.prototype.extend({
}; };
}, },
async handlePreKeyBundleMessage(pubKey, preKeyBundleMessage) { async handlePreKeyBundleMessage(pubKey, preKeyBundleMessage) {
if (!preKeyBundleMessage) return null;
const { const {
preKeyId, preKeyId,
signedKeyId, signedKeyId,

@ -202,25 +202,33 @@ OutgoingMessage.prototype = {
return messagePartCount * 160; return messagePartCount * 160;
}, },
convertMessageToText(message) {
const messageBuffer = message.toArrayBuffer();
const plaintext = new Uint8Array(
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
);
plaintext.set(new Uint8Array(messageBuffer));
plaintext[messageBuffer.byteLength] = 0x80;
return plaintext;
},
getPlaintext() { getPlaintext() {
if (!this.plaintext) { if (!this.plaintext) {
const messageBuffer = this.message.toArrayBuffer(); this.plaintext = this.convertMessageToText(this.message);
this.plaintext = new Uint8Array(
this.getPaddedMessageLength(messageBuffer.byteLength + 1) - 1
);
this.plaintext.set(new Uint8Array(messageBuffer));
this.plaintext[messageBuffer.byteLength] = 0x80;
} }
return this.plaintext; return this.plaintext;
}, },
async wrapInWebsocketMessage(outgoingObject) { async wrapInWebsocketMessage(outgoingObject) {
const preKeyEnvelope = outgoingObject.preKeyBundleMessage ? {
preKeyBundleMessage: outgoingObject.preKeyBundleMessage,
} : {};
const messageEnvelope = new textsecure.protobuf.Envelope({ const messageEnvelope = new textsecure.protobuf.Envelope({
type: outgoingObject.type, type: outgoingObject.type,
source: outgoingObject.ourKey, source: outgoingObject.ourKey,
sourceDevice: outgoingObject.sourceDevice, sourceDevice: outgoingObject.sourceDevice,
timestamp: this.timestamp, timestamp: this.timestamp,
content: outgoingObject.content, content: outgoingObject.content,
...preKeyEnvelope,
}); });
const requestMessage = new textsecure.protobuf.WebSocketRequestMessage({ const requestMessage = new textsecure.protobuf.WebSocketRequestMessage({
id: new Uint8Array(libsignal.crypto.getRandomBytes(1))[0], // random ID for now id: new Uint8Array(libsignal.crypto.getRandomBytes(1))[0], // random ID for now
@ -275,6 +283,18 @@ OutgoingMessage.prototype = {
const address = new libsignal.SignalProtocolAddress(number, deviceId); const address = new libsignal.SignalProtocolAddress(number, deviceId);
const ourKey = textsecure.storage.user.getNumber(); const ourKey = textsecure.storage.user.getNumber();
const options = {}; const options = {};
const fallBackEncryption = new libloki.FallBackSessionCipher(address);
// Check if we need to attach the preKeys
let preKeys = {};
if (this.attachPrekeys) {
// Encrypt them with the fallback
const preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(number);
const textBundle = this.convertMessageToText(preKeyBundleMessage);
const encryptedBundle = await fallBackEncryption.encrypt(textBundle);
preKeys = { preKeyBundleMessage: encryptedBundle.body };
window.log.info('attaching prekeys to outgoing message');
}
// No limit on message keys if we're communicating with our other devices // No limit on message keys if we're communicating with our other devices
if (ourKey === number) { if (ourKey === number) {
@ -283,7 +303,7 @@ OutgoingMessage.prototype = {
let sessionCipher; let sessionCipher;
if (this.fallBackEncryption) { if (this.fallBackEncryption) {
sessionCipher = new libloki.FallBackSessionCipher(address); sessionCipher = fallBackEncryption;
} else { } else {
sessionCipher = new libsignal.SessionCipher( sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
@ -292,26 +312,26 @@ OutgoingMessage.prototype = {
); );
} }
ciphers[address.getDeviceId()] = sessionCipher; ciphers[address.getDeviceId()] = sessionCipher;
return sessionCipher
.encrypt(plaintext) // Encrypt our plain text
.then(ciphertext => { const ciphertext = await sessionCipher.encrypt(plaintext);
if (!this.fallBackEncryption) if (!this.fallBackEncryption) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
ciphertext.body = new Uint8Array( ciphertext.body = new Uint8Array(
dcodeIO.ByteBuffer.wrap( dcodeIO.ByteBuffer.wrap(
ciphertext.body, ciphertext.body,
'binary' 'binary'
).toArrayBuffer() ).toArrayBuffer()
); );
return ciphertext; }
}) return {
.then(ciphertext => ({ type: ciphertext.type, // FallBackSessionCipher sets this to FRIEND_REQUEST
type: ciphertext.type, ourKey,
ourKey, sourceDevice: 1,
sourceDevice: 1, destinationRegistrationId: ciphertext.registrationId,
destinationRegistrationId: ciphertext.registrationId, content: ciphertext.body,
content: ciphertext.body, ...preKeys,
})); };
}) })
) )
.then(async outgoingObjects => { .then(async outgoingObjects => {
@ -441,34 +461,27 @@ OutgoingMessage.prototype = {
return this.getStaleDeviceIdsForNumber(number).then(updateDevices => return this.getStaleDeviceIdsForNumber(number).then(updateDevices =>
this.getKeysForNumber(number, updateDevices) this.getKeysForNumber(number, updateDevices)
.then(async keysFound => { .then(async keysFound => {
let attachPrekeys = false; this.attachPrekeys = false;
if (!keysFound) { if (!keysFound) {
log.info('Fallback encryption enabled'); log.info('Fallback encryption enabled');
this.fallBackEncryption = true; this.fallBackEncryption = true;
attachPrekeys = true; this.attachPrekeys = true;
} else if (conversation) { } else if (conversation) {
try { try {
attachPrekeys = !conversation.isKeyExchangeCompleted(); this.attachPrekeys = !conversation.isKeyExchangeCompleted();
} catch (e) { } catch (e) {
// do nothing // do nothing
} }
} }
if (this.fallBackEncryption && conversation) { if (this.fallBackEncryption && conversation) {
conversation.onFriendRequestSent(); await conversation.onFriendRequestSent();
}
if (attachPrekeys) {
log.info('attaching prekeys to outgoing message');
this.message.preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(
number
);
} }
}) })
.then(this.reloadDevicesAndSend(number, true)) .then(this.reloadDevicesAndSend(number, true))
.catch(error => { .catch(error => {
if (this.fallBackEncryption && conversation) { if (this.fallBackEncryption && conversation) {
conversation.onFriendRequestTimedOut(); conversation.updateFriendRequestUI();
} }
if (error.message === 'Identity key changed') { if (error.message === 'Identity key changed') {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign

@ -9,7 +9,7 @@ message Envelope {
UNKNOWN = 0; UNKNOWN = 0;
CIPHERTEXT = 1; CIPHERTEXT = 1;
KEY_EXCHANGE = 2; KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3; PREKEY_BUNDLE = 3; //Used By Signal. DO NOT TOUCH! we don't use this at all.
RECEIPT = 5; RECEIPT = 5;
UNIDENTIFIED_SENDER = 6; UNIDENTIFIED_SENDER = 6;
FRIEND_REQUEST = 101; // contains prekeys + message and is using simple encryption FRIEND_REQUEST = 101; // contains prekeys + message and is using simple encryption
@ -24,6 +24,7 @@ message Envelope {
optional bytes content = 8; // Contains an encrypted Content optional bytes content = 8; // Contains an encrypted Content
optional string serverGuid = 9; optional string serverGuid = 9;
optional uint64 serverTimestamp = 10; optional uint64 serverTimestamp = 10;
optional bytes preKeyBundleMessage = 101;
} }
@ -33,7 +34,6 @@ message Content {
optional CallMessage callMessage = 3; optional CallMessage callMessage = 3;
optional NullMessage nullMessage = 4; optional NullMessage nullMessage = 4;
optional ReceiptMessage receiptMessage = 5; optional ReceiptMessage receiptMessage = 5;
optional PreKeyBundleMessage preKeyBundleMessage = 6;
} }
message PreKeyBundleMessage { message PreKeyBundleMessage {

@ -8,7 +8,7 @@ interface Props {
text: string; text: string;
direction: 'incoming' | 'outgoing'; direction: 'incoming' | 'outgoing';
status: string; status: string;
friendStatus: 'pending' | 'accepted' | 'declined'; friendStatus: 'pending' | 'accepted' | 'declined' | 'expired';
i18n: Localizer; i18n: Localizer;
isBlocked: boolean; isBlocked: boolean;
onAccept: () => void; onAccept: () => void;
@ -25,11 +25,13 @@ export class FriendRequest extends React.Component<Props> {
switch (friendStatus) { switch (friendStatus) {
case 'pending': case 'pending':
return `friendRequestPending`; return 'friendRequestPending';
case 'accepted': case 'accepted':
return `friendRequestAccepted`; return 'friendRequestAccepted';
case 'declined': case 'declined':
return `friendRequestDeclined`; return 'friendRequestDeclined';
case 'expired':
return 'friendRequestExpired'
default: default:
throw new Error(`Invalid friend request status: ${friendStatus}`); throw new Error(`Invalid friend request status: ${friendStatus}`);
} }
@ -48,7 +50,6 @@ export class FriendRequest extends React.Component<Props> {
<MessageBody text={text || ''} i18n={i18n} /> <MessageBody text={text || ''} i18n={i18n} />
</div> </div>
</div> </div>
); );
} }
@ -143,7 +144,7 @@ export class FriendRequest extends React.Component<Props> {
public render() { public render() {
const { direction } = this.props; const { direction } = this.props;
return ( return (
<div <div
className={classNames( className={classNames(
@ -159,7 +160,7 @@ export class FriendRequest extends React.Component<Props> {
'module-message-friend-request__container', 'module-message-friend-request__container',
)} )}
> >
<div <div
className={classNames( className={classNames(
'module-message__text', 'module-message__text',
`module-message__text--${direction}`, `module-message__text--${direction}`,

Loading…
Cancel
Save