diff --git a/js/models/conversations.js b/js/models/conversations.js index 134ff543f..34384145a 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -685,6 +685,10 @@ friendStatus: 'pending', direction: 'incoming', preKeyBundle: null, + timestamp: null, + source: null, + sourceDevice: null, + received_at: null, ...options, }; @@ -695,13 +699,13 @@ return; } - const lastMessage = this.get('timestamp') || Date.now(); + const timestamp = _options.timestamp || this.get('timestamp') || Date.now(); window.log.info( 'adding friend request for', this.ourNumber, this.idForLogging(), - lastMessage + timestamp ); this.lastMessageStatus = 'sending'; @@ -731,12 +735,12 @@ } // Add the new message - const timestamp = Date.now(); + const received_at = _options.received_at || Date.now(); const message = { conversationId: this.id, type: 'friend-request', - sent_at: lastMessage, - received_at: timestamp, + sent_at: timestamp, + received_at, unread: 1, from: this.id, to: this.ourNumber, @@ -744,6 +748,8 @@ direction: _options.direction, body, preKeyBundle: _options.preKeyBundle, + source: _options.source, + sourceDevice: _options.sourceDevice, }; const id = await window.Signal.Data.saveMessage(message, { diff --git a/js/modules/data.js b/js/modules/data.js index 330e579a0..69c46f107 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -664,13 +664,14 @@ async function searchConversations(query, { ConversationCollection }) { } // Message - +const MESSAGE_PRE_KEYS = ['identityKey', 'preKey', 'signature', 'signedKey'].map(k => `preKeyBundle.${k}`); async function getMessageCount() { return channels.getMessageCount(); } async function saveMessage(data, { forceSave, Message } = {}) { - const id = await channels.saveMessage(_cleanData(data), { forceSave }); + const updated = keysFromArrayBuffer(MESSAGE_PRE_KEYS, data); + const id = await channels.saveMessage(_cleanData(updated), { forceSave }); Message.refreshExpirationTimer(); return id; } @@ -713,7 +714,8 @@ async function saveLegacyMessage(data) { } async function saveMessages(arrayOfMessages, { forceSave } = {}) { - await channels.saveMessages(_cleanData(arrayOfMessages), { forceSave }); + const updated = arrayOfMessages.map(m => keysFromArrayBuffer(MESSAGE_PRE_KEYS, m)); + await channels.saveMessages(_cleanData(updated), { forceSave }); } async function removeMessage(id, { Message }) { @@ -738,13 +740,16 @@ async function getMessageById(id, { Message }) { return null; } - return new Message(message); + const encoded = keysToArrayBuffer(MESSAGE_PRE_KEYS, message); + + return new Message(encoded); } // For testing only async function getAllMessages({ MessageCollection }) { const messages = await channels.getAllMessages(); - return new MessageCollection(messages); + const encoded = messages.map(m => keysToArrayBuffer(MESSAGE_PRE_KEYS, m)); + return new MessageCollection(encoded); } async function getAllMessageIds() { @@ -766,7 +771,9 @@ async function getMessageBySender( return null; } - return new Message(messages[0]); + const encoded = keysToArrayBuffer(MESSAGE_PRE_KEYS, messages[0]); + + return new Message(encoded); } async function getUnreadByConversation(conversationId, { MessageCollection }) { @@ -784,7 +791,9 @@ async function getMessagesByConversation( type, }); - return new MessageCollection(messages); + const encoded = messages.map(m => keysToArrayBuffer(MESSAGE_PRE_KEYS, m)); + + return new MessageCollection(encoded); } async function removeAllMessagesInConversation( @@ -819,22 +828,26 @@ async function removeAllMessagesInConversation( async function getMessagesBySentAt(sentAt, { MessageCollection }) { const messages = await channels.getMessagesBySentAt(sentAt); - return new MessageCollection(messages); + const encoded = messages.map(m => keysToArrayBuffer(MESSAGE_PRE_KEYS, m)); + return new MessageCollection(encoded); } async function getExpiredMessages({ MessageCollection }) { const messages = await channels.getExpiredMessages(); - return new MessageCollection(messages); + const encoded = messages.map(m => keysToArrayBuffer(MESSAGE_PRE_KEYS, m)); + return new MessageCollection(encoded); } async function getOutgoingWithoutExpiresAt({ MessageCollection }) { const messages = await channels.getOutgoingWithoutExpiresAt(); - return new MessageCollection(messages); + const encoded = messages.map(m => keysToArrayBuffer(MESSAGE_PRE_KEYS, m)); + return new MessageCollection(encoded); } async function getNextExpiringMessage({ MessageCollection }) { const messages = await channels.getNextExpiringMessage(); - return new MessageCollection(messages); + const encoded = messages.map(m => keysToArrayBuffer(MESSAGE_PRE_KEYS, m)); + return new MessageCollection(encoded); } // Unprocessed diff --git a/js/views/app_view.js b/js/views/app_view.js index d1dd803c2..bcf325768 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -178,12 +178,13 @@ }); } }, - async showFriendRequest({ pubKey, message, preKeyBundle }) { + 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, }); } }, diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index e70ba299c..2d34aa7de 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -934,11 +934,17 @@ MessageReceiver.prototype.extend({ return this.innerHandleContentMessage(envelope, plaintext); }); }, - promptUserToAcceptFriendRequest(pubKey, message, preKeyBundle) { + promptUserToAcceptFriendRequest(envelope, message, preKeyBundleMessage) { window.Whisper.events.trigger('showFriendRequest', { - pubKey, + pubKey: envelope.source, message, - preKeyBundle, + 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 @@ -973,52 +979,49 @@ MessageReceiver.prototype.extend({ const content = textsecure.protobuf.Content.decode(plaintext); if (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) { - // only prompt friend request if there is no conversation yet let conversation; try { conversation = ConversationController.get(envelope.source); } catch (e) { } + + // only prompt friend request if there is no conversation yet if (!conversation) { this.promptUserToAcceptFriendRequest( - envelope.source, + envelope, content.dataMessage.body, - content.preKeyBundle, + content.preKeyBundleMessage, ); - return; - } - } - - // Check if our friend request got accepted - if (content.preKeyBundle) { - // By default we don't want to save the preKey - let savePreKey = false; - - // The conversation - let conversation = null; - - try { - conversation = ConversationController.get(envelope.source); - - // We only want to save the preKey if we have a outgoing friend request which is pending - const pending = await conversation.getPendingFriendRequests('outgoing'); - const successful = pending.filter(p => !p.hasErrors()); - - // Save the key only if we have an outgoing friend request - savePreKey = (successful.length > 0); - } catch (e) {} - - // Save the pre key if we have a conversation - if (savePreKey && conversation) { - await this.handlePreKeyBundleMessage( - envelope.source, - content.preKeyBundle - ); - - // Update the conversation - await conversation.onFriendRequestAccepted(); + } else { + const keyExchangeComplete = conversation.isKeyExchangeCompleted(); + + // Check here if we received preKeys from the other user + // 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), + ); - return; + // Update the conversation + await conversation.onFriendRequestAccepted(); + } + } } + + // Exit early since the friend request reply will be a regular empty message + return; } if (content.syncMessage) { @@ -1235,8 +1238,7 @@ MessageReceiver.prototype.extend({ return this.removeFromCache(envelope); }, - async handlePreKeyBundleMessage(pubKey, preKeyBundleMessage) { - const { preKeyId, signedKeyId } = preKeyBundleMessage; + decodePreKeyBundleMessage(preKeyBundleMessage) { const [identityKey, preKey, signedKey, signature] = [ preKeyBundleMessage.identityKey, preKeyBundleMessage.preKey, @@ -1244,6 +1246,24 @@ MessageReceiver.prototype.extend({ preKeyBundleMessage.signature, ].map(k => dcodeIO.ByteBuffer.wrap(k).toArrayBuffer()); + return { + ...preKeyBundleMessage, + identityKey, + preKey, + signedKey, + signature, + }; + }, + async handlePreKeyBundleMessage(pubKey, preKeyBundleMessage) { + const { + preKeyId, + signedKeyId, + identityKey, + preKey, + signedKey, + signature, + } = preKeyBundleMessage; + if (pubKey != StringView.arrayBufferToHex(identityKey)) { throw new Error( 'Error in handlePreKeyBundleMessage: envelope pubkey does not match pubkey in prekey bundle'