From b65d6a6d2c67400b41aecacc09a3410d81740813 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Fri, 23 Nov 2018 18:29:40 +1100 Subject: [PATCH] Refactor friend request status to use a state enum variable --- app/sql.js | 14 +++- js/background.js | 8 -- js/models/conversations.js | 111 ++++++++++++++++++++++++--- js/models/messages.js | 12 +-- libloki/libloki-protocol.js | 2 +- libtextsecure/message_receiver.js | 121 +++++++++++++----------------- libtextsecure/outgoing_message.js | 9 +-- protos/SignalService.proto | 2 +- 8 files changed, 173 insertions(+), 106 deletions(-) diff --git a/app/sql.js b/app/sql.js index 5b882a325..181cfcfc8 100644 --- a/app/sql.js +++ b/app/sql.js @@ -394,6 +394,11 @@ async function updateToSchemaVersion6(currentVersion, instance) { console.log('updateToSchemaVersion6: starting...'); await instance.run('BEGIN TRANSACTION;'); + await instance.run( + `ALTER TABLE conversations + ADD COLUMN friendStatus INTEGER;` + ); + await instance.run( `CREATE TABLE seenMessages( hash STRING PRIMARY KEY, @@ -959,7 +964,7 @@ async function getConversationCount() { async function saveConversation(data) { // eslint-disable-next-line camelcase - const { id, active_at, type, members, name, profileName } = data; + const { id, active_at, type, members, name, friendStatus, profileName } = data; await db.run( `INSERT INTO conversations ( @@ -970,6 +975,7 @@ async function saveConversation(data) { type, members, name, + friendStatus, profileName ) values ( $id, @@ -979,6 +985,7 @@ async function saveConversation(data) { $type, $members, $name, + $friendStatus, $profileName );`, { @@ -989,6 +996,7 @@ async function saveConversation(data) { $type: type, $members: members ? members.join(' ') : null, $name: name, + $friendStatus: friendStatus, $profileName: profileName, } ); @@ -1012,7 +1020,7 @@ async function saveConversations(arrayOfConversations) { async function updateConversation(data) { // eslint-disable-next-line camelcase - const { id, active_at, type, members, name, profileName } = data; + const { id, active_at, type, members, name, friendStatus, profileName } = data; await db.run( `UPDATE conversations SET @@ -1022,6 +1030,7 @@ async function updateConversation(data) { type = $type, members = $members, name = $name, + friendStatus = $friendStatus, profileName = $profileName WHERE id = $id;`, { @@ -1032,6 +1041,7 @@ async function updateConversation(data) { $type: type, $members: members ? members.join(' ') : null, $name: name, + $friendStatus: friendStatus, $profileName: profileName, } ); diff --git a/js/background.js b/js/background.js index c772ba62a..71e5935ee 100644 --- a/js/background.js +++ b/js/background.js @@ -568,14 +568,6 @@ } }); - // Gets called when a user accepts or declines a friend request - Whisper.events.on('friendRequestUpdated', friendRequest => { - const { pubKey, ...message } = friendRequest; - if (messageReceiver) { - messageReceiver.onFriendRequestUpdate(pubKey, message); - } - }); - Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => { try { const conversation = ConversationController.get(pubKey); diff --git a/js/models/conversations.js b/js/models/conversations.js index 95a4ff17a..a97664dc8 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -38,6 +38,20 @@ deleteAttachmentData, } = window.Signal.Migrations; + // Possible conversation friend states + 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 send a friend request, waiting for response + pendingResponse: 2, + // Have send and received prekeybundle, waiting for ciphertext confirming key exchange + pendingCipher: 3, + // We did it! + friends: 4, + }); + const COLORS = [ 'red', 'deep_orange', @@ -78,6 +92,7 @@ return { unreadCount: 0, verified: textsecure.storage.protocol.VerifiedStatus.DEFAULT, + friendStatus: FriendStatusEnum.none, isFriend: false, keyExchangeCompleted: false, unlockTimestamp: null, // Timestamp used for expiring friend requests. @@ -473,13 +488,12 @@ return true; } - return this.get('keyExchangeCompleted') || false; + return this.get('friendStatus') === FriendStatusEnum.friends; }, - async setKeyExchangeCompleted(value) { - // Only update the value if it's different - if (this.get('keyExchangeCompleted') === value) return; + async setKeyExchangeCompleted() { + if (this.get('friendStatus') !== FriendStatusEnum.pendingCipher) return; - this.set({ keyExchangeCompleted: value }); + this.set({ friendStatus: FriendStatusEnum.friends }); await window.Signal.Data.updateConversation(this.id, this.attributes, { Conversation: Whisper.Conversation, }); @@ -492,9 +506,22 @@ 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; + } + }, isFriend() { - return this.get('isFriend'); + return this.get('friendStatus') === FriendStatusEnum.pendingCipher || + this.get('friendStatus') === FriendStatusEnum.friends; }, // Update any pending friend requests for the current user async updateFriendRequestUI() { @@ -525,15 +552,42 @@ if (pending.length > 0) this.notifyFriendRequest(this.id, 'accepted') }, + // 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) { + this.set({ friendStatus: FriendStatusEnum.none }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + await this.updateFriendRequestUI(); + await this.updatePendingFriendRequests(); + await window.libloki.removePreKeyBundleForNumber(this.id); + } + }, + // 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 }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + await this.updateFriendRequestUI(); + await this.updatePendingFriendRequests(); + window.libloki.sendFriendRequestAccepted(this.id); + } + }, + // Our outgoing friend request has been accepted async onFriendRequestAccepted() { - if (!this.isFriend()) { - this.set({ isFriend: true }); + // TODO: Think about how we want to handle other states + if (this.get('friendStatus') === FriendStatusEnum.pendingResponse) { + this.set({ friendStatus: FriendStatusEnum.pendingCipher }); await window.Signal.Data.updateConversation(this.id, this.attributes, { Conversation: Whisper.Conversation, }); + await this.updateFriendRequestUI(); } - - await this.updateFriendRequestUI(); }, async onFriendRequestTimeout() { // Unset the timer @@ -568,6 +622,33 @@ await this.updatePendingFriendRequests(); 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}'`); + } + }, async onFriendRequestSent() { // Check if we need to set the friend request expiry const unlockTimestamp = this.get('unlockTimestamp'); @@ -584,6 +665,12 @@ this.setFriendRequestExpiryTimeout(); } + if (this.get('friendStatus') === FriendStatusEnum.none) { + this.set({ friendStatus: FriendStatusEnum.pendingResponse }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + } this.updateFriendRequestUI(); }, setFriendRequestExpiryTimeout() { @@ -1209,10 +1296,12 @@ getSendOptions(options = {}) { const senderCertificate = storage.get('senderCertificate'); const numberInfo = this.getNumberInfo(options); + const preKeyBundleType = this.getPreKeyBundleType(); return { senderCertificate, numberInfo, + preKeyBundleType, }; }, diff --git a/js/models/messages.js b/js/models/messages.js index 2b4ea3d2c..96e270e6c 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -306,11 +306,7 @@ await window.Signal.Data.saveMessage(this.attributes, { Message: Whisper.Message, }); - - window.Whisper.events.trigger('friendRequestUpdated', { - pubKey: conversation.id, - ...this.attributes, - }); + conversation.onAcceptFriendRequest(); }, async declineFriendRequest() { if (this.get('friendStatus') !== 'pending') return; @@ -320,11 +316,7 @@ await window.Signal.Data.saveMessage(this.attributes, { Message: Whisper.Message, }); - - window.Whisper.events.trigger('friendRequestUpdated', { - pubKey: conversation.id, - ...this.attributes, - }); + conversation.onDeclineFriendRequest(); }, getPropsForFriendRequest() { const friendStatus = this.get('friendStatus') || 'pending'; diff --git a/libloki/libloki-protocol.js b/libloki/libloki-protocol.js index 0a78f0ca0..76e4963fc 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.FRIEND_REQUEST, + type: textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT, body: ivAndCiphertext, registrationId: null, }; diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index be8cfdb06..a969155f2 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -632,9 +632,13 @@ MessageReceiver.prototype.extend({ return this.onDeliveryReceipt(envelope); } + if (envelope.preKeyBundleMessage) { + return this.handlePreKeyBundleMessage(envelope); + } if (envelope.content) { return this.handleContentMessage(envelope); - } else if (envelope.legacyMessage) { + } + if (envelope.legacyMessage) { return this.handleLegacyMessage(envelope); } this.removeFromCache(envelope); @@ -711,23 +715,6 @@ MessageReceiver.prototype.extend({ 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 preKeyBundle - await this.handlePreKeyBundleMessage( - envelope.source, - envelope.preKeyBundleMessage - ); - } - const me = { number: ourNumber, deviceId: parseInt(textsecure.storage.user.getDeviceId(), 10), @@ -739,7 +726,7 @@ MessageReceiver.prototype.extend({ promise = sessionCipher.decryptWhisperMessage(ciphertext) .then(this.unpad); break; - case textsecure.protobuf.Envelope.Type.FRIEND_REQUEST: { + case textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT: { window.log.info('friend-request message from ', envelope.source); promise = fallBackSessionCipher.decrypt(ciphertext.toArrayBuffer()) .then(this.unpad); @@ -993,37 +980,6 @@ MessageReceiver.prototype.extend({ return this.innerHandleContentMessage(envelope, plaintext); }); }, - // A handler function for when a friend request is accepted or declined - async onFriendRequestUpdate(pubKey, message) { - if (!message || !message.direction || !message.friendStatus) return; - - // Update the conversation - const conversation = window.ConversationController.get(pubKey); - if (conversation) { - // Update the conversation friend request indicator - await conversation.updatePendingFriendRequests(); - await conversation.updateTextInputState(); - } - - // Check if we changed the state of the incoming friend request - if (message.direction === 'incoming') { - // If we accepted an incoming friend request then update our state - if (message.friendStatus === 'accepted') { - // Accept the friend request - if (conversation) { - await conversation.onFriendRequestAccepted(); - } - - // Send a reply back - libloki.sendFriendRequestAccepted(pubKey); - } else if (message.friendStatus === 'declined') { - // Delete the preKeys - await libloki.removePreKeyBundleForNumber(pubKey); - } - } - - window.log.info(`Friend request for ${pubKey} was ${message.friendStatus}`, message); - }, async innerHandleContentMessage(envelope, plaintext) { const content = textsecure.protobuf.Content.decode(plaintext); @@ -1034,27 +990,22 @@ MessageReceiver.prototype.extend({ window.log.info('Error getting conversation: ', envelope.source); } - // Check if the other user accepted our friend request if ( envelope.preKeyBundleMessage && - envelope.preKeyBundleMessage.type === textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST_ACCEPT && - conversation + envelope.preKeyBundleMessage.type === + textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST ) { - await conversation.onFriendRequestAccepted(); - } - - if (envelope.type === textsecure.protobuf.Envelope.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 - ) { + 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(true); + await conversation.setKeyExchangeCompleted(); } } @@ -1289,7 +1240,44 @@ MessageReceiver.prototype.extend({ signature, }; }, - async handlePreKeyBundleMessage(pubKey, preKeyBundleMessage) { + 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; const { @@ -1303,7 +1291,7 @@ MessageReceiver.prototype.extend({ if (pubKey !== StringView.arrayBufferToHex(identityKey)) { throw new Error( - 'Error in handlePreKeyBundleMessage: envelope pubkey does not match pubkey in prekey bundle' + 'Error in savePreKeyBundleMessage: envelope pubkey does not match pubkey in prekey bundle' ); } @@ -1551,8 +1539,7 @@ textsecure.MessageReceiver = function MessageReceiverWrapper( ); this.getStatus = messageReceiver.getStatus.bind(messageReceiver); this.close = messageReceiver.close.bind(messageReceiver); - this.onFriendRequestUpdate = messageReceiver.onFriendRequestUpdate.bind(messageReceiver); - this.handlePreKeyBundleMessage = messageReceiver.handlePreKeyBundleMessage.bind(messageReceiver); + this.savePreKeyBundleMessage = messageReceiver.savePreKeyBundleMessage.bind(messageReceiver); messageReceiver.connect(); }; diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 4bed82dba..a6fab39ac 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -46,7 +46,8 @@ function OutgoingMessage( const { numberInfo, senderCertificate, preKeyBundleType } = options; this.numberInfo = numberInfo; this.senderCertificate = senderCertificate; - this.preKeyBundleType = preKeyBundleType || textsecure.protobuf.PreKeyBundleMessage.Type.UNKNOWN; + this.preKeyBundleType = + preKeyBundleType || textsecure.protobuf.PreKeyBundleMessage.Type.UNKNOWN; } OutgoingMessage.prototype = { @@ -293,10 +294,6 @@ OutgoingMessage.prototype = { const preKeyBundleMessage = await libloki.getPreKeyBundleForNumber(number); preKeyBundleMessage.type = this.preKeyBundleType; - // If we have to use fallback encryption then this must be a friend request - if (this.fallBackEncryption) - preKeyBundleMessage.type = textsecure.protobuf.PreKeyBundleMessage.Type.FRIEND_REQUEST; - const textBundle = this.convertMessageToText(preKeyBundleMessage); const encryptedBundle = await fallBackEncryption.encrypt(textBundle); preKeys = { preKeyBundleMessage: encryptedBundle.body }; @@ -349,7 +346,7 @@ OutgoingMessage.prototype = { // TODO: Allow user to set ttl manually if ( outgoingObject.type === - textsecure.protobuf.Envelope.Type.FRIEND_REQUEST + textsecure.protobuf.Envelope.Type.FALLBACK_CIPHERTEXT ) { ttl = 4 * 24 * 60 * 60; // 4 days for friend request message } else { diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 4a9faa797..6e1cce788 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; - FRIEND_REQUEST = 101; // contains prekeys + message and is using simple encryption + FALLBACK_CIPHERTEXT = 101; // contains prekeys + message and is using simple encryption } optional Type type = 1;