Refactor PushDecryptJob

This commit is contained in:
nielsandriesse 2020-05-13 12:29:31 +10:00
parent afe049c9c3
commit 6205b6c9f6
20 changed files with 462 additions and 693 deletions

View File

@ -652,9 +652,9 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
public void checkNeedsDatabaseReset() { public void checkNeedsDatabaseReset() {
if (TextSecurePreferences.getNeedsDatabaseReset(this)) { if (TextSecurePreferences.getNeedsDatabaseReset(this)) {
boolean wasUnlinked = TextSecurePreferences.setNeedsDatabaseResetFromUnlink(this); boolean wasUnlinked = TextSecurePreferences.getWasUnlinked(this);
TextSecurePreferences.clearAll(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); MasterSecretUtil.clear(this);
if (this.deleteDatabase("signal.db")) { if (this.deleteDatabase("signal.db")) {
Log.d("Loki", "Deleted database"); Log.d("Loki", "Deleted database");

View File

@ -99,7 +99,7 @@ public class GroupMessageProcessor {
} }
// Loki - Ignore message if needed // Loki - Ignore message if needed
if (ClosedGroupsProtocol.shouldIgnoreMessage(context, group)) { if (ClosedGroupsProtocol.shouldIgnoreGroupCreatedMessage(context, group)) {
return null; return null;
} }

View File

@ -80,7 +80,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob implements InjectableTy
this(context, address, true); 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() this(new Job.Parameters.Builder()
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.setQueue("MultiDeviceContactUpdateJob") .setQueue("MultiDeviceContactUpdateJob")

View File

@ -68,15 +68,18 @@ import org.thoughtcrime.securesms.linkpreview.Link;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
import org.thoughtcrime.securesms.logging.Log; 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.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.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.Broadcaster;
import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities;
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities; import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage; import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
import org.thoughtcrime.securesms.mms.MmsException; 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.meta.LokiServiceMessage;
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink; import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLink;
import org.whispersystems.signalservice.loki.protocol.multidevice.DeviceLinkingSession; 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.LokiMessageFriendRequestStatus;
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus; import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus;
import org.whispersystems.signalservice.loki.utilities.PromiseUtil; import org.whispersystems.signalservice.loki.utilities.PromiseUtil;
@ -285,9 +288,9 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
SignalServiceContent content = cipher.decrypt(envelope); SignalServiceContent content = cipher.decrypt(envelope);
// Loki - Ignore any friend requests that we got before restoration // Loki - Ignore any friend requests from before restoration
if (content.isFriendRequest() && content.getTimestamp() < TextSecurePreferences.getRestorationTime(context)) { if (FriendRequestProtocol.isFriendRequestFromBeforeRestoration(content)) {
Log.d("Loki", "Ignoring friend request received before restoration."); Log.d("Loki", "Ignoring friend request from before restoration.");
return; return;
} }
@ -297,18 +300,15 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
// Loki - Handle friend request acceptance if needed // Loki - Handle friend request acceptance if needed
if (!content.isFriendRequest() && !isGroupChatMessage(content)) { FriendRequestProtocol.handleFriendRequestAcceptanceIfNeeded(content);
becomeFriendsWithContactIfNeeded(content.getSender(), true, false);
} // Loki - Handle pre key bundle message if needed
SessionManagementProtocol.handlePreKeyBundleMessageIfNeeded(content);
// Loki - Handle session request if needed // Loki - Handle session request if needed
handleSessionRequestIfNeeded(content); SessionManagementProtocol.handleSessionRequestIfNeeded(content);
// Loki - Store pre key bundle if needed
if (!content.getDeviceLink().isPresent()) {
storePreKeyBundleIfNeeded(content);
}
// Loki - Handle address message if needed
if (content.lokiServiceMessage.isPresent()) { if (content.lokiServiceMessage.isPresent()) {
LokiServiceMessage lokiMessage = content.lokiServiceMessage.get(); LokiServiceMessage lokiMessage = content.lokiServiceMessage.get();
if (lokiMessage.getAddressMessage() != null) { if (lokiMessage.getAddressMessage() != null) {
@ -316,47 +316,33 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
} }
// Loki - Store the sender display name if needed // Loki - Handle profile update if needed
Optional<String> rawSenderDisplayName = content.senderDisplayName; SessionMetaProtocol.handleProfileUpdateIfNeeded(content);
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;
});
}
if (content.getDeviceLink().isPresent()) { if (content.getDeviceLink().isPresent()) {
handleDeviceLinkMessage(content.getDeviceLink().get(), content); MultiDeviceProtocol.handleDeviceLinkMessageIfNeeded(content);
} else if (content.getDataMessage().isPresent()) { } else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get(); SignalServiceDataMessage message = content.getDataMessage().get();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent(); boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
if (!content.isFriendRequest() && message.isUnlinkingRequest()) { // Loki - Handle unlinking request if needed
// Make sure we got the request from our master device if (message.isUnlinkingRequest()) {
String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context); MultiDeviceProtocol.handleUnlinkingRequest(message);
if (ourMasterDevice != null && ourMasterDevice.equals(content.getSender())) {
TextSecurePreferences.setNeedDatabaseResetFromUnlink(context, true);
MultiDeviceUtilities.checkIsRevokedSlaveDevice(context);
}
} else { } 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.isSessionRestorationRequest() || message.isSessionRequest()) { return; }
if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId); if (message.isEndSession()) {
else if (message.isGroupUpdate()) handleGroupMessage(content, message, smsMessageId); handleEndSessionMessage(content, smsMessageId);
else if (message.isExpirationUpdate()) } else if (message.isGroupUpdate()) {
handleGroupMessage(content, message, smsMessageId);
} else if (message.isExpirationUpdate()) {
handleExpirationUpdate(content, message, smsMessageId); handleExpirationUpdate(content, message, smsMessageId);
else if (isMediaMessage) } else if (isMediaMessage) {
handleMediaMessage(content, message, smsMessageId, Optional.absent()); handleMediaMessage(content, message, smsMessageId, Optional.absent());
else if (message.getBody().isPresent()) } else if (message.getBody().isPresent()) {
handleTextMessage(content, message, smsMessageId, Optional.absent()); handleTextMessage(content, message, smsMessageId, Optional.absent());
}
if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get()))) { if (message.getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getGroupInfo().get()))) {
handleUnknownGroupMessage(content, message.getGroupInfo().get()); handleUnknownGroupMessage(content, message.getGroupInfo().get());
@ -366,25 +352,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
handleProfileKey(content, message); handleProfileKey(content, message);
} }
// Loki - This doesn't get invoked for group chats
if (content.isNeedsReceipt()) { if (content.isNeedsReceipt()) {
handleNeedsDeliveryReceipt(content, message); handleNeedsDeliveryReceipt(content, message);
} }
// If we received a friend request, but we were already friends with the user, reset the session // Loki - Handle friend request message if needed
if (content.isFriendRequest() && !message.isGroupMessage()) { FriendRequestProtocol.handleFriendRequestMessageIfNeeded(content);
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);
} }
} else if (content.getSyncMessage().isPresent()) { } else if (content.getSyncMessage().isPresent()) {
TextSecurePreferences.setMultiDevice(context, true); TextSecurePreferences.setMultiDevice(context, true);
@ -557,26 +530,11 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
if (threadId != null) { if (threadId != null) {
resetSession(content.getSender()); SessionManagementProtocol.handleEndSessionMessage(content);
MessageNotifier.updateNotification(context, threadId); 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) private long handleSynchronizeSentEndSessionMessage(@NonNull SentTranscriptMessage message)
{ {
SmsDatabase database = DatabaseFactory.getSmsDatabase(context); 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<DeviceContact> 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<DeviceGroup> 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<LokiPublicChat> 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, private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
@NonNull SentTranscriptMessage message) @NonNull SentTranscriptMessage message)
throws StorageFailedException throws StorageFailedException
@ -800,8 +672,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get()); handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get());
} }
String ourMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
boolean isSenderMasterDevice = ourMasterDevice != null && ourMasterDevice.equals(content.getSender());
if (message.getMessage().getProfileKey().isPresent()) { if (message.getMessage().getProfileKey().isPresent()) {
Recipient recipient = null; Recipient recipient = null;
@ -813,16 +683,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); 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 // Loki - Handle profile key update if needed
if (isSenderMasterDevice) {
handleProfileKey(content, message.getMessage()); handleProfileKey(content, message.getMessage());
} }
}
// Loki - Update display name from master device // Loki - Update profile if needed
if (isSenderMasterDevice && content.senderDisplayName.isPresent() && content.senderDisplayName.get().length() > 0) { SessionMetaProtocol.handleProfileUpdateIfNeeded(content);
TextSecurePreferences.setProfileName(context, content.senderDisplayName.get());
}
if (threadId != null) { if (threadId != null) {
DatabaseFactory.getThreadDatabase(context).setRead(threadId, true); DatabaseFactory.getThreadDatabase(context).setRead(threadId, true);
@ -913,17 +779,13 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or("")); Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
Optional<Attachment> sticker = getStickerAttachment(message.getSticker()); Optional<Attachment> 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()) { if (message.isGroupMessage()) {
sender = getMasterRecipient(content.getSender()).getAddress(); masterAddress = getMasterRecipient(content.getSender()).getAddress();
} }
// Ignore messages from ourselves IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1,
if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; }
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender, message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
quote, sharedContacts, linkPreviews, sticker); quote, sharedContacts, linkPreviews, sticker);
@ -967,19 +829,16 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
MessageNotifier.updateNotification(context, insertResult.get().getThreadId()); MessageNotifier.updateNotification(context, insertResult.get().getThreadId());
} }
// Loki - Run database updates in the background, we should look into fixing this in the future // Loki - Store message server ID if needed
AsyncTask.execute(() -> {
// Loki - Store message server ID
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult); updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
// Loki - Update mapping of message to original thread ID // Loki - Update mapping of message ID to original thread ID
if (insertResult.isPresent()) { if (insertResult.isPresent()) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient); long originalThreadId = threadDatabase.getThreadIdFor(originalRecipient);
lokiMessageDatabase.setOriginalThreadID(insertResult.get().getMessageId(), originalThreadId); lokiMessageDatabase.setOriginalThreadID(insertResult.get().getMessageId(), originalThreadId);
} }
});
} }
private long handleSynchronizeSentExpirationUpdate(@NonNull SentTranscriptMessage message) throws MmsException { private long handleSynchronizeSentExpirationUpdate(@NonNull SentTranscriptMessage message) throws MmsException {
@ -1108,14 +967,10 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
Address sender = masterRecipient.getAddress(); 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()) { if (message.isGroupMessage()) {
sender = getMasterRecipient(content.getSender()).getAddress(); sender = getMasterRecipient(content.getSender()).getAddress();
} }
// Ignore messages from ourselves
if (sender.serialize().equalsIgnoreCase(TextSecurePreferences.getLocalNumber(context))) { return; }
IncomingTextMessage tm = new IncomingTextMessage(sender, IncomingTextMessage tm = new IncomingTextMessage(sender,
content.getSenderDevice(), content.getSenderDevice(),
message.getTimestamp(), body, message.getTimestamp(), body,
@ -1141,13 +996,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
MessageNotifier.updateNotification(context, threadId); MessageNotifier.updateNotification(context, threadId);
} }
// Loki - Run database updates in background, we should look into fixing this in the future
AsyncTask.execute(() -> {
if (insertResult.isPresent()) { if (insertResult.isPresent()) {
InsertResult result = insertResult.get(); InsertResult result = insertResult.get();
// Loki - Cache the user hex encoded public key (for mentions) // Loki - Cache the user hex encoded public key (for mentions)
MentionManagerUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(result.getThreadId(), context); MentionManagerUtilities.INSTANCE.populateUserHexEncodedPublicKeyCacheIfNeeded(result.getThreadId(), context);
MentionsManager.INSTANCE.cache(textMessage.getSender().serialize(), result.getThreadId()); MentionsManager.shared.cache(textMessage.getSender().serialize(), result.getThreadId());
// Loki - Store message server ID // Loki - Store message server ID
updateGroupChatMessageServerID(messageServerIDOrNull, insertResult); updateGroupChatMessageServerID(messageServerIDOrNull, insertResult);
@ -1160,231 +1014,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId); 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());
}
}
private void setDisplayName(String hexEncodedPublicKey, String profileName) {
String displayName = profileName + " (..." + hexEncodedPublicKey.substring(hexEncodedPublicKey.length() - 8) + ")";
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(hexEncodedPublicKey, displayName);
}
private void updateGroupChatMessageServerID(Optional<Long> messageServerIDOrNull, Optional<InsertResult> 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);
}
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<Boolean, Exception> 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);
}
} }
} }
@ -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, private void handleCorruptMessage(@NonNull String sender, int senderDevice, long timestamp,
@NonNull Optional<Long> smsMessageId) @NonNull Optional<Long> smsMessageId)
{ {
@ -1521,14 +1135,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
triggerSessionRestorePrompt(sender); 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, private void handleLegacyMessage(@NonNull String sender, int senderDevice, long timestamp,
@NonNull Optional<Long> smsMessageId) @NonNull Optional<Long> smsMessageId)
{ {
@ -1580,10 +1186,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
String url = content.senderProfilePictureURL.or(""); String url = content.senderProfilePictureURL.or("");
ApplicationContext.getInstance(context).getJobManager().add(new RetrieveProfileAvatarJob(recipient, url)); 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 SessionMetaProtocol.handleProfileKeyUpdateIfNeeded(content, message);
if (recipient.isUserMasterDevice()) {
ApplicationContext.getInstance(context).updatePublicChatProfilePictureIfNeeded();
}
} }
} }
@ -1647,7 +1250,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
long threadId; long threadId;
if (typingMessage.getGroupId().isPresent()) { 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)); Address groupAddress = Address.fromSerialized(GroupUtil.getEncodedId(typingMessage.getGroupId().get(), false));
Recipient groupRecipient = Recipient.from(context, groupAddress, 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) { private void notifyTypingStoppedFromIncomingMessage(@NonNull Recipient conversationRecipient, @NonNull String sender, int device) {
Recipient author = Recipient.from(context, Address.fromSerialized(sender), false); Recipient author = Recipient.from(context, Address.fromSerialized(sender), false);
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(conversationRecipient); 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 isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get());
boolean isLeaveMessage = message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() == SignalServiceGroup.Type.QUIT; boolean isLeaveMessage = message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() == SignalServiceGroup.Type.QUIT;
boolean isClosedGroup = conversation.getAddress().isClosedGroup(); boolean shouldIgnoreContentMessage = ClosedGroupsProtocol.shouldIgnoreContentMessage(context, conversation, groupId.orNull(), content);
boolean isGroupMember = true; return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage) || (isContentMessage && !shouldIgnoreContentMessage);
// 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);
} else { } else {
return sender.isBlocked(); return sender.isBlocked();
} }
} else if (content.getCallMessage().isPresent() || content.getTypingMessage().isPresent()) { } else if (content.getCallMessage().isPresent() || content.getTypingMessage().isPresent()) {
return sender.isBlocked(); return sender.isBlocked();
} else if (content.getSyncMessage().isPresent()) { } else if (content.getSyncMessage().isPresent()) {
try { return SyncMessagesProtocol.shouldIgnoreSyncMessage(context, sender);
// 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 false; return false;
} }
private boolean isSessionRequest(SignalServiceContent content) {
return content.getDataMessage().isPresent() && content.getDataMessage().get().isSessionRequest();
}
private boolean isGroupChatMessage(SignalServiceContent content) { private boolean isGroupChatMessage(SignalServiceContent content) {
return content.getDataMessage().isPresent() && content.getDataMessage().get().isGroupMessage(); return content.getDataMessage().isPresent() && content.getDataMessage().get().isGroupMessage();
} }

View File

@ -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<PushMessageSyncSendJob> {
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)
}
}
}
}

View File

@ -40,7 +40,7 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega
registerButton.setOnClickListener { register() } registerButton.setOnClickListener { register() }
restoreButton.setOnClickListener { restore() } restoreButton.setOnClickListener { restore() }
linkButton.setOnClickListener { linkDevice() } linkButton.setOnClickListener { linkDevice() }
if (TextSecurePreferences.setNeedsDatabaseResetFromUnlink(this)) { if (TextSecurePreferences.getWasUnlinked(this)) {
Toast.makeText(this, "Your device was unlinked successfully", Toast.LENGTH_LONG).show() Toast.makeText(this, "Your device was unlinked successfully", Toast.LENGTH_LONG).show()
} }
} }

View File

@ -5,11 +5,13 @@ import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore
import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.recipient
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.GroupUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.SignalProtocolAddress import org.whispersystems.libsignal.SignalProtocolAddress
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.messages.SignalServiceGroup import org.whispersystems.signalservice.api.messages.SignalServiceGroup
import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDeviceProtocol
@ -17,10 +19,20 @@ import org.whispersystems.signalservice.loki.protocol.multidevice.MultiDevicePro
object ClosedGroupsProtocol { object ClosedGroupsProtocol {
@JvmStatic @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 members = group.members
val masterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context) val userMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
return !members.isPresent || !members.get().contains(masterDevice) return !members.isPresent || !members.get().contains(userMasterDevice)
} }
@JvmStatic @JvmStatic
@ -31,19 +43,20 @@ object ClosedGroupsProtocol {
result.add(Address.fromSerialized(groupID)) result.add(Address.fromSerialized(groupID))
return result return result
} else { } else {
// A closed group's members should never include slave devices
val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false) 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) } MultiDeviceProtocol.shared.getAllLinkedDevices(member.address.serialize()).map { Address.fromSerialized(it) }
}.toMutableSet() }.toMutableSet()
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (masterPublicKey != null && recipients.contains(Address.fromSerialized(masterPublicKey))) { if (userMasterPublicKey != null && destinations.contains(Address.fromSerialized(userMasterPublicKey))) {
recipients.remove(Address.fromSerialized(masterPublicKey)) destinations.remove(Address.fromSerialized(userMasterPublicKey))
} }
val userPublicKey = TextSecurePreferences.getLocalNumber(context) val userPublicKey = TextSecurePreferences.getLocalNumber(context)
if (userPublicKey != null && recipients.contains(Address.fromSerialized(userPublicKey))) { if (userPublicKey != null && destinations.contains(Address.fromSerialized(userPublicKey))) {
recipients.remove(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) val message = GroupUtil.createGroupLeaveMessage(context, recipient)
if (threadID < 0 || !message.isPresent) { return false } if (threadID < 0 || !message.isPresent) { return false }
MessageSender.send(context, message.get(), threadID, false, null) 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 masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
val publicKeyToUse = masterPublicKey ?: TextSecurePreferences.getLocalNumber(context) val publicKeyToRemove = masterPublicKey ?: TextSecurePreferences.getLocalNumber(context)
val groupDatabase = DatabaseFactory.getGroupDatabase(context) val groupDatabase = DatabaseFactory.getGroupDatabase(context)
val groupID = recipient.address.toGroupString() val groupID = recipient.address.toGroupString()
groupDatabase.setActive(groupID, false) groupDatabase.setActive(groupID, false)
groupDatabase.remove(groupID, Address.fromSerialized(publicKeyToUse)) groupDatabase.remove(groupID, Address.fromSerialized(publicKeyToRemove))
return true return true
} }
@JvmStatic @JvmStatic
fun establishSessionsWithMembersIfNeeded(context: Context, members: List<String>) { fun establishSessionsWithMembersIfNeeded(context: Context, members: List<String>) {
// A closed group's members should never include slave devices
val allDevices = members.flatMap { member -> val allDevices = members.flatMap { member ->
MultiDeviceProtocol.shared.getAllLinkedDevices(member) MultiDeviceProtocol.shared.getAllLinkedDevices(member)
}.toMutableSet() }.toMutableSet()
val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) val userMasterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context)
if (masterPublicKey != null && allDevices.contains(masterPublicKey)) { if (userMasterPublicKey != null && allDevices.contains(userMasterPublicKey)) {
allDevices.remove(masterPublicKey) allDevices.remove(userMasterPublicKey)
} }
val userPublicKey = TextSecurePreferences.getLocalNumber(context) val userPublicKey = TextSecurePreferences.getLocalNumber(context)
if (userPublicKey != null && allDevices.contains(userPublicKey)) { if (userPublicKey != null && allDevices.contains(userPublicKey)) {
allDevices.remove(userPublicKey) allDevices.remove(userPublicKey)
} }
for (device in allDevices) { for (device in allDevices) {
val address = SignalProtocolAddress(device, SignalServiceAddress.DEFAULT_DEVICE_ID) val deviceAsAddress = SignalProtocolAddress(device, SignalServiceAddress.DEFAULT_DEVICE_ID)
val hasSession = TextSecureSessionStore(context).containsSession(address) val hasSession = TextSecureSessionStore(context).containsSession(deviceAsAddress)
if (!hasSession) { sendSessionRequest(context, device) } if (hasSession) { continue }
} val sessionRequest = EphemeralMessage.createSessionRequest(device)
}
@JvmStatic
fun sendSessionRequest(context: Context, publicKey: String) {
val sessionRequest = EphemeralMessage.createSessionRequest(publicKey)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRequest)) ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRequest))
} }
} }
}

View File

@ -1,15 +1,122 @@
package org.thoughtcrime.securesms.loki.protocol package org.thoughtcrime.securesms.loki.protocol
import android.content.Context 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.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.utilities.recipient
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.sms.OutgoingTextMessage import org.thoughtcrime.securesms.sms.OutgoingTextMessage
import org.thoughtcrime.securesms.util.TextSecurePreferences 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.LokiMessageFriendRequestStatus
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
object FriendRequestProtocol { 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 @JvmStatic
fun shouldUpdateFriendRequestStatusFromOutgoingTextMessage(context: Context, message: OutgoingTextMessage): Boolean { fun shouldUpdateFriendRequestStatusFromOutgoingTextMessage(context: Context, message: OutgoingTextMessage): Boolean {
// The order of these checks matters // The order of these checks matters
@ -43,4 +150,34 @@ object FriendRequestProtocol {
threadDB.setFriendRequestStatus(threadID, LokiThreadFriendRequestStatus.REQUEST_SENDING) 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)
}
}
} }

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.loki.protocol package org.thoughtcrime.securesms.loki.protocol
import android.content.Context import android.content.Context
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.whispersystems.libsignal.loki.LokiSessionResetProtocol import org.whispersystems.libsignal.loki.LokiSessionResetProtocol
import org.whispersystems.libsignal.loki.LokiSessionResetStatus import org.whispersystems.libsignal.loki.LokiSessionResetStatus
@ -18,7 +19,8 @@ class LokiSessionResetImplementation(private val context: Context) : LokiSession
override fun onNewSessionAdopted(hexEncodedPublicKey: String, oldSessionResetStatus: LokiSessionResetStatus) { override fun onNewSessionAdopted(hexEncodedPublicKey: String, oldSessionResetStatus: LokiSessionResetStatus) {
if (oldSessionResetStatus == LokiSessionResetStatus.IN_PROGRESS) { 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 // TODO: Show session reset succeed message
} }

View File

@ -19,7 +19,6 @@ import javax.inject.Inject
class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters) : BaseJob(parameters), InjectableType { class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters) : BaseJob(parameters), InjectableType {
companion object { companion object {
const val KEY = "MultiDeviceOpenGroupUpdateJob" const val KEY = "MultiDeviceOpenGroupUpdateJob"
} }
@ -35,7 +34,7 @@ class MultiDeviceOpenGroupUpdateJob private constructor(parameters: Parameters)
override fun getFactoryKey(): String { return KEY } 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) @Throws(Exception::class)
public override fun onRun() { public override fun onRun() {

View File

@ -1,26 +1,28 @@
package org.thoughtcrime.securesms.loki.protocol package org.thoughtcrime.securesms.loki.protocol
import android.content.Context import android.content.Context
import android.util.Log
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.jobs.PushMediaSendJob import org.thoughtcrime.securesms.jobs.PushMediaSendJob
import org.thoughtcrime.securesms.jobs.PushSendJob import org.thoughtcrime.securesms.jobs.PushSendJob
import org.thoughtcrime.securesms.jobs.PushTextSendJob import org.thoughtcrime.securesms.jobs.PushTextSendJob
import org.thoughtcrime.securesms.loki.utilities.Broadcaster
import org.thoughtcrime.securesms.recipients.Recipient 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.api.fileserver.LokiFileServerAPI
import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol 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.multidevice.MultiDeviceProtocol
import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus import org.whispersystems.signalservice.loki.protocol.todo.LokiMessageFriendRequestStatus
import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
object MultiDeviceProtocol { object MultiDeviceProtocol {
@JvmStatic // TODO: Closed groups
fun sendUnlinkingRequest(context: Context, publicKey: String) {
val unlinkingRequest = EphemeralMessage.createUnlinkingRequest(publicKey)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(unlinkingRequest))
}
enum class MessageType { Text, Media } enum class MessageType { Text, Media }
@ -34,7 +36,6 @@ object MultiDeviceProtocol {
sendMessagePush(context, recipient, messageID, MessageType.Media) sendMessagePush(context, recipient, messageID, MessageType.Media)
} }
// TODO: Closed groups
private fun sendMessagePushToDevice(context: Context, recipient: Recipient, messageID: Long, messageType: MessageType): PushSendJob { private fun sendMessagePushToDevice(context: Context, recipient: Recipient, messageID: Long, messageType: MessageType): PushSendJob {
val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient)
val threadFRStatus = DatabaseFactory.getLokiThreadDatabase(context).getFriendRequestStatus(threadID) 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()
}
}
}
} }

View File

@ -18,8 +18,7 @@ import java.util.concurrent.TimeUnit
class PushEphemeralMessageSendJob private constructor(parameters: Parameters, private val message: EphemeralMessage) : BaseJob(parameters) { class PushEphemeralMessageSendJob private constructor(parameters: Parameters, private val message: EphemeralMessage) : BaseJob(parameters) {
companion object { companion object {
private val KEY_MESSAGE = "message" private const val KEY_MESSAGE = "message"
const val KEY = "PushBackgroundMessageSendJob" const val KEY = "PushBackgroundMessageSendJob"
} }
@ -32,14 +31,13 @@ class PushEphemeralMessageSendJob private constructor(parameters: Parameters, pr
message) message)
override fun serialize(): Data { override fun serialize(): Data {
// TODO: Is this correct?
return Data.Builder() return Data.Builder()
.putString(KEY_MESSAGE, message.serialize()) .putString(KEY_MESSAGE, message.serialize())
.build() .build()
} }
override fun getFactoryKey(): String { override fun getFactoryKey(): String { return KEY }
return KEY
}
public override fun onRun() { public override fun onRun() {
val recipient = message.get<String?>("recipient", null) ?: throw IllegalStateException() val recipient = message.get<String?>("recipient", null) ?: throw IllegalStateException()

View File

@ -5,8 +5,16 @@ import android.util.Log
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.PreKeyUtil 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.jobs.CleanPreKeysJob
import org.thoughtcrime.securesms.loki.utilities.recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences 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 { object SessionManagementProtocol {
@ -24,8 +32,50 @@ object SessionManagementProtocol {
} }
@JvmStatic @JvmStatic
fun sendSessionRestorationRequest(context: Context, publicKey: String) { fun handlePreKeyBundleMessageIfNeeded(context: Context, content: SignalServiceContent) {
val sessionRestorationRequest = EphemeralMessage.createSessionRestorationRequest(publicKey) val recipient = recipient(context, content.sender)
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(sessionRestorationRequest)) 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
} }
} }

View File

@ -5,14 +5,38 @@ import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.recipients.Recipient 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 import org.whispersystems.signalservice.loki.protocol.todo.LokiThreadFriendRequestStatus
object SessionMetaProtocol { object SessionMetaProtocol {
@JvmStatic @JvmStatic
fun sendEphemeralMessage(context: Context, publicKey: String) { fun handleProfileUpdateIfNeeded(context: Context, content: SignalServiceContent) {
val ephemeralMessage = EphemeralMessage.create(publicKey) val rawDisplayName = content.senderDisplayName.orNull() ?: return
ApplicationContext.getInstance(context).jobManager.add(PushEphemeralMessageSendJob(ephemeralMessage)) 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()
} }
/** /**

View File

@ -10,12 +10,24 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob
import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob import org.thoughtcrime.securesms.jobs.MultiDeviceGroupUpdateJob
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.TextSecurePreferences 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.protocol.todo.LokiThreadFriendRequestStatus
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
import java.util.* import java.util.*
object SyncMessagesProtocol { 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 @JvmStatic
fun syncAllContacts(context: Context) { fun syncAllContacts(context: Context) {
ApplicationContext.getInstance(context).jobManager.add(MultiDeviceContactUpdateJob(context, true)) ApplicationContext.getInstance(context).jobManager.add(MultiDeviceContactUpdateJob(context, true))

View File

@ -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)
}

View File

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.push;
import android.content.Context; import android.content.Context;
import org.thoughtcrime.securesms.crypto.SecurityEvent; 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.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; 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) { @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) { @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) { @Override public void onFriendRequestSendingFailed(long messageID, long threadID) {
FriendRequestHandler.updateFriendRequestState(context, FriendRequestHandler.ActionType.Failed, messageID, threadID); FriendRequestProtocol.setFriendRequestStatusToFailedIfNeeded(context, messageID, threadID);
} }
} }

View File

@ -27,7 +27,7 @@ public class WelcomeActivity extends BaseActionBarActivity {
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
if (TextSecurePreferences.setNeedsDatabaseResetFromUnlink(this)) { if (TextSecurePreferences.getWasUnlinked(this)) {
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.dialog_device_unlink_title); builder.setTitle(R.string.dialog_device_unlink_title);
builder.setMessage(R.string.dialog_device_unlink_message); builder.setMessage(R.string.dialog_device_unlink_message);
@ -36,7 +36,7 @@ public class WelcomeActivity extends BaseActionBarActivity {
@Override @Override
public void onDismiss(DialogInterface dialog) { public void onDismiss(DialogInterface dialog) {
TextSecurePreferences.setNeedDatabaseResetFromUnlink(getBaseContext(), false); TextSecurePreferences.setWasUnlinked(getBaseContext(), false);
} }
}); });
builder.show(); builder.show();

View File

@ -1264,12 +1264,12 @@ public class TextSecurePreferences {
return getBooleanPreference(context, "database_reset", false); 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 // We do it this way so that it gets persisted in storage straight away
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean("database_reset_unpair", value).commit(); 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); return getBooleanPreference(context, "database_reset_unpair", false);
} }