diff --git a/js/models/conversations.js b/js/models/conversations.js index 7af61f601..ce16f3960 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1,5 +1,6 @@ /* global _: false */ /* global Backbone: false */ +/* global BlockedNumberController: false */ /* global ConversationController: false */ /* global i18n: false */ /* global libsignal: false */ @@ -42,14 +43,10 @@ const FriendStatusEnum = Object.freeze({ // New conversation, no messages sent or received none: 0, - // Have received a friend request, waiting to accept/decline - pendingAction: 1, - // Have sent a friend request, waiting for response - pendingResponse: 2, - // Have sent and received prekeybundle, waiting for ciphertext confirming key exchange - pendingCipher: 3, + // Friend request not complete yet, input blocked + pending: 1, // We did it! - friends: 4, + friends: 2, }); const COLORS = [ @@ -66,26 +63,6 @@ '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({ storeName: 'conversations', defaults() { @@ -93,7 +70,6 @@ unreadCount: 0, verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT, friendStatus: FriendStatusEnum.none, - keyExchangeCompleted: false, unlockTimestamp: null, // Timestamp used for expiring friend requests. }; }, @@ -477,50 +453,17 @@ return contact.isVerified(); }); }, - isKeyExchangeCompleted() { - if (!this.isPrivate()) { - return false; - // throw new Error('isKeyExchangeCompleted not implemented for groups'); - } - - if (this.isMe()) { - return true; - } - - return this.get('friendStatus') === FriendStatusEnum.friends; - }, - async setKeyExchangeCompleted() { - if (this.get('friendStatus') !== FriendStatusEnum.pendingCipher) return; - - this.set({ friendStatus: FriendStatusEnum.friends }); - await window.Signal.Data.updateConversation(this.id, this.attributes, { - 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()); + // 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); - }, - getPreKeyBundleType() { - switch (this.get('friendStatus')) { - case FriendStatusEnum.none: - case FriendStatusEnum.pendingResponse: - return textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST; - case FriendStatusEnum.pendingAction: - case FriendStatusEnum.pendingCipher: - return textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST_ACCEPT; - default: - return textsecure.protobuf.PreKeyBundleMessage.Type.UNKNOWN; - } + return (incoming.length > 0 || successfulOutgoing.length > 0); }, isFriend() { - return this.get('friendStatus') === FriendStatusEnum.pendingCipher || - this.get('friendStatus') === FriendStatusEnum.friends; + return this.get('friendStatus') === FriendStatusEnum.friends; }, // Update any pending friend requests for the current user async updateFriendRequestUI() { @@ -554,7 +497,7 @@ // We have declined an incoming friend request async onDeclineFriendRequest() { // Should we change states for other states? (They should never happen) - if (this.get('friendStatus') === FriendStatusEnum.pendingAction) { + if (this.get('friendStatus') === FriendStatusEnum.pending) { this.set({ friendStatus: FriendStatusEnum.none }); await window.Signal.Data.updateConversation(this.id, this.attributes, { Conversation: Whisper.Conversation, @@ -567,8 +510,8 @@ // We have accepted an incoming friend request async onAcceptFriendRequest() { // Should we change states for other states? (They should never happen) - if (this.get('friendStatus') === FriendStatusEnum.pendingAction) { - this.set({ friendStatus: FriendStatusEnum.pendingCipher }); + if (this.get('friendStatus') === FriendStatusEnum.pending) { + this.set({ friendStatus: FriendStatusEnum.friends }); await window.Signal.Data.updateConversation(this.id, this.attributes, { Conversation: Whisper.Conversation, }); @@ -579,9 +522,8 @@ }, // Our outgoing friend request has been accepted async onFriendRequestAccepted() { - // TODO: Think about how we want to handle other states - if (this.get('friendStatus') === FriendStatusEnum.pendingResponse) { - this.set({ friendStatus: FriendStatusEnum.pendingCipher }); + if (this.get('friendStatus') === FriendStatusEnum.pending) { + this.set({ friendStatus: FriendStatusEnum.friends }); await window.Signal.Data.updateConversation(this.id, this.attributes, { Conversation: Whisper.Conversation, }); @@ -622,30 +564,12 @@ await this.updateFriendRequestUI(); }, async onFriendRequestReceived() { - switch (this.get('friendStatus')) { - case FriendStatusEnum.none: - this.set({ friendStatus: FriendStatusEnum.pendingAction }); - await window.Signal.Data.updateConversation(this.id, this.attributes, { - Conversation: Whisper.Conversation, - }); - await this.updateFriendRequestUI(); - return; - case FriendStatusEnum.pendingResponse: - this.set({ friendStatus: FriendStatusEnum.pendingCipher }); - await window.Signal.Data.updateConversation(this.id, this.attributes, { - Conversation: Whisper.Conversation, - }); - await this.updateFriendRequestUI(); - return; - case FriendStatusEnum.pendingAction: - case FriendStatusEnum.pendingCipher: - // No need to change state - return; - case FriendStatusEnum.friends: - // TODO: Handle this case (discuss with squad) - return; - default: - throw new TypeError(`Invalid friendStatus type: '${this.friendStatus}'`); + if (this.get('friendStatus') === FriendStatusEnum.none) { + this.set({ friendStatus: FriendStatusEnum.pending }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + await this.updateFriendRequestUI(); } }, async onFriendRequestSent() { @@ -665,7 +589,7 @@ } if (this.get('friendStatus') === FriendStatusEnum.none) { - this.set({ friendStatus: FriendStatusEnum.pendingResponse }); + this.set({ friendStatus: FriendStatusEnum.pending }); await window.Signal.Data.updateConversation(this.id, this.attributes, { Conversation: Whisper.Conversation, }); @@ -1170,6 +1094,7 @@ ); const options = this.getSendOptions(); + options.messageType = message.get('type'); // Add the message sending on another queue so that our UI doesn't get blocked this.queueMessageSend(async () => @@ -1295,12 +1220,10 @@ getSendOptions(options = {}) { const senderCertificate = storage.get('senderCertificate'); const numberInfo = this.getNumberInfo(options); - const preKeyBundleType = this.getPreKeyBundleType(); return { senderCertificate, numberInfo, - preKeyBundleType, }; }, @@ -1371,8 +1294,8 @@ // Delete the old messages if it's pending await Promise.all( incoming - .filter(i => i.id !== message.id) - .map(request => this._removeMessage(request.id)) + .filter(i => i.id !== message.id) + .map(request => this._removeMessage(request.id)) ); // If we have an outgoing friend request then diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 588babf78..cf9b1fc0b 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -164,7 +164,7 @@ color: this.model.getColor(), avatarPath: this.model.getAvatarPath(), isVerified: this.model.isVerified(), - isKeysPending: this.model.isKeyExchangeCompleted() === false, + isKeysPending: !this.model.isFriend(), isMe: this.model.isMe(), isBlocked: this.model.isBlocked(), isGroup: !this.model.isPrivate(), diff --git a/libloki/libloki-protocol.js b/libloki/libloki-protocol.js index 76e4963fc..8d2a36cb5 100644 --- a/libloki/libloki-protocol.js +++ b/libloki/libloki-protocol.js @@ -25,7 +25,7 @@ ivAndCiphertext.set(new Uint8Array(iv)); ivAndCiphertext.set(new Uint8Array(ciphertext), iv.byteLength); return { - type: textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT, + type: textsecure.protobuf.Envelope.Type.FRIEND_REQUEST, body: ivAndCiphertext, registrationId: null, }; @@ -141,9 +141,7 @@ log.info('empty message sent successfully'); } }; - const options = { - preKeyBundleType: textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST_ACCEPT, - }; + const options = {}; // send an empty message. The logic in ougoing_message will attach the prekeys. const outgoingMessage = new textsecure.OutgoingMessage( null, // server diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 3e255a274..c4bea25b5 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -631,9 +631,6 @@ MessageReceiver.prototype.extend({ return this.onDeliveryReceipt(envelope); } - if (envelope.preKeyBundleMessage) { - return this.handlePreKeyBundleMessage(envelope); - } if (envelope.content) { return this.handleContentMessage(envelope); } @@ -725,7 +722,7 @@ MessageReceiver.prototype.extend({ promise = sessionCipher.decryptWhisperMessage(ciphertext) .then(this.unpad); break; - case textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT: { + case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: { window.log.info('friend-request message from ', envelope.source); promise = fallBackSessionCipher.decrypt(ciphertext.toArrayBuffer()) .then(this.unpad); @@ -984,45 +981,38 @@ MessageReceiver.prototype.extend({ let conversation; try { - conversation = window.ConversationController.get(envelope.source); + conversation = await window.ConversationController.getOrCreateAndWait(envelope.source, 'private'); } catch (e) { window.log.info('Error getting conversation: ', envelope.source); } - - if ( - envelope.preKeyBundleMessage && - envelope.preKeyBundleMessage.type === - textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST - ) { - return this.handleFriendRequestMessage(envelope, content.dataMessage); - } else if ( - envelope.type === textsecure.protobuf.Envelope.Type.CIPHERTEXT || - // We also need to check for PREKEY_BUNDLE aswell if the session hasn't started. - // ref: libsignal-protocol.js:36120 - envelope.type === textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE - ) { - // If we get a cipher and we're already friends - // then we set our key exchange to complete - if (conversation && conversation.isFriend()) { - await conversation.setKeyExchangeCompleted(); - } + if (envelope.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) { + conversation.onFriendRequestReceived(); + } else { + conversation.onFriendRequestAccepted(); } - if (content.syncMessage) { + if (content.preKeyBundleMessage) { + const preKeyBundleMessage = + this.decodePreKeyBundleMessage(content.preKeyBundleMessage); + await this.savePreKeyBundleMessage( + envelope.source, + preKeyBundleMessage + ); + return this.handleDataMessage(envelope, content.dataMessage, 'friend-request'); + } + if (content.syncMessage) return this.handleSyncMessage(envelope, content.syncMessage); - } else if (content.dataMessage) { + if (content.dataMessage) return this.handleDataMessage(envelope, content.dataMessage); - } else if (content.nullMessage) { + if (content.nullMessage) return this.handleNullMessage(envelope, content.nullMessage); - } else if (content.callMessage) { + if (content.callMessage) return this.handleCallMessage(envelope, content.callMessage); - } else if (content.receiptMessage) { + if (content.receiptMessage) return this.handleReceiptMessage(envelope, content.receiptMessage); - } - this.removeFromCache(envelope); - if (envelope.preKeyBundleMessage) return null; - throw new Error('Unsupported content message'); + this.removeFromCache(envelope); + return null; }, handleCallMessage(envelope) { window.log.info('call message from', this.getEnvelopeId(envelope)); @@ -1240,43 +1230,6 @@ MessageReceiver.prototype.extend({ signature, }; }, - async handlePreKeyBundleMessage(envelope) { - const preKeyBundleMessage = await this.decryptPreKeyBundleMessage(envelope); - // eslint-disable-next-line no-param-reassign - envelope.preKeyBundleMessage = preKeyBundleMessage; - await this.savePreKeyBundleMessage( - envelope.source, - preKeyBundleMessage - ); - const conversation = await window.ConversationController.getOrCreateAndWait( - envelope.source, - 'private' - ); - if (preKeyBundleMessage.type === textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST) { - conversation.onFriendRequestReceived(); - } else if (preKeyBundleMessage.type === textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST_ACCEPT) { - conversation.onFriendRequestAccepted(); - } else { - window.log.warn('Unknown PreKeyBundleMessage Type') - } - if (envelope.content) - return this.handleContentMessage(envelope); - return null; - }, - async decryptPreKeyBundleMessage(envelope) { - if (!envelope.preKeyBundleMessage) return null; - const address = new libsignal.SignalProtocolAddress(envelope.source, envelope.sourceDevice); - const fallBackSessionCipher = new libloki.FallBackSessionCipher( - address - ); - 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); - - return decodedBundle; - }, async savePreKeyBundleMessage(pubKey, preKeyBundleMessage) { if (!preKeyBundleMessage) return null; diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 7c3a93441..ebfdc4e29 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -43,11 +43,11 @@ function OutgoingMessage( this.failoverNumbers = []; this.unidentifiedDeliveries = []; - const { numberInfo, senderCertificate, preKeyBundleType } = options; + const { numberInfo, senderCertificate, messageType } = options; this.numberInfo = numberInfo; this.senderCertificate = senderCertificate; - this.preKeyBundleType = - preKeyBundleType || textsecure.protobuf.PreKeyBundleMessage.Type.UNKNOWN; + this.messageType = + messageType || 'outgoing'; } OutgoingMessage.prototype = { @@ -221,16 +221,12 @@ OutgoingMessage.prototype = { return this.plaintext; }, async wrapInWebsocketMessage(outgoingObject) { - const preKeyEnvelope = outgoingObject.preKeyBundleMessage ? { - preKeyBundleMessage: outgoingObject.preKeyBundleMessage, - } : {}; const messageEnvelope = new textsecure.protobuf.Envelope({ type: outgoingObject.type, source: outgoingObject.ourKey, sourceDevice: outgoingObject.sourceDevice, timestamp: this.timestamp, content: outgoingObject.content, - ...preKeyEnvelope, }); const requestMessage = new textsecure.protobuf.WebSocketRequestMessage({ id: new Uint8Array(libsignal.crypto.getRandomBytes(1))[0], // random ID for now @@ -248,7 +244,6 @@ OutgoingMessage.prototype = { }, doSendMessage(number, deviceIds, recurse) { const ciphers = {}; - const plaintext = this.getPlaintext(); /* Disabled because i'm not sure how senderCertificate works :thinking: const { numberInfo, senderCertificate } = this; @@ -288,25 +283,11 @@ OutgoingMessage.prototype = { const fallBackEncryption = new libloki.FallBackSessionCipher(address); // Check if we need to attach the preKeys - let preKeys = {}; - if (this.attachPrekeys) { + let sessionCipher; + if (this.messageType === 'friend-request') { // Encrypt them with the fallback - const preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(number); - preKeyBundleMessage.type = this.preKeyBundleType; - - const textBundle = this.convertMessageToText(preKeyBundleMessage); - const encryptedBundle = await fallBackEncryption.encrypt(textBundle); - preKeys = { preKeyBundleMessage: encryptedBundle.body }; + this.message.preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(number); window.log.info('attaching prekeys to outgoing message'); - } - - // No limit on message keys if we're communicating with our other devices - if (ourKey === number) { - options.messageKeysLimit = false; - } - - let sessionCipher; - if (this.fallBackEncryption) { sessionCipher = fallBackEncryption; } else { sessionCipher = new libsignal.SessionCipher( @@ -315,6 +296,13 @@ OutgoingMessage.prototype = { options ); } + const plaintext = this.getPlaintext(); + + // No limit on message keys if we're communicating with our other devices + if (ourKey === number) { + options.messageKeysLimit = false; + } + ciphers[address.getDeviceId()] = sessionCipher; // Encrypt our plain text @@ -334,7 +322,6 @@ OutgoingMessage.prototype = { sourceDevice: 1, destinationRegistrationId: ciphertext.registrationId, content: ciphertext.body, - ...preKeys, }; }) ) @@ -344,10 +331,7 @@ OutgoingMessage.prototype = { const socketMessage = await this.wrapInWebsocketMessage(outgoingObject); let ttl; // TODO: Allow user to set ttl manually - if ( - outgoingObject.type === - textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT - ) { + if (outgoingObject.type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) { ttl = 4 * 24 * 60 * 60; // 4 days for friend request message } else { ttl = 24 * 60 * 60; // 1 day default for any other message @@ -465,17 +449,9 @@ OutgoingMessage.prototype = { return this.getStaleDeviceIdsForNumber(number).then(updateDevices => this.getKeysForNumber(number, updateDevices) .then(async keysFound => { - this.attachPrekeys = false; if (!keysFound) { log.info('Fallback encryption enabled'); this.fallBackEncryption = true; - this.attachPrekeys = true; - } else if (conversation) { - try { - this.attachPrekeys = !conversation.isKeyExchangeCompleted(); - } catch (e) { - // do nothing - } } if (this.fallBackEncryption && conversation) { diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 6e1cce788..08b2adfc5 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -12,7 +12,7 @@ message Envelope { PREKEY_BUNDLE = 3; //Used By Signal. DO NOT TOUCH! we don't use this at all. RECEIPT = 5; UNIDENTIFIED_SENDER = 6; - FALLBACK_CIPHERTEXT = 101; // contains prekeys + message and is using simple encryption + FRIEND_REQUEST = 101; // contains prekeys + message and is using simple encryption } optional Type type = 1; @@ -24,7 +24,6 @@ message Envelope { optional bytes content = 8; // Contains an encrypted Content optional string serverGuid = 9; optional uint64 serverTimestamp = 10; - optional bytes preKeyBundleMessage = 101; } @@ -34,16 +33,10 @@ message Content { optional CallMessage callMessage = 3; optional NullMessage nullMessage = 4; optional ReceiptMessage receiptMessage = 5; + optional PreKeyBundleMessage preKeyBundleMessage = 101; } message PreKeyBundleMessage { - enum Type { - UNKNOWN = 0; - FRIEND_REQUEST = 1; - FRIEND_REQUEST_ACCEPT = 2; - RESET_SESSION = 3; - RESET_SESSION_ACK = 4; - } optional bytes identityKey = 1; optional uint32 deviceId = 2; optional uint32 preKeyId = 3; @@ -51,7 +44,6 @@ message PreKeyBundleMessage { optional bytes preKey = 5; optional bytes signedKey = 6; optional bytes signature = 7; - optional Type type = 8; } message CallMessage {