diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index 11a1e7107d..84b58c6bcd 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -652,9 +652,9 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc public void checkNeedsDatabaseReset() { if (TextSecurePreferences.getNeedsDatabaseReset(this)) { - boolean wasUnlinked = TextSecurePreferences.setNeedsDatabaseResetFromUnlink(this); + boolean wasUnlinked = TextSecurePreferences.getWasUnlinked(this); TextSecurePreferences.clearAll(this); - TextSecurePreferences.setNeedDatabaseResetFromUnlink(this, wasUnlinked); // Loki - Re-set the preference so we can use it in the starting screen to determine whether device was unlinked or not + TextSecurePreferences.setWasUnlinked(this, wasUnlinked); // Loki - Re-set the preference so we can use it in the starting screen to determine whether device was unlinked or not MasterSecretUtil.clear(this); if (this.deleteDatabase("signal.db")) { Log.d("Loki", "Deleted database"); diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index ed7957aa6a..e5079b3081 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -99,7 +99,7 @@ public class GroupMessageProcessor { } // Loki - Ignore message if needed - if (ClosedGroupsProtocol.shouldIgnoreMessage(context, group)) { + if (ClosedGroupsProtocol.shouldIgnoreGroupCreatedMessage(context, group)) { return null; } diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java index 43af6a4aa6..24f803c8ee 100644 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java @@ -80,7 +80,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy this(context, address, true); } - private MultiDeviceContactUpdateJob(@NonNull Context context, @Nullable Address address, boolean forceSync) { + public MultiDeviceContactUpdateJob(@NonNull Context context, @Nullable Address address, boolean forceSync) { this(new Job.Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setQueue("MultiDeviceContactUpdateJob") diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 78617bef1f..562511853e 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -68,15 +68,18 @@ import org.thoughtcrime.securesms.linkpreview.Link; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.loki.FriendRequestHandler; -import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; -import org.thoughtcrime.securesms.loki.protocol.LokiSessionResetImplementation; -import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; -import org.thoughtcrime.securesms.loki.MultiDeviceUtilities; import org.thoughtcrime.securesms.loki.activities.HomeActivity; -import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities; +import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.database.LokiPreKeyBundleDatabase; +import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; +import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol; +import org.thoughtcrime.securesms.loki.protocol.FriendRequestProtocol; +import org.thoughtcrime.securesms.loki.protocol.LokiSessionResetImplementation; +import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol; +import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; +import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol; import org.thoughtcrime.securesms.loki.utilities.Broadcaster; +import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities; import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.MmsException; @@ -144,7 +147,7 @@ import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager; import org.whispersystems.signalservice.loki.protocol.meta.LokiServiceMessage; import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink; import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLinkingSession; -import org.whispersystems.signalservice.loki.protocol.multidevice.LokiDeviceLinkUtilities; +import org.whispersystems.signalservice.loki.protocol.sessionmanagement.SessionManagementProtocol; import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus; import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus; import org.whispersystems.signalservice.loki.utilities.PromiseUtil; @@ -285,9 +288,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType { SignalServiceContent content = cipher.decrypt(envelope); - // Loki - Ignore any friend requests that we got before restoration - if (content.isFriendRequest() && content.getTimestamp() < TextSecurePreferences.getRestorationTime(context)) { - Log.d("Loki", "Ignoring friend request received before restoration."); + // Loki - Ignore any friend requests from before restoration + if (FriendRequestProtocol.isFriendRequestFromBeforeRestoration(content)) { + Log.d("Loki", "Ignoring friend request from before restoration."); return; } @@ -297,18 +300,15 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } // Loki - Handle friend request acceptance if needed - if (!content.isFriendRequest() && !isGroupChatMessage(content)) { - becomeFriendsWithContactIfNeeded(content.getSender(), true, false); - } + FriendRequestProtocol.handleFriendRequestAcceptanceIfNeeded(content); - // Loki - Handle session request if needed - handleSessionRequestIfNeeded(content); + // Loki - Handle pre key bundle message if needed + SessionManagementProtocol.handlePreKeyBundleMessageIfNeeded(content); - // Loki - Store pre key bundle if needed - if (!content.getDeviceLink().isPresent()) { - storePreKeyBundleIfNeeded(content); - } + // Loki - Handle session request if needed + SessionManagementProtocol.handleSessionRequestIfNeeded(content); + // Loki - Handle address message if needed if (content.lokiServiceMessage.isPresent()) { LokiServiceMessage lokiMessage = content.lokiServiceMessage.get(); if (lokiMessage.getAddressMessage() != null) { @@ -316,47 +316,33 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } - // Loki - Store the sender display name if needed - Optional rawSenderDisplayName = content.senderDisplayName; - if (rawSenderDisplayName.isPresent() && rawSenderDisplayName.get().length() > 0) { - // If we got a name from our master device then set our display name to match - String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context); - if (ourMasterDevice != null && content.getSender().equals(ourMasterDevice)) { - TextSecurePreferences.setProfileName(context, rawSenderDisplayName.get()); - } - - // If we receive a message from our device then don't set the display name in the database (as we probably have a alias set for them) - MultiDeviceUtilities.isOneOfOurDevices(context, Address.fromSerialized(content.getSender())).success( isOneOfOurDevices -> { - if (!isOneOfOurDevices) { setDisplayName(content.getSender(), rawSenderDisplayName.get()); } - return Unit.INSTANCE; - }); - } + // Loki - Handle profile update if needed + SessionMetaProtocol.handleProfileUpdateIfNeeded(content); if (content.getDeviceLink().isPresent()) { - handleDeviceLinkMessage(content.getDeviceLink().get(), content); + MultiDeviceProtocol.handleDeviceLinkMessageIfNeeded(content); } else if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent(); - if (!content.isFriendRequest() && message.isUnlinkingRequest()) { - // Make sure we got the request from our master device - String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context); - if (ourMasterDevice != null && ourMasterDevice.equals(content.getSender())) { - TextSecurePreferences.setNeedDatabaseResetFromUnlink(context, true); - MultiDeviceUtilities.checkIsRevokedSlaveDevice(context); - } + // Loki - Handle unlinking request if needed + if (message.isUnlinkingRequest()) { + MultiDeviceProtocol.handleUnlinkingRequest(message); } else { - // Loki - Don't process session restore message any further + // Loki - Don't process session restoration requests or session requests any further if (message.isSessionRestorationRequest() || message.isSessionRequest()) { return; } - if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId); - else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId); - else if (message.isExpirationUpdate()) + if (message.isEndSession()) { + handleEndSessionMessage(content, smsMessageId); + } else if (message.isGroupUpdate()) { + handleGroupMessage(content, message, smsMessageId); + } else if (message.isExpirationUpdate()) { handleExpirationUpdate(content, message, smsMessageId); - else if (isMediaMessage) + } else if (isMediaMessage) { handleMediaMessage(content, message, smsMessageId, Optional.absent()); - else if (message.getBody().isPresent()) + } else if (message.getBody().isPresent()) { handleTextMessage(content, message, smsMessageId, Optional.absent()); + } if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get()))) { handleUnknownGroupMessage(content, message.getGroupInfo().get()); @@ -366,25 +352,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType { handleProfileKey(content, message); } - // Loki - This doesn't get invoked for group chats if (content.isNeedsReceipt()) { handleNeedsDeliveryReceipt(content, message); } - // If we received a friend request, but we were already friends with the user, reset the session - if (content.isFriendRequest() && !message.isGroupMessage()) { - Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false); - ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); - long threadID = threadDatabase.getThreadIdIfExistsFor(sender); - if (lokiThreadDatabase.getFriendRequestStatus(threadID) == LokiThreadFriendRequestStatus.FRIENDS) { - resetSession(content.getSender()); - // Let our other devices know that we have reset the session - MessageSender.syncContact(context, sender.getAddress()); - } - } - - // Loki - Handle friend request logic if needed - updateFriendRequestStatusIfNeeded(content, message); + // Loki - Handle friend request message if needed + FriendRequestProtocol.handleFriendRequestMessageIfNeeded(content); } } else if (content.getSyncMessage().isPresent()) { TextSecurePreferences.setMultiDevice(context, true); @@ -557,26 +530,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } if (threadId != null) { - resetSession(content.getSender()); + SessionManagementProtocol.handleEndSessionMessage(content); MessageNotifier.updateNotification(context, threadId); } } - private void resetSession(String hexEncodedPublicKey) { - TextSecureSessionStore sessionStore = new TextSecureSessionStore(context); - LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context); - - Log.d("Loki", "Received a session reset request from: " + hexEncodedPublicKey + "; archiving the session."); - - sessionStore.archiveAllSessions(hexEncodedPublicKey); - lokiThreadDatabase.setSessionResetStatus(hexEncodedPublicKey, LokiSessionResetStatus.REQUEST_RECEIVED); - - Log.d("Loki", "Sending a ping back to " + hexEncodedPublicKey + "."); - MessageSender.sendBackgroundMessage(context, hexEncodedPublicKey); - - SecurityEvent.broadcastSecurityUpdateEvent(context); - } - private long handleSynchronizeSentEndSessionMessage(@NonNull SentTranscriptMessage message) { SmsDatabase database = DatabaseFactory.getSmsDatabase(context); @@ -688,92 +646,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } - private void handleContactSyncMessage(@NonNull ContactsMessage contactsMessage) { - if (!contactsMessage.getContactsStream().isStream()) { return; } - Log.d("Loki", "Received contact sync message."); - - try { - InputStream in = contactsMessage.getContactsStream().asStream().getInputStream(); - DeviceContactsInputStream contactsInputStream = new DeviceContactsInputStream(in); - List deviceContacts = contactsInputStream.readAll(); - for (DeviceContact deviceContact : deviceContacts) { - // Check if we have the contact as a friend and that we're not trying to sync our own device - String hexEncodedPublicKey = deviceContact.getNumber(); - Address address = Address.fromSerialized(hexEncodedPublicKey); - if (!address.isPhone() || address.toPhoneString().equals(TextSecurePreferences.getLocalNumber(context))) { continue; } - - /* - If we're not friends with the contact we received or our friend request expired then we should send them a friend request. - Otherwise, if we have received a friend request from them, automatically accept the friend request. - */ - Recipient recipient = Recipient.from(context, address, false); - long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); - LokiThreadFriendRequestStatus status = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID); - if (status == LokiThreadFriendRequestStatus.NONE || status == LokiThreadFriendRequestStatus.REQUEST_EXPIRED) { - // TODO: We should ensure that our mapping has been uploaded to the server before sending out this message - MessageSender.sendBackgroundFriendRequest(context, hexEncodedPublicKey, "Please accept to enable messages to be synced across devices"); - Log.d("Loki", "Sent friend request to " + hexEncodedPublicKey); - } else if (status == LokiThreadFriendRequestStatus.REQUEST_RECEIVED) { - // Accept the incoming friend request - becomeFriendsWithContactIfNeeded(hexEncodedPublicKey, false, false); - // Send them an accept message back - MessageSender.sendBackgroundMessage(context, hexEncodedPublicKey); - Log.d("Loki", "Became friends with " + deviceContact.getNumber()); - } - - // TODO: Handle blocked - If user is not blocked then we should do the friend request logic otherwise add them to our block list - // TODO: Handle expiration timer - Update expiration timer? - // TODO: Handle avatar - Download and set avatar? - } - } catch (Exception e) { - Log.d("Loki", "Failed to sync contact: " + e + "."); - } - } - - private void handleGroupSyncMessage(@NonNull SignalServiceContent content, @NonNull SignalServiceAttachment groupMessage) { - if (groupMessage.isStream()) { - Log.d("Loki", "Received a group sync message."); - try { - InputStream in = groupMessage.asStream().getInputStream(); - DeviceGroupsInputStream groupsInputStream = new DeviceGroupsInputStream(in); - List groups = groupsInputStream.readAll(); - for (DeviceGroup group : groups) { - SignalServiceGroup serviceGroup = new SignalServiceGroup( - SignalServiceGroup.Type.UPDATE, - group.getId(), - SignalServiceGroup.GroupType.SIGNAL, - group.getName().orNull(), - group.getMembers(), - group.getAvatar().orNull(), - group.getAdmins() - ); - SignalServiceDataMessage dataMessage = new SignalServiceDataMessage(content.getTimestamp(), serviceGroup, null, null); - GroupMessageProcessor.process(context, content, dataMessage, false); - } - } catch (Exception e) { - Log.d("Loki", "Failed to sync group due to error: " + e + "."); - } - } - } - - private void handleOpenGroupSyncMessage(@NonNull List openGroups) { - try { - for (LokiPublicChat openGroup : openGroups) { - long threadID = GroupManager.getOpenGroupThreadID(openGroup.getId(), context); - if (threadID > -1) continue; - - String url = openGroup.getServer(); - long channel = openGroup.getChannel(); - OpenGroupUtilities.addGroup(context, url, channel).fail(e -> { - Log.d("Loki", "Failed to sync open group: " + url + " due to error: " + e + "."); - return Unit.INSTANCE; - }); - } - } catch (Exception e) { - Log.d("Loki", "Failed to sync open groups due to error: " + e + "."); - } - } - private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content, @NonNull SentTranscriptMessage message) throws StorageFailedException @@ -800,8 +672,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get()); } - String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context); - boolean isSenderMasterDevice = ourMasterDevice != null && ourMasterDevice.equals(content.getSender()); if (message.getMessage().getProfileKey().isPresent()) { Recipient recipient = null; @@ -813,16 +683,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType { DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); } - // Loki - If we received a sync message from our master device then we need to extract the profile picture url - if (isSenderMasterDevice) { - handleProfileKey(content, message.getMessage()); - } + // Loki - Handle profile key update if needed + handleProfileKey(content, message.getMessage()); } - // Loki - Update display name from master device - if (isSenderMasterDevice && content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) { - TextSecurePreferences.setProfileName(context, content.senderDisplayName.get()); - } + // Loki - Update profile if needed + SessionMetaProtocol.handleProfileUpdateIfNeeded(content); if (threadId != null) { DatabaseFactory.getThreadDatabase(context).setRead(threadId, true); @@ -913,17 +779,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType { Optional> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or("")); Optional sticker = getStickerAttachment(message.getSticker()); - Address sender = masterRecipient.getAddress(); + Address masterAddress = masterRecipient.getAddress(); - // If message is from group then we need to map it to get the sender of the message if (message.isGroupMessage()) { - sender = getMasterRecipient(content.getSender()).getAddress(); + masterAddress = getMasterRecipient(content.getSender()).getAddress(); } - // Ignore messages from ourselves - if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; } - - IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender, message.getTimestamp(), -1, + IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1, message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), quote, sharedContacts, linkPreviews, sticker); @@ -967,19 +829,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType { MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); } - // Loki - Run database updates in the background, we should look into fixing this in the future - AsyncTask.execute(() -> { - // Loki - Store message server ID - updateGroupChatMessageServerID(messageServerIDOrNull, insertResult); + // Loki - Store message server ID if needed + updateGroupChatMessageServerID(messageServerIDOrNull, insertResult); - // Loki - Update mapping of message to original thread ID - if (insertResult.isPresent()) { - ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); - LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); - long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient); - lokiMessageDatabase.setOriginalThreadID(insertResult.get().getMessageId(), originalThreadId); - } - }); + // Loki - Update mapping of message ID to original thread ID + if (insertResult.isPresent()) { + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); + LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); + long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient); + lokiMessageDatabase.setOriginalThreadID(insertResult.get().getMessageId(), originalThreadId); + } } private long handleSynchronizeSentExpirationUpdate(@NonNull SentTranscriptMessage message) throws MmsException { @@ -1108,14 +967,10 @@ public class PushDecryptJob extends BaseJob implements InjectableType { Address sender = masterRecipient.getAddress(); - // If message is from group then we need to map it to get the sender of the message if (message.isGroupMessage()) { sender = getMasterRecipient(content.getSender()).getAddress(); } - // Ignore messages from ourselves - if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; } - IncomingTextMessage tm = new IncomingTextMessage(sender, content.getSenderDevice(), message.getTimestamp(), body, @@ -1141,249 +996,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType { MessageNotifier.updateNotification(context, threadId); } - // Loki - Run database updates in background, we should look into fixing this in the future - AsyncTask.execute(() -> { - if (insertResult.isPresent()) { - InsertResult result = insertResult.get(); - // Loki - Cache the user hex encoded public key (for mentions) - MentionManagerUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(result.getThreadId(), context); - MentionsManager.INSTANCE.cache(textMessage.getSender().serialize(), result.getThreadId()); - - // Loki - Store message server ID - updateGroupChatMessageServerID(messageServerIDOrNull, insertResult); - - // Loki - Update mapping of message to original thread ID - if (result.getMessageId() > -1) { - ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); - LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); - long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient); - lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId); - } - } - }); - } - } - - private boolean isValidDeviceLinkMessage(@NonNull DeviceLink authorisation) { - boolean isSecondaryDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null; - String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context); - boolean isRequest = (authorisation.getType() == DeviceLink.Type.REQUEST); - if (authorisation.getRequestSignature() == null) { - Log.d("Loki", "Ignoring pairing request message without a request signature."); - return false; - } else if (isRequest && isSecondaryDevice) { - Log.d("Loki", "Ignoring unexpected pairing request message (the device is already paired as a secondary device)."); - return false; - } else if (isRequest && !authorisation.getMasterHexEncodedPublicKey().equals(userHexEncodedPublicKey)) { - Log.d("Loki", "Ignoring pairing request message addressed to another user."); - return false; - } else if (isRequest && authorisation.getSlaveHexEncodedPublicKey().equals(userHexEncodedPublicKey)) { - Log.d("Loki", "Ignoring pairing request message from self."); - return false; - } - return authorisation.verify(); - } - - private void handleDeviceLinkMessage(@NonNull DeviceLink deviceLink, @NonNull SignalServiceContent content) { - String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context); - if (deviceLink.getType() == DeviceLink.Type.REQUEST) { - handleDeviceLinkRequestMessage(deviceLink, content); - } else if (deviceLink.getSlaveHexEncodedPublicKey().equals(userHexEncodedPublicKey)) { - handleDeviceLinkAuthorizedMessage(deviceLink, content); - } - } - - private void handleDeviceLinkRequestMessage(@NonNull DeviceLink deviceLink, @NonNull SignalServiceContent content) { - DeviceLinkingSession linkingSession = DeviceLinkingSession.Companion.getShared(); - if (!linkingSession.isListeningForLinkingRequests()) { - new Broadcaster(context).broadcast("unexpectedDeviceLinkRequestReceived"); - return; - } - boolean isValid = isValidDeviceLinkMessage(deviceLink); - if (!isValid) { return; } - storePreKeyBundleIfNeeded(content); - linkingSession.processLinkingRequest(deviceLink); - } - - private void handleDeviceLinkAuthorizedMessage(@NonNull DeviceLink deviceLink, @NonNull SignalServiceContent content) { - // Check preconditions - boolean hasExistingDeviceLink = TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null; - if (hasExistingDeviceLink) { - Log.d("Loki", "Ignoring unexpected device link message (the device is already linked as a slave device)."); - return; - } - boolean isValid = isValidDeviceLinkMessage(deviceLink); - if (!isValid) { - Log.d("Loki", "Ignoring invalid device link message."); - return; - } - if (!DeviceLinkingSession.Companion.getShared().isListeningForLinkingRequests()) { - Log.d("Loki", "Ignoring device link message."); - return; - } - if (deviceLink.getType() != DeviceLink.Type.AUTHORIZATION) { return; } - Log.d("Loki", "Received device link authorized message from: " + deviceLink.getMasterHexEncodedPublicKey() + "."); - // Save pre key bundle if we somehow got one - storePreKeyBundleIfNeeded(content); - // Process - DeviceLinkingSession.Companion.getShared().processLinkingAuthorization(deviceLink); - // Store the master device's ID - String userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context); - DatabaseFactory.getLokiAPIDatabase(context).clearDeviceLinks(userHexEncodedPublicKey); - DatabaseFactory.getLokiAPIDatabase(context).addDeviceLink(deviceLink); - TextSecurePreferences.setMasterHexEncodedPublicKey(context, deviceLink.getMasterHexEncodedPublicKey()); - TextSecurePreferences.setMultiDevice(context, true); - // Send a background message to the master device - MessageSender.sendBackgroundMessage(context, deviceLink.getMasterHexEncodedPublicKey()); - /* - Update device link on the file server. - We put this here because after receiving the authorisation message, we will also receive all sync messages. - If these sync messages are contact syncs then we need to send them friend requests so that we can establish multi-device communication. - If our device mapping is not stored on the server before the other party receives our message, they will think that they got a friend request from a non-multi-device user. - */ - try { - PromiseUtil.timeout(LokiFileServerAPI.shared.addDeviceLink(deviceLink), 8000).get(); - } catch (Exception e) { - Log.w("Loki", "Failed to upload device links to the file server! " + e); - } - // Update display name if needed - if (content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) { - TextSecurePreferences.setProfileName(context, content.senderDisplayName.get()); - } - // Update profile picture if needed - if (content.getDataMessage().isPresent()) { - handleProfileKey(content, content.getDataMessage().get()); - } - // Handle contact sync if needed - if (content.getSyncMessage().isPresent() && content.getSyncMessage().get().getContacts().isPresent()) { - handleContactSyncMessage(content.getSyncMessage().get().getContacts().get()); - } - } + if (insertResult.isPresent()) { + InsertResult result = insertResult.get(); - private void setDisplayName(String hexEncodedPublicKey, String profileName) { - String displayName = profileName + " (..." + hexEncodedPublicKey.substring(hexEncodedPublicKey.length() - 8) + ")"; - DatabaseFactory.getLokiUserDatabase(context).setDisplayName(hexEncodedPublicKey, displayName); - } + // Loki - Cache the user hex encoded public key (for mentions) + MentionManagerUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(result.getThreadId(), context); + MentionsManager.shared.cache(textMessage.getSender().serialize(), result.getThreadId()); - private void updateGroupChatMessageServerID(Optional messageServerIDOrNull, Optional insertResult) { - if (!insertResult.isPresent() || !messageServerIDOrNull.isPresent()) { return; } - long messageID = insertResult.get().getMessageId(); - long messageServerID = messageServerIDOrNull.get(); - DatabaseFactory.getLokiMessageDatabase(context).setServerID(messageID, messageServerID); - } - - private void storePreKeyBundleIfNeeded(@NonNull SignalServiceContent content) { - Recipient sender = Recipient.from(context, Address.fromSerialized(content.getSender()), false); - if (sender.isGroupRecipient() || !content.lokiServiceMessage.isPresent()) { return; } - LokiServiceMessage lokiMessage = content.lokiServiceMessage.get(); - if (lokiMessage.getPreKeyBundleMessage() == null) { return; } - int registrationID = TextSecurePreferences.getLocalRegistrationId(context); - LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context); - if (registrationID <= 0) { return; } - Log.d("Loki", "Received a pre key bundle from: " + content.getSender() + "."); - PreKeyBundle preKeyBundle = lokiMessage.getPreKeyBundleMessage().getPreKeyBundle(registrationID); - lokiPreKeyBundleDatabase.setPreKeyBundle(content.getSender(), preKeyBundle); + // Loki - Store message server ID + updateGroupChatMessageServerID(messageServerIDOrNull, insertResult); - } - - private void handleSessionRequestIfNeeded(@NonNull SignalServiceContent content) { - if (!content.isFriendRequest() || !isSessionRequest(content)) { return; } - // Check if the session request came from a member in one of our groups or one of our friends - LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(content.getSender()).success(masterHexEncodedPublicKey -> { - String sender = masterHexEncodedPublicKey != null ? masterHexEncodedPublicKey : content.getSender(); - long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(sender), false)); - LokiThreadFriendRequestStatus threadFriendRequestStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID); - boolean isOurFriend = threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS; - boolean isInOneOfOurGroups = DatabaseFactory.getGroupDatabase(context).isClosedGroupMember(sender); - boolean shouldAcceptSessionRequest = isOurFriend || isInOneOfOurGroups; - if (shouldAcceptSessionRequest) { - MessageSender.sendBackgroundMessage(context, content.getSender()); // Send a background message to acknowledge - } - return Unit.INSTANCE; - }); - } - - private void becomeFriendsWithContactIfNeeded(String hexEncodedPublicKey, boolean requiresContactSync, boolean canSkip) { - // Ignore friend requests to group recipients - LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context); - Recipient contactID = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false); - if (contactID.isGroupRecipient()) return; - // Ignore friend requests to recipients we're already friends with - long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(contactID); - LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID); - if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.FRIENDS) { return; } - // We shouldn't be able to skip from NONE to FRIENDS under normal circumstances. - // Multi-device is the one exception to this rule because we want to automatically become friends with slave devices. - if (!canSkip && threadFriendRequestStatus == LokiThreadFriendRequestStatus.NONE) { return; } - // If the thread's friend request status is not `FRIENDS` or `NONE`, but we're receiving a message, - // it must be a friend request accepted message. Declining a friend request doesn't send a message. - lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS); - // Send out a contact sync message if needed - if (requiresContactSync) { - MessageSender.syncContact(context, contactID.getAddress()); - } - // Enable profile sharing with the recipient - DatabaseFactory.getRecipientDatabase(context).setProfileSharing(contactID, true); - // Update the last message if needed - LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(hexEncodedPublicKey).success( masterHexEncodedPublicKey -> { - Util.runOnMain(() -> { - long masterThreadID = (masterHexEncodedPublicKey == null) ? threadID : DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(masterHexEncodedPublicKey), false)); - FriendRequestHandler.updateLastFriendRequestMessage(context, masterThreadID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED); - }); - return Unit.INSTANCE; - }); - } - - private void updateFriendRequestStatusIfNeeded(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) { - if (!content.isFriendRequest() || message.isGroupMessage() || message.isSessionRequest()) { return; } - Promise promise = PromiseUtil.timeout(MultiDeviceUtilities.shouldAutomaticallyBecomeFriendsWithDevice(content.getSender(), context), 8000); - boolean shouldBecomeFriends = PromiseUtil.get(promise, false); - if (shouldBecomeFriends) { - // Become friends AND update the message they sent - becomeFriendsWithContactIfNeeded(content.getSender(), true, true); - // Send them an accept message back - MessageSender.sendBackgroundMessage(context, content.getSender()); - } else { - // Do regular friend request logic checks - Recipient originalRecipient = getRecipientForMessage(content, message); - Recipient masterRecipient = getMasterRecipientForMessage(content, message); - LokiThreadDatabase lokiThreadDatabase = DatabaseFactory.getLokiThreadDatabase(context); - - // Loki - Friend requests only work in direct chats - if (!originalRecipient.getAddress().isPhone()) { return; } - - long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(originalRecipient); - long primaryDeviceThreadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(masterRecipient); - LokiThreadFriendRequestStatus threadFriendRequestStatus = lokiThreadDatabase.getFriendRequestStatus(threadID); - - if (threadFriendRequestStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) { - // This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his - // mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request - // and send a friend request accepted message back to Bob. We don't check that sending the - // friend request accepted message succeeded. Even if it doesn't, the thread's current friend - // request status will be set to `FRIENDS` for Alice making it possible - // for Alice to send messages to Bob. When Bob receives a message, his thread's friend request status - // will then be set to `FRIENDS`. If we do check for a successful send - // before updating Alice's thread's friend request status to `FRIENDS`, - // we can end up in a deadlock where both users' threads' friend request statuses are - // `REQUEST_SENT`. - lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS); - // Since messages are forwarded to the primary device thread, we need to update it there - FriendRequestHandler.updateLastFriendRequestMessage(context, primaryDeviceThreadID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED); - // Accept the friend request - MessageSender.sendBackgroundMessage(context, content.getSender()); - // Send contact sync message - MessageSender.syncContact(context, originalRecipient.getAddress()); - } else if (threadFriendRequestStatus != LokiThreadFriendRequestStatus.FRIENDS) { - // Checking that the sender of the message isn't already a friend is necessary because otherwise - // the following situation can occur: Alice and Bob are friends. Bob loses his database and his - // friend request status is reset to `NONE`. Bob now sends Alice a friend - // request. Alice's thread's friend request status is reset to - // `REQUEST_RECEIVED`. - lokiThreadDatabase.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_RECEIVED); - - // Since messages are forwarded to the primary device thread, we need to update it there - FriendRequestHandler.receivedIncomingFriendRequestMessage(context, primaryDeviceThreadID); + // Loki - Update mapping of message to original thread ID + if (result.getMessageId() > -1) { + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); + LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); + long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient); + lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId); + } } } } @@ -1464,21 +1093,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } - private SmsMessageRecord getLastMessage(String sender) { - try { - SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); - Recipient recipient = Recipient.from(context, Address.fromSerialized(sender), false); - long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipient); - if (threadID < 0) { return null; } - int messageCount = smsDatabase.getMessageCountForThread(threadID); - if (messageCount <= 0) { return null; } - long lastMessageID = smsDatabase.getIDForMessageAtIndex(threadID, messageCount - 1); - return smsDatabase.getMessage(lastMessageID); - } catch (Exception e) { - return null; - } - } - private void handleCorruptMessage(@NonNull String sender, int senderDevice, long timestamp, @NonNull Optional smsMessageId) { @@ -1521,14 +1135,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { triggerSessionRestorePrompt(sender); } - private void triggerSessionRestorePrompt(@NonNull String sender) { - Recipient primaryRecipient = getMasterRecipient(sender); - if (!primaryRecipient.isGroupRecipient()) { - long threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(primaryRecipient); - DatabaseFactory.getLokiThreadDatabase(context).addSessionRestoreDevice(threadID, sender); - } - } - private void handleLegacyMessage(@NonNull String sender, int senderDevice, long timestamp, @NonNull Optional smsMessageId) { @@ -1580,10 +1186,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { String url = content.senderProfilePictureURL.or(""); ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileAvatarJob(recipient, url)); - // Loki - If the recipient is our master device then we need to go and update our avatar mappings on the public chats - if (recipient.isUserMasterDevice()) { - ApplicationContext.getInstance(context).updatePublicChatProfilePictureIfNeeded(); - } + SessionMetaProtocol.handleProfileKeyUpdateIfNeeded(content, message); } } @@ -1647,7 +1250,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { long threadId; if (typingMessage.getGroupId().isPresent()) { - // Typing messages should only apply to signal groups, thus we use `getEncodedId` + // Typing messages should only apply to closed groups, thus we use `getEncodedId` Address groupAddress = Address.fromSerialized(GroupUtil.getEncodedId(typingMessage.getGroupId().get(), false)); Recipient groupRecipient = Recipient.from(context, groupAddress, false); @@ -1814,45 +1417,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType { } } - private Recipient getRecipientForMessage(SignalServiceContent content, SignalServiceDataMessage message) { - if (message.isGroupMessage()) { - return Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get())), false); - } else { - return Recipient.from(context, Address.fromSerialized(content.getSender()), false); - } - } - - private Recipient getMasterRecipientForMessage(SignalServiceContent content, SignalServiceDataMessage message) { - if (message.isGroupMessage()) { - return getRecipientForMessage(content, message); - } else { - return getMasterRecipient(content.getSender()); - } - } - - /** - * Get the master device recipient of the provided device. - * - * If the device doesn't have a master device this will return the same device. - * If the device is our master device then it will return our current device. - * Otherwise it will return the master device. - */ - private Recipient getMasterRecipient(String hexEncodedPublicKey) { - try { - String masterHexEncodedPublicKey = PromiseUtil.timeout(LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(hexEncodedPublicKey), 5000).get(); - String targetHexEncodedPublicKey = (masterHexEncodedPublicKey != null) ? masterHexEncodedPublicKey : hexEncodedPublicKey; - // If the public key matches our master device then we need to forward the message to ourselves (note to self) - String ourMasterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context); - if (ourMasterHexEncodedPublicKey != null && ourMasterHexEncodedPublicKey.equals(targetHexEncodedPublicKey)) { - targetHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context); - } - return Recipient.from(context, Address.fromSerialized(targetHexEncodedPublicKey), false); - } catch (Exception e) { - Log.d("Loki", "Failed to get master device for: " + hexEncodedPublicKey + ". " + e.getMessage()); - return Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false); - } - } - private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull String sender, int device) { Recipient author = Recipient.from(context, Address.fromSerialized(sender), false); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(conversationRecipient); @@ -1895,53 +1459,20 @@ public class PushDecryptJob extends BaseJob implements InjectableType { boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get()); boolean isLeaveMessage = message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() == SignalServiceGroup.Type.QUIT; - boolean isClosedGroup = conversation.getAddress().isClosedGroup(); - boolean isGroupMember = true; - - // Only allow messages from group members - if (isClosedGroup) { - String senderHexEncodedPublicKey = content.getSender(); - - try { - String masterHexEncodedPublicKey = PromiseUtil.timeout(LokiDeviceLinkUtilities.INSTANCE.getMasterHexEncodedPublicKey(content.getSender()), 5000).get(); - if (masterHexEncodedPublicKey != null) { - senderHexEncodedPublicKey = masterHexEncodedPublicKey; - } - } catch (Exception e) { - e.printStackTrace(); - } - - Recipient senderMasterAddress = Recipient.from(context, Address.fromSerialized(senderHexEncodedPublicKey), false); - - isGroupMember = groupId.isPresent() && groupDatabase.getGroupMembers(groupId.get(), true).contains(senderMasterAddress); - } - - return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage) || (isContentMessage && !isGroupMember); + boolean shouldIgnoreContentMessage = ClosedGroupsProtocol.shouldIgnoreContentMessage(context, conversation, groupId.orNull(), content); + return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage) || (isContentMessage && !shouldIgnoreContentMessage); } else { return sender.isBlocked(); } } else if (content.getCallMessage().isPresent() || content.getTypingMessage().isPresent()) { return sender.isBlocked(); } else if (content.getSyncMessage().isPresent()) { - try { - // We should ignore a sync message if the sender is not one of our devices - boolean isOurDevice = PromiseUtil.timeout(MultiDeviceUtilities.isOneOfOurDevices(context, sender.getAddress()), 5000).get(); - if (!isOurDevice) { - Log.w(TAG, "Got a sync message from a device that is not ours!."); - } - return !isOurDevice; - } catch (Exception e) { - return true; - } + return SyncMessagesProtocol.shouldIgnoreSyncMessage(context, sender); } return false; } - private boolean isSessionRequest(SignalServiceContent content) { - return content.getDataMessage().isPresent() && content.getDataMessage().get().isSessionRequest(); - } - private boolean isGroupChatMessage(SignalServiceContent content) { return content.getDataMessage().isPresent() && content.getDataMessage().get().isGroupMessage(); } diff --git a/src/org/thoughtcrime/securesms/loki/PushMessageSyncSendJob.kt b/src/org/thoughtcrime/securesms/loki/PushMessageSyncSendJob.kt deleted file mode 100644 index 87d19cf9c2..0000000000 --- a/src/org/thoughtcrime/securesms/loki/PushMessageSyncSendJob.kt +++ /dev/null @@ -1,93 +0,0 @@ -package org.thoughtcrime.securesms.loki - -import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil -import org.thoughtcrime.securesms.database.Address -import org.thoughtcrime.securesms.dependencies.InjectableType -import org.thoughtcrime.securesms.jobmanager.Data -import org.thoughtcrime.securesms.jobmanager.Job -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint -import org.thoughtcrime.securesms.jobs.BaseJob -import org.thoughtcrime.securesms.recipients.Recipient -import org.whispersystems.signalservice.api.SignalServiceMessageSender -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException -import org.whispersystems.signalservice.api.push.SignalServiceAddress -import java.io.IOException -import java.util.concurrent.TimeUnit -import javax.inject.Inject - -class PushMessageSyncSendJob private constructor( - parameters: Parameters, - private val messageID: Long, - private val recipient: Address, - private val timestamp: Long, - private val message: ByteArray, - private val ttl: Int -) : BaseJob(parameters), InjectableType { - - companion object { - const val KEY = "PushMessageSyncSendJob" - - private val TAG = PushMessageSyncSendJob::class.java.simpleName - - private val KEY_MESSAGE_ID = "message_id" - private val KEY_RECIPIENT = "recipient" - private val KEY_TIMESTAMP = "timestamp" - private val KEY_MESSAGE = "message" - private val KEY_TTL = "ttl" - } - - @Inject - lateinit var messageSender: SignalServiceMessageSender - - constructor(messageID: Long, recipient: Address, timestamp: Long, message: ByteArray, ttl: Int) : this(Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setQueue(KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(1) - .build(), - messageID, recipient, timestamp, message, ttl) - - override fun serialize(): Data { - return Data.Builder() - .putLong(KEY_MESSAGE_ID, messageID) - .putString(KEY_RECIPIENT, recipient.serialize()) - .putLong(KEY_TIMESTAMP, timestamp) - .putByteArray(KEY_MESSAGE, message) - .putInt(KEY_TTL, ttl) - .build() - } - - override fun getFactoryKey(): String { - return KEY - } - - @Throws(IOException::class, UntrustedIdentityException::class) - public override fun onRun() { - // Don't send sync messages to a group - if (recipient.isGroup || recipient.isEmail) { return } - val unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, recipient, false)) - messageSender.lokiSendSyncMessage(messageID, SignalServiceAddress(recipient.toPhoneString()), unidentifiedAccess, timestamp, message, ttl) - } - - public override fun onShouldRetry(e: Exception): Boolean { - // Loki - Disable since we have our own retrying when sending messages - return false - } - - override fun onCanceled() {} - - class Factory : Job.Factory { - override fun create(parameters: Parameters, data: Data): PushMessageSyncSendJob { - try { - return PushMessageSyncSendJob(parameters, - data.getLong(KEY_MESSAGE_ID), - Address.fromSerialized(data.getString(KEY_RECIPIENT)), - data.getLong(KEY_TIMESTAMP), - data.getByteArray(KEY_MESSAGE), - data.getInt(KEY_TTL)) - } catch (e: IOException) { - throw AssertionError(e) - } - } - } -} diff --git a/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt index 4aecee1905..ccebd29a32 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt @@ -40,7 +40,7 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega registerButton.setOnClickListener { register() } restoreButton.setOnClickListener { restore() } linkButton.setOnClickListener { linkDevice() } - if (TextSecurePreferences.setNeedsDatabaseResetFromUnlink(this)) { + if (TextSecurePreferences.getWasUnlinked(this)) { Toast.makeText(this, "Your device was unlinked successfully", Toast.LENGTH_LONG).show() } } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 0b7b2c3aac..6770c70ded 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -5,11 +5,13 @@ import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.loki.utilities.recipient import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.libsignal.SignalProtocolAddress +import org.whispersystems.signalservice.api.messages.SignalServiceContent import org.whispersystems.signalservice.api.messages.SignalServiceGroup import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol @@ -17,10 +19,20 @@ import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDevicePro object ClosedGroupsProtocol { @JvmStatic - fun shouldIgnoreMessage(context: Context, group: SignalServiceGroup): Boolean { + fun shouldIgnoreContentMessage(context: Context, conversation: Recipient, groupID: String?, content: SignalServiceContent): Boolean { + if (!conversation.address.isClosedGroup || groupID == null) { return false } + val senderPublicKey = content.sender + val senderMasterPublicKey = MultiDeviceProtocol.shared.getMasterDevice(senderPublicKey) + val publicKeyToCheckFor = senderMasterPublicKey ?: senderPublicKey + val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, true) + return !members.contains(recipient(context, publicKeyToCheckFor)) + } + + @JvmStatic + fun shouldIgnoreGroupCreatedMessage(context: Context, group: SignalServiceGroup): Boolean { val members = group.members - val masterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) - return !members.isPresent || !members.get().contains(masterDevice) + val userMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) + return !members.isPresent || !members.get().contains(userMasterDevice) } @JvmStatic @@ -31,19 +43,20 @@ object ClosedGroupsProtocol { result.add(Address.fromSerialized(groupID)) return result } else { + // A closed group's members should never include slave devices val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false) - val recipients = members.flatMap { member -> + val destinations = members.flatMap { member -> MultiDeviceProtocol.shared.getAllLinkedDevices(member.address.serialize()).map { Address.fromSerialized(it) } }.toMutableSet() - val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) - if (masterPublicKey != null && recipients.contains(Address.fromSerialized(masterPublicKey))) { - recipients.remove(Address.fromSerialized(masterPublicKey)) + val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) + if (userMasterPublicKey != null && destinations.contains(Address.fromSerialized(userMasterPublicKey))) { + destinations.remove(Address.fromSerialized(userMasterPublicKey)) } val userPublicKey = TextSecurePreferences.getLocalNumber(context) - if (userPublicKey != null && recipients.contains(Address.fromSerialized(userPublicKey))) { - recipients.remove(Address.fromSerialized(userPublicKey)) + if (userPublicKey != null && destinations.contains(Address.fromSerialized(userPublicKey))) { + destinations.remove(Address.fromSerialized(userPublicKey)) } - return recipients.toList() + return destinations.toList() } } @@ -54,39 +67,36 @@ object ClosedGroupsProtocol { val message = GroupUtil.createGroupLeaveMessage(context, recipient) if (threadID < 0 || !message.isPresent) { return false } MessageSender.send(context, message.get(), threadID, false, null) - // Remove the *master* device from the group + // Remove the master device from the group (a closed group's members should never include slave devices) val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) - val publicKeyToUse = masterPublicKey ?: TextSecurePreferences.getLocalNumber(context) + val publicKeyToRemove = masterPublicKey ?: TextSecurePreferences.getLocalNumber(context) val groupDatabase = DatabaseFactory.getGroupDatabase(context) val groupID = recipient.address.toGroupString() groupDatabase.setActive(groupID, false) - groupDatabase.remove(groupID, Address.fromSerialized(publicKeyToUse)) + groupDatabase.remove(groupID, Address.fromSerialized(publicKeyToRemove)) return true } @JvmStatic fun establishSessionsWithMembersIfNeeded(context: Context, members: List) { + // A closed group's members should never include slave devices val allDevices = members.flatMap { member -> MultiDeviceProtocol.shared.getAllLinkedDevices(member) }.toMutableSet() - val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) - if (masterPublicKey != null && allDevices.contains(masterPublicKey)) { - allDevices.remove(masterPublicKey) + val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) + if (userMasterPublicKey != null && allDevices.contains(userMasterPublicKey)) { + allDevices.remove(userMasterPublicKey) } val userPublicKey = TextSecurePreferences.getLocalNumber(context) if (userPublicKey != null && allDevices.contains(userPublicKey)) { allDevices.remove(userPublicKey) } for (device in allDevices) { - val address = SignalProtocolAddress(device, SignalServiceAddress.DEFAULT_DEVICE_ID) - val hasSession = TextSecureSessionStore(context).containsSession(address) - if (!hasSession) { sendSessionRequest(context, device) } + val deviceAsAddress = SignalProtocolAddress(device, SignalServiceAddress.DEFAULT_DEVICE_ID) + val hasSession = TextSecureSessionStore(context).containsSession(deviceAsAddress) + if (hasSession) { continue } + val sessionRequest = EphemeralMessage.createSessionRequest(device) + ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRequest)) } } - - @JvmStatic - fun sendSessionRequest(context: Context, publicKey: String) { - val sessionRequest = EphemeralMessage.createSessionRequest(publicKey) - ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRequest)) - } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/protocol/EphemeralMessage.kt b/src/org/thoughtcrime/securesms/loki/protocol/EphemeralMessage.kt index c813dc813a..ca46684223 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/EphemeralMessage.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/EphemeralMessage.kt @@ -16,7 +16,7 @@ class EphemeralMessage private constructor(val data: Map<*, *>) { fun createSessionRestorationRequest(publicKey: String) = EphemeralMessage(mapOf( "recipient" to publicKey, "friendRequest" to true, "sessionRestore" to true )) @JvmStatic - fun createSessionRequest(publicKey: String) = EphemeralMessage(mapOf("recipient" to publicKey, "friendRequest" to true, "sessionRequest" to true)) + fun createSessionRequest(publicKey: String) = EphemeralMessage(mapOf( "recipient" to publicKey, "friendRequest" to true, "sessionRequest" to true )) internal fun parse(serialized: String): EphemeralMessage { val data = JsonUtil.fromJson(serialized, Map::class.java) ?: throw IllegalArgumentException("Couldn't parse string to JSON") diff --git a/src/org/thoughtcrime/securesms/loki/protocol/FriendRequestProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/FriendRequestProtocol.kt index 266c47ddb3..985ff815eb 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/FriendRequestProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/FriendRequestProtocol.kt @@ -1,15 +1,122 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.loki.utilities.recipient import org.thoughtcrime.securesms.mms.OutgoingMediaMessage import org.thoughtcrime.securesms.sms.OutgoingTextMessage import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.api.messages.SignalServiceContent +import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus object FriendRequestProtocol { + private fun getLastMessageID(context: Context, threadID: Long): Long? { + val db = DatabaseFactory.getSmsDatabase(context) + val messageCount = db.getMessageCountForThread(threadID) + if (messageCount == 0) { return null } + return db.getIDForMessageAtIndex(threadID, messageCount - 1) + } + + @JvmStatic + fun handleFriendRequestAcceptanceIfNeeded(context: Context, publicKey: String, content: SignalServiceContent) { + // If we get an envelope that isn't a friend request, then we can infer that we had to use + // Signal cipher decryption and thus that we have a session with the other person. + if (content.isFriendRequest) { return } + val recipient = recipient(context, publicKey) + // Friend requests don't apply to groups + if (recipient.isGroupRecipient) { return } + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) + val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context) + val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID) + // Guard against invalid state transitions + if (threadFRStatus != LokiThreadFriendRequestStatus.REQUEST_SENDING && threadFRStatus != LokiThreadFriendRequestStatus.REQUEST_SENT + && threadFRStatus != LokiThreadFriendRequestStatus.REQUEST_RECEIVED) { return } + lokiThreadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS) + val lastMessageID = getLastMessageID(context, threadID) + if (lastMessageID != null) { + DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED) + } + // Send a contact sync message if needed + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey) + if (allUserDevices.contains(publicKey)) { return } + val deviceToSync = MultiDeviceProtocol.shared.getMasterDevice(publicKey) ?: publicKey + SyncMessagesProtocol.syncContact(context, Address.fromSerialized(deviceToSync)) + } + + private fun canFriendRequestBeAutoAccepted(context: Context, publicKey: String): Boolean { + val recipient = recipient(context, publicKey) + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) + val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context) + val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID) + if (threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_SENT) { + // This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his + // mind and sent a friend request to Alice. In this case we want Alice to auto-accept the request + // and send a friend request accepted message back to Bob. We don't check that sending the + // friend request accepted message succeeds. Even if it doesn't, the thread's current friend + // request status will be set to FRIENDS for Alice making it possible for Alice to send messages + // to Bob. When Bob receives a message, his thread's friend request status will then be set to + // FRIENDS. If we do check for a successful send before updating Alice's thread's friend request + // status to FRIENDS, we can end up in a deadlock where both users' threads' friend request statuses + // are SENT. + return true + } + // Auto-accept any friend requests from the user's own linked devices + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey) + if (allUserDevices.contains(publicKey)) { return true } + // Auto-accept if the user is friends with any of the sender's linked devices. + val allSenderDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(publicKey) + if (allSenderDevices.any { device -> + val deviceAsRecipient = recipient(context, publicKey) + val deviceThreadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(deviceAsRecipient) + lokiThreadDB.getFriendRequestStatus(deviceThreadID) == LokiThreadFriendRequestStatus.FRIENDS + }) { + return true + } + return false + } + + @JvmStatic + fun handleFriendRequestMessageIfNeeded(context: Context, publicKey: String, content: SignalServiceContent) { + if (!content.isFriendRequest) { return } + val recipient = recipient(context, publicKey) + // Friend requests don't apply to groups + if (recipient.isGroupRecipient) { return } + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) + val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context) + val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID) + if (canFriendRequestBeAutoAccepted(context, publicKey)) { + lokiThreadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.FRIENDS) + val lastMessageID = getLastMessageID(context, threadID) + if (lastMessageID != null) { + DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_ACCEPTED) + } + val ephemeralMessage = EphemeralMessage.create(publicKey) + ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage)) + } else if (threadFRStatus != LokiThreadFriendRequestStatus.FRIENDS) { + // Checking that the sender of the message isn't already a friend is necessary because otherwise + // the following situation can occur: Alice and Bob are friends. Bob loses his database and his + // friend request status is reset to NONE. Bob now sends Alice a friend request. Alice's thread's + // friend request status is reset to RECEIVED + lokiThreadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_RECEIVED) + val lastMessageID = getLastMessageID(context, threadID) + if (lastMessageID != null) { + DatabaseFactory.getLokiMessageDatabase(context).setFriendRequestStatus(lastMessageID, LokiMessageFriendRequestStatus.REQUEST_PENDING) + } + } + } + + @JvmStatic + fun isFriendRequestFromBeforeRestoration(context: Context, content: SignalServiceContent): Boolean { + return content.isFriendRequest && content.timestamp < TextSecurePreferences.getRestorationTime(context) + } + @JvmStatic fun shouldUpdateFriendRequestStatusFromOutgoingTextMessage(context: Context, message: OutgoingTextMessage): Boolean { // The order of these checks matters @@ -43,4 +150,34 @@ object FriendRequestProtocol { threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_SENDING) } } + + @JvmStatic + fun setFriendRequestStatusToSentIfNeeded(context: Context, messageID: Long, threadID: Long) { + val messageDB = DatabaseFactory.getLokiMessageDatabase(context) + val messageFRStatus = messageDB.getFriendRequestStatus(messageID) + if (messageFRStatus == LokiMessageFriendRequestStatus.NONE || messageFRStatus == LokiMessageFriendRequestStatus.REQUEST_EXPIRED + || messageFRStatus == LokiMessageFriendRequestStatus.REQUEST_SENDING) { + messageDB.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_PENDING) + } + val threadDB = DatabaseFactory.getLokiThreadDatabase(context) + val threadFRStatus = threadDB.getFriendRequestStatus(threadID) + if (threadFRStatus == LokiThreadFriendRequestStatus.NONE || threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_EXPIRED + || threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_SENDING) { + threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_SENT) + } + } + + @JvmStatic + fun setFriendRequestStatusToFailedIfNeeded(context: Context, messageID: Long, threadID: Long) { + val messageDB = DatabaseFactory.getLokiMessageDatabase(context) + val messageFRStatus = messageDB.getFriendRequestStatus(messageID) + if (messageFRStatus == LokiMessageFriendRequestStatus.REQUEST_SENDING) { + messageDB.setFriendRequestStatus(messageID, LokiMessageFriendRequestStatus.REQUEST_FAILED) + } + val threadDB = DatabaseFactory.getLokiThreadDatabase(context) + val threadFRStatus = threadDB.getFriendRequestStatus(threadID) + if (threadFRStatus == LokiThreadFriendRequestStatus.REQUEST_SENDING) { + threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.NONE) + } + } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/protocol/LokiSessionResetImplementation.kt b/src/org/thoughtcrime/securesms/loki/protocol/LokiSessionResetImplementation.kt index 0cec3cb624..2a33dda63f 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/LokiSessionResetImplementation.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/LokiSessionResetImplementation.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context +import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.DatabaseFactory import org.whispersystems.libsignal.loki.LokiSessionResetProtocol import org.whispersystems.libsignal.loki.LokiSessionResetStatus @@ -18,7 +19,8 @@ class LokiSessionResetImplementation(private val context: Context) : LokiSession override fun onNewSessionAdopted(hexEncodedPublicKey: String, oldSessionResetStatus: LokiSessionResetStatus) { if (oldSessionResetStatus == LokiSessionResetStatus.IN_PROGRESS) { - SessionMetaProtocol.sendEphemeralMessage(context, hexEncodedPublicKey) + val ephemeralMessage = EphemeralMessage.create(hexEncodedPublicKey) + ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage)) } // TODO: Show session reset succeed message } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt index 1b60841643..d1068e3756 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt @@ -19,7 +19,6 @@ import javax.inject.Inject class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters) : BaseJob(parameters), InjectableType { companion object { - const val KEY = "MultiDeviceOpenGroupUpdateJob" } @@ -35,7 +34,7 @@ class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters) override fun getFactoryKey(): String { return KEY } - override fun serialize(): Data { return Data.EMPTY } + override fun serialize(): Data { return Data.EMPTY } // TODO: Should we implement this? @Throws(Exception::class) public override fun onRun() { diff --git a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt index e4cf14c5e7..fd055ce8f4 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt @@ -1,26 +1,28 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context +import android.util.Log import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.jobs.PushMediaSendJob import org.thoughtcrime.securesms.jobs.PushSendJob import org.thoughtcrime.securesms.jobs.PushTextSendJob +import org.thoughtcrime.securesms.loki.utilities.Broadcaster import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.api.messages.SignalServiceContent import org.whispersystems.signalservice.loki.api.fileserver.LokiFileServerAPI import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol +import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink +import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLinkingSession import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus object MultiDeviceProtocol { - @JvmStatic - fun sendUnlinkingRequest(context: Context, publicKey: String) { - val unlinkingRequest = EphemeralMessage.createUnlinkingRequest(publicKey) - ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(unlinkingRequest)) - } + // TODO: Closed groups enum class MessageType { Text, Media } @@ -34,7 +36,6 @@ object MultiDeviceProtocol { sendMessagePush(context, recipient, messageID, MessageType.Media) } - // TODO: Closed groups private fun sendMessagePushToDevice(context: Context, recipient: Recipient, messageID: Long, messageType: MessageType): PushSendJob { val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) val threadFRStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID) @@ -92,4 +93,93 @@ object MultiDeviceProtocol { } } } + + @JvmStatic + fun handleDeviceLinkMessageIfNeeded(context: Context, deviceLink: DeviceLink, content: SignalServiceContent) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + if (deviceLink.type == DeviceLink.Type.REQUEST) { + handleDeviceLinkRequestMessage(context, deviceLink, content) + } else if (deviceLink.slaveHexEncodedPublicKey == userPublicKey) { + handleDeviceLinkAuthorizedMessage(context, deviceLink, content) + } + } + + private fun isValidDeviceLinkMessage(context: Context, deviceLink: DeviceLink): Boolean { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val isRequest = (deviceLink.type == DeviceLink.Type.REQUEST) + if (deviceLink.requestSignature == null) { + Log.d("Loki", "Ignoring device link without a request signature.") + return false + } else if (isRequest && TextSecurePreferences.getMasterHexEncodedPublicKey(context) != null) { + Log.d("Loki", "Ignoring unexpected device link message (the device is a slave device).") + return false + } else if (isRequest && deviceLink.masterHexEncodedPublicKey != userPublicKey) { + Log.d("Loki", "Ignoring device linking message addressed to another user.") + return false + } else if (isRequest && deviceLink.slaveHexEncodedPublicKey == userPublicKey) { + Log.d("Loki", "Ignoring device linking request message from self.") + return false + } + return deviceLink.verify() + } + + private fun handleDeviceLinkRequestMessage(context: Context, deviceLink: DeviceLink, content: SignalServiceContent) { + val linkingSession = DeviceLinkingSession.shared + if (!linkingSession.isListeningForLinkingRequests) { + return Broadcaster(context).broadcast("unexpectedDeviceLinkRequestReceived") + } + val isValid = isValidDeviceLinkMessage(context, deviceLink) + if (!isValid) { return } + SessionManagementProtocol.handlePreKeyBundleMessageIfNeeded(context, content) + linkingSession.processLinkingRequest(deviceLink) + } + + private fun handleDeviceLinkAuthorizedMessage(context: Context, deviceLink: DeviceLink, content: SignalServiceContent) { + val linkingSession = DeviceLinkingSession.shared + if (!linkingSession.isListeningForLinkingRequests) { + return + } + val isValid = isValidDeviceLinkMessage(context, deviceLink) + if (!isValid) { return } + SessionManagementProtocol.handlePreKeyBundleMessageIfNeeded(context, content) + linkingSession.processLinkingAuthorization(deviceLink) + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + DatabaseFactory.getLokiAPIDatabase(context).clearDeviceLinks(userPublicKey) + DatabaseFactory.getLokiAPIDatabase(context).addDeviceLink(deviceLink) + TextSecurePreferences.setMasterHexEncodedPublicKey(context, deviceLink.masterHexEncodedPublicKey) + TextSecurePreferences.setMultiDevice(context, true) + LokiFileServerAPI.shared.addDeviceLink(deviceLink) + org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content) + org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.handleProfileKeyUpdateIfNeeded(context, content) + } + + @JvmStatic + fun handleUnlinkingRequestIfNeeded(context: Context, content: SignalServiceContent) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + // Check that the request was sent by the user's master device + val masterDevicePublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) ?: return + val wasSentByMasterDevice = (content.sender == masterDevicePublicKey) + if (!wasSentByMasterDevice) { return } + // Ignore the request if we don't know about the device link in question + val masterDeviceLinks = DatabaseFactory.getLokiAPIDatabase(context).getDeviceLinks(masterDevicePublicKey) + if (masterDeviceLinks.none { + it.masterHexEncodedPublicKey == masterDevicePublicKey && it.slaveHexEncodedPublicKey == userPublicKey + }) { + return + } + LokiFileServerAPI.shared.getDeviceLinks(userPublicKey, true).success { slaveDeviceLinks -> + // Check that the device link IS present on the file server. + // Note that the device link as seen from the master device's perspective has been deleted at this point, but the + // device link as seen from the slave perspective hasn't. + if (slaveDeviceLinks.any { + it.masterHexEncodedPublicKey == masterDevicePublicKey && it.slaveHexEncodedPublicKey == userPublicKey + }) { + for (slaveDeviceLink in slaveDeviceLinks) { // In theory there should only be one + LokiFileServerAPI.shared.removeDeviceLink(slaveDeviceLink) // Attempt to clean up on the file server + } + TextSecurePreferences.setWasUnlinked(context, true) + ApplicationContext.getInstance(context).clearData() + } + } + } } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/PushEphemeralMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/PushEphemeralMessageSendJob.kt index 74cc3fcf3f..0678fc6421 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/PushEphemeralMessageSendJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/PushEphemeralMessageSendJob.kt @@ -18,8 +18,7 @@ import java.util.concurrent.TimeUnit class PushEphemeralMessageSendJob private constructor(parameters: Parameters, private val message: EphemeralMessage) : BaseJob(parameters) { companion object { - private val KEY_MESSAGE = "message" - + private const val KEY_MESSAGE = "message" const val KEY = "PushBackgroundMessageSendJob" } @@ -32,14 +31,13 @@ class PushEphemeralMessageSendJob private constructor(parameters: Parameters, pr message) override fun serialize(): Data { + // TODO: Is this correct? return Data.Builder() .putString(KEY_MESSAGE, message.serialize()) .build() } - override fun getFactoryKey(): String { - return KEY - } + override fun getFactoryKey(): String { return KEY } public override fun onRun() { val recipient = message.get("recipient", null) ?: throw IllegalStateException() diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt index c0f7e7a3e9..15e70decdd 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt @@ -5,8 +5,16 @@ import android.util.Log import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.PreKeyUtil +import org.thoughtcrime.securesms.crypto.SecurityEvent +import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore +import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.jobs.CleanPreKeysJob +import org.thoughtcrime.securesms.loki.utilities.recipient import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.libsignal.loki.LokiSessionResetStatus +import org.whispersystems.signalservice.api.messages.SignalServiceContent +import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus + object SessionManagementProtocol { @@ -24,8 +32,50 @@ object SessionManagementProtocol { } @JvmStatic - fun sendSessionRestorationRequest(context: Context, publicKey: String) { - val sessionRestorationRequest = EphemeralMessage.createSessionRestorationRequest(publicKey) - ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRestorationRequest)) + fun handlePreKeyBundleMessageIfNeeded(context: Context, content: SignalServiceContent) { + val recipient = recipient(context, content.sender) + if (recipient.isGroupRecipient) { return } + val preKeyBundleMessage = content.lokiServiceMessage.orNull()?.preKeyBundleMessage ?: return + val registrationID = TextSecurePreferences.getLocalRegistrationId(context) // TODO: It seems wrong to use the local registration ID for this? + val lokiPreKeyBundleDatabase = DatabaseFactory.getLokiPreKeyBundleDatabase(context) + Log.d("Loki", "Received a pre key bundle from: " + content.sender.toString() + ".") + val preKeyBundle = preKeyBundleMessage.getPreKeyBundle(registrationID) + lokiPreKeyBundleDatabase.setPreKeyBundle(content.sender, preKeyBundle) + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) + val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context) + val threadFRStatus = lokiThreadDB.getFriendRequestStatus(threadID) + // If we received a friend request (i.e. also a new pre key bundle), but we were already friends with the other user, reset the session. + if (content.isFriendRequest && threadFRStatus == LokiThreadFriendRequestStatus.FRIENDS) { + val sessionStore = TextSecureSessionStore(context) + sessionStore.archiveAllSessions(content.sender) + val ephemeralMessage = EphemeralMessage.create(content.sender) + ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage)) + } + } + + @JvmStatic + fun handleSessionRequestIfNeeded(context: Context, content: SignalServiceContent) { + // Auto-accept all session requests + val ephemeralMessage = EphemeralMessage.create(content.sender) + ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage)) + } + + @JvmStatic + fun handleEndSessionMessage(context: Context, content: SignalServiceContent) { + // TODO: Notify the user + val sessionStore = TextSecureSessionStore(context) + val lokiThreadDB = DatabaseFactory.getLokiThreadDatabase(context) + Log.d("Loki", "Received a session reset request from: ${content.sender}; archiving the session.") + sessionStore.archiveAllSessions(content.sender) + lokiThreadDB.setSessionResetStatus(content.sender, LokiSessionResetStatus.REQUEST_RECEIVED) + Log.d("Loki", "Sending an ephemeral message back to: ${content.sender}.") + val ephemeralMessage = EphemeralMessage.create(content.sender) + ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage)) + SecurityEvent.broadcastSecurityUpdateEvent(context) + } + + @JvmStatic + private fun isSessionRequest(content: SignalServiceContent): Boolean { + return content.dataMessage.isPresent && content.dataMessage.get().isSessionRequest } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt index 1f93126208..f62bbd91bd 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt @@ -5,14 +5,38 @@ import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.api.messages.SignalServiceContent +import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus object SessionMetaProtocol { @JvmStatic - fun sendEphemeralMessage(context: Context, publicKey: String) { - val ephemeralMessage = EphemeralMessage.create(publicKey) - ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage)) + fun handleProfileUpdateIfNeeded(context: Context, content: SignalServiceContent) { + val rawDisplayName = content.senderDisplayName.orNull() ?: return + if (rawDisplayName.isBlank()) { return } + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) + val sender = content.sender.toLowerCase() + if (userMasterPublicKey == sender) { + // Update the user's local name if the message came from their master device + TextSecurePreferences.setProfileName(context, rawDisplayName) + } + // Don't overwrite if the message came from a linked device; the device name is + // stored as a user name + val allUserDevices = MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey) + if (!allUserDevices.contains(sender)) { + val displayName = rawDisplayName + " (..." + sender.substring(sender.length - 8) + ")" + DatabaseFactory.getLokiUserDatabase(context).setDisplayName(sender, displayName) + } + } + + @JvmStatic + fun handleProfileKeyUpdateIfNeeded(context: Context, content: SignalServiceContent) { + val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) + if (userMasterPublicKey != content.sender) { return } + ApplicationContext.getInstance(context).updatePublicChatProfilePictureIfNeeded() } /** diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt index 96b09268af..a36750494b 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt @@ -10,12 +10,24 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation import java.util.* object SyncMessagesProtocol { + @JvmStatic + fun shouldIgnoreSyncMessage(context: Context, sender: Recipient): Boolean { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + return MultiDeviceProtocol.shared.getAllLinkedDevices(userPublicKey).contains(sender.address.serialize()) + } + + @JvmStatic + fun syncContact(context: Context, address: Address) { + ApplicationContext.getInstance(context).jobManager.add(MultiDeviceContactUpdateJob(context, address, true)) + } + @JvmStatic fun syncAllContacts(context: Context) { ApplicationContext.getInstance(context).jobManager.add(MultiDeviceContactUpdateJob(context, true)) diff --git a/src/org/thoughtcrime/securesms/loki/utilities/RecipientUtilities.kt b/src/org/thoughtcrime/securesms/loki/utilities/RecipientUtilities.kt new file mode 100644 index 0000000000..0b6d5eee4b --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/utilities/RecipientUtilities.kt @@ -0,0 +1,9 @@ +package org.thoughtcrime.securesms.loki.utilities + +import android.content.Context +import org.thoughtcrime.securesms.database.Address +import org.thoughtcrime.securesms.recipients.Recipient + +fun recipient(context: Context, publicKey: String): Recipient { + return Recipient.from(context, Address.fromSerialized(publicKey), false) +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/push/MessageSenderEventListener.java b/src/org/thoughtcrime/securesms/push/MessageSenderEventListener.java index 43eb0c8523..9b203e34d7 100644 --- a/src/org/thoughtcrime/securesms/push/MessageSenderEventListener.java +++ b/src/org/thoughtcrime/securesms/push/MessageSenderEventListener.java @@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.push; import android.content.Context; import org.thoughtcrime.securesms.crypto.SecurityEvent; -import org.thoughtcrime.securesms.loki.FriendRequestHandler; +import org.thoughtcrime.securesms.loki.protocol.FriendRequestProtocol; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -23,14 +23,14 @@ public class MessageSenderEventListener implements SignalServiceMessageSender.Ev } @Override public void onFriendRequestSending(long messageID, long threadID) { - FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sending, messageID, threadID); + FriendRequestProtocol.setFriendRequestStatusToSendingIfNeeded(context, messageID, threadID); } @Override public void onFriendRequestSent(long messageID, long threadID) { - FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Sent, messageID, threadID); + FriendRequestProtocol.setFriendRequestStatusToSentIfNeeded(context, messageID, threadID); } @Override public void onFriendRequestSendingFailed(long messageID, long threadID) { - FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Failed, messageID, threadID); + FriendRequestProtocol.setFriendRequestStatusToFailedIfNeeded(context, messageID, threadID); } } diff --git a/src/org/thoughtcrime/securesms/registration/WelcomeActivity.java b/src/org/thoughtcrime/securesms/registration/WelcomeActivity.java index cfa8d6e810..d0ce12f074 100644 --- a/src/org/thoughtcrime/securesms/registration/WelcomeActivity.java +++ b/src/org/thoughtcrime/securesms/registration/WelcomeActivity.java @@ -27,7 +27,7 @@ public class WelcomeActivity extends BaseActionBarActivity { @Override protected void onResume() { super.onResume(); - if (TextSecurePreferences.setNeedsDatabaseResetFromUnlink(this)) { + if (TextSecurePreferences.getWasUnlinked(this)) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.dialog_device_unlink_title); builder.setMessage(R.string.dialog_device_unlink_message); @@ -36,7 +36,7 @@ public class WelcomeActivity extends BaseActionBarActivity { @Override public void onDismiss(DialogInterface dialog) { - TextSecurePreferences.setNeedDatabaseResetFromUnlink(getBaseContext(), false); + TextSecurePreferences.setWasUnlinked(getBaseContext(), false); } }); builder.show(); diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 2f6bb402d3..aaf3002d3e 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -1264,12 +1264,12 @@ public class TextSecurePreferences { return getBooleanPreference(context, "database_reset", false); } - public static void setNeedDatabaseResetFromUnlink(Context context, boolean value) { + public static void setWasUnlinked(Context context, boolean value) { // We do it this way so that it gets persisted in storage straight away PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("database_reset_unpair", value).commit(); } - public static boolean setNeedsDatabaseResetFromUnlink(Context context) { + public static boolean getWasUnlinked(Context context) { return getBooleanPreference(context, "database_reset_unpair", false); }