Merge pull request #441 from hjubb/testing_multi_merge

Testing multi merge
This commit is contained in:
Niels Andriesse 2021-02-16 11:20:02 +11:00 committed by GitHub
commit c72c87d698
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1504 additions and 764 deletions

View File

@ -158,8 +158,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.2' testImplementation 'org.robolectric:shadows-multidex:4.2'
} }
def canonicalVersionCode = 121 def canonicalVersionCode = 135
def canonicalVersionName = "1.6.4" def canonicalVersionName = "1.6.12"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase;
import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase; import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
import org.thoughtcrime.securesms.loki.utilities.Broadcaster; import org.thoughtcrime.securesms.loki.utilities.Broadcaster;
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities; import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities;
@ -115,7 +116,6 @@ import org.session.libsignal.service.loki.protocol.meta.SessionMetaProtocol;
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocol; import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocol;
import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocolDelegate; import org.session.libsignal.service.loki.protocol.sessionmanagement.SessionManagementProtocolDelegate;
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink; import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink;
import org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol;
import org.session.libsignal.service.loki.protocol.shelved.syncmessages.SyncMessagesProtocol; import org.session.libsignal.service.loki.protocol.shelved.syncmessages.SyncMessagesProtocol;
import java.io.File; import java.io.File;
@ -206,7 +206,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
SessionMetaProtocol.Companion.configureIfNeeded(apiDB, userPublicKey); SessionMetaProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
SyncMessagesProtocol.Companion.configureIfNeeded(apiDB, userPublicKey); SyncMessagesProtocol.Companion.configureIfNeeded(apiDB, userPublicKey);
} }
MultiDeviceProtocol.Companion.configureIfNeeded(apiDB); org.session.libsignal.service.loki.protocol.shelved.multidevice.MultiDeviceProtocol.Companion.configureIfNeeded(apiDB);
SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, sskDatabase, this); SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, sskDatabase, this);
setUpP2PAPIIfNeeded(); setUpP2PAPIIfNeeded();
PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG); PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG);
@ -249,6 +249,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
startPollingIfNeeded(); startPollingIfNeeded();
publicChatManager.markAllAsNotCaughtUp(); publicChatManager.markAllAsNotCaughtUp();
publicChatManager.startPollersIfNeeded(); publicChatManager.startPollersIfNeeded();
MultiDeviceProtocol.syncConfigurationIfNeeded(this);
} }
@Override @Override

View File

@ -1109,7 +1109,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
try { try {
if (isSSKBasedClosedGroup) { if (isSSKBasedClosedGroup) {
ClosedGroupsProtocolV2.leave(this, groupPublicKey); ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey);
initializeEnabledCheck(); initializeEnabledCheck();
} else if (ClosedGroupsProtocol.leaveLegacyGroup(this, groupRecipient)) { } else if (ClosedGroupsProtocol.leaveLegacyGroup(this, groupRecipient)) {
initializeEnabledCheck(); initializeEnabledCheck();

View File

@ -150,6 +150,17 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
return new Reader(cursor); return new Reader(cursor);
} }
public List<GroupRecord> getAllGroups() {
Reader reader = getGroups();
GroupRecord record;
List<GroupRecord> groups = new LinkedList<>();
while ((record = reader.getNext()) != null) {
if (record.isActive()) { groups.add(record); }
}
reader.close();
return groups;
}
public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) { public @NonNull List<Recipient> getGroupMembers(String groupId, boolean includeSelf) {
List<Address> members = getCurrentMembers(groupId); List<Address> members = getCurrentMembers(groupId);
List<Recipient> recipients = new LinkedList<>(); List<Recipient> recipients = new LinkedList<>();
@ -184,7 +195,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
} }
} }
public void create(@NonNull String groupId, @Nullable String title, @NonNull List<Address> members, public long create(@NonNull String groupId, @Nullable String title, @NonNull List<Address> members,
@Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List<Address> admins) @Nullable SignalServiceAttachmentPointer avatar, @Nullable String relay, @Nullable List<Address> admins)
{ {
Collections.sort(members); Collections.sort(members);
@ -211,7 +222,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
contentValues.put(ADMINS, Address.Companion.toSerializedList(admins, ',')); contentValues.put(ADMINS, Address.Companion.toSerializedList(admins, ','));
} }
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues); long threadId = databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
Recipient.applyCached(Address.Companion.fromSerialized(groupId), recipient -> { Recipient.applyCached(Address.Companion.fromSerialized(groupId), recipient -> {
recipient.setName(title); recipient.setName(title);
@ -220,6 +231,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
}); });
notifyConversationListListeners(); notifyConversationListListeners();
return threadId;
} }
public boolean delete(@NonNull String groupId) { public boolean delete(@NonNull String groupId) {

View File

@ -902,6 +902,17 @@ public class MmsDatabase extends MessagingDatabase {
return insertMessageInbox(retrieved, contentLocation, threadId, type, 0); return insertMessageInbox(retrieved, contentLocation, threadId, type, 0);
} }
public Optional<InsertResult> insertSecureDecryptedMessageOutbox(OutgoingMediaMessage retrieved, long threadId, long serverTimestamp)
throws MmsException
{
long messageId = insertMessageOutbox(retrieved, threadId, false, null, serverTimestamp);
if (messageId == -1) {
return Optional.absent();
}
markAsSent(messageId, true);
return Optional.fromNullable(new InsertResult(messageId, threadId));
}
public Optional<InsertResult> insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId, long serverTimestamp) public Optional<InsertResult> insertSecureDecryptedMessageInbox(IncomingMediaMessage retrieved, long threadId, long serverTimestamp)
throws MmsException throws MmsException
{ {

View File

@ -87,7 +87,7 @@ public class MmsSmsDatabase extends Database {
} }
} }
public @Nullable MessageRecord getMessageFor(long timestamp, Address author) { public @Nullable MessageRecord getMessageFor(long timestamp, String serializedAuthor) {
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context); MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) { try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
@ -96,8 +96,10 @@ public class MmsSmsDatabase extends Database {
MessageRecord messageRecord; MessageRecord messageRecord;
while ((messageRecord = reader.getNext()) != null) { while ((messageRecord = reader.getNext()) != null) {
if ((Util.isOwnNumber(context, author.serialize()) && messageRecord.isOutgoing()) || if ((Util.isOwnNumber(context, serializedAuthor) && messageRecord.isOutgoing()) ||
(!Util.isOwnNumber(context, author.serialize()) && messageRecord.getIndividualRecipient().getAddress().equals(author))) (!Util.isOwnNumber(context, serializedAuthor)
&& messageRecord.getIndividualRecipient().getAddress().serialize().equals(serializedAuthor)
))
{ {
return messageRecord; return messageRecord;
} }
@ -107,6 +109,10 @@ public class MmsSmsDatabase extends Database {
return null; return null;
} }
public @Nullable MessageRecord getMessageFor(long timestamp, Address author) {
return getMessageFor(timestamp, author.serialize());
}
public Cursor getConversation(long threadId, long offset, long limit) { public Cursor getConversation(long threadId, long offset, long limit) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC"; String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
String selection = MmsSmsColumns.THREAD_ID + " = " + threadId; String selection = MmsSmsColumns.THREAD_ID + " = " + threadId;

View File

@ -686,6 +686,15 @@ public class SmsDatabase extends MessagingDatabase {
return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp); return insertMessageInbox(message, Types.BASE_INBOX_TYPE, serverTimestamp);
} }
public Optional<InsertResult> insertMessageOutbox(long threadId, OutgoingTextMessage message, long serverTimestamp) {
long messageId = insertMessageOutbox(threadId, message, false, serverTimestamp, null);
if (messageId == -1) {
return Optional.absent();
}
markAsSent(messageId, true);
return Optional.fromNullable(new InsertResult(messageId, threadId));
}
public long insertMessageOutbox(long threadId, OutgoingTextMessage message, public long insertMessageOutbox(long threadId, OutgoingTextMessage message,
boolean forceSms, long date, InsertListener insertListener) boolean forceSms, long date, InsertListener insertListener)
{ {
@ -718,7 +727,6 @@ public class SmsDatabase extends MessagingDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase(); SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues); long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues);
if (insertListener != null) { if (insertListener != null) {
insertListener.onComplete(); insertListener.onComplete();
} }

View File

@ -8,7 +8,6 @@ import org.session.libsession.messaging.jobs.AttachmentUploadJob
import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.Job
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageSendJob import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.opengroups.OpenGroup import org.session.libsession.messaging.opengroups.OpenGroup
@ -27,9 +26,11 @@ import org.session.libsignal.service.api.messages.SignalServiceAttachment
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.api.opengroups.PublicChat
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
import org.thoughtcrime.securesms.loki.utilities.get import org.thoughtcrime.securesms.loki.utilities.get
import org.thoughtcrime.securesms.loki.utilities.getString import org.thoughtcrime.securesms.loki.utilities.getString
import org.thoughtcrime.securesms.mms.IncomingMediaMessage import org.thoughtcrime.securesms.mms.IncomingMediaMessage
@ -351,7 +352,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
.setName(name) .setName(name)
.addAllMembers(members) .addAllMembers(members)
.addAllAdmins(admins) .addAllAdmins(admins)
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf()) val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf())
val mmsDB = DatabaseFactory.getMmsDatabase(context) val mmsDB = DatabaseFactory.getMmsDatabase(context)
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
mmsDB.markAsSent(infoMessageID, true) mmsDB.markAsSent(infoMessageID, true)
@ -371,6 +372,38 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
return DatabaseFactory.getLokiAPIDatabase(context).getLatestClosedGroupEncryptionKeyPair(groupPublicKey) return DatabaseFactory.getLokiAPIDatabase(context).getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
} }
override fun getAllClosedGroupPublicKeys(): Set<String> {
return DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys()
}
override fun addClosedGroupPublicKey(groupPublicKey: String) {
DatabaseFactory.getLokiAPIDatabase(context).addClosedGroupPublicKey(groupPublicKey)
}
override fun removeClosedGroupPublicKey(groupPublicKey: String) {
DatabaseFactory.getLokiAPIDatabase(context).removeClosedGroupPublicKey(groupPublicKey)
}
override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
DatabaseFactory.getLokiAPIDatabase(context).addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
}
override fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) {
DatabaseFactory.getLokiAPIDatabase(context).removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
}
override fun getAllOpenGroups(): Map<Long, PublicChat> {
return DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats()
}
override fun addOpenGroup(server: String, channel: Long) {
OpenGroupUtilities.addGroup(context, server, channel)
}
override fun getAllGroups(): List<GroupRecord> {
return DatabaseFactory.getGroupDatabase(context).allGroups
}
override fun setProfileSharing(address: Address, value: Boolean) { override fun setProfileSharing(address: Address, value: Boolean) {
val recipient = Recipient.from(context, address, false) val recipient = Recipient.from(context, address, false)
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, value) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, value)

View File

@ -194,7 +194,7 @@ public class GroupManager {
avatarAttachment = new UriAttachment(avatarUri, MediaTypes.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null); avatarAttachment = new UriAttachment(avatarUri, MediaTypes.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null, null);
} }
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null, Collections.emptyList(), Collections.emptyList()); OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, 0, null, Collections.emptyList(), Collections.emptyList());
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null); long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
return new GroupActionResult(groupRecipient, threadId); return new GroupActionResult(groupRecipient, threadId);

View File

@ -13,6 +13,8 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult; import org.thoughtcrime.securesms.database.MessagingDatabase.InsertResult;
import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.jobs.AvatarDownloadJob; import org.thoughtcrime.securesms.jobs.AvatarDownloadJob;
import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob; import org.thoughtcrime.securesms.jobs.PushGroupUpdateJob;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
@ -123,11 +125,19 @@ public class GroupMessageProcessor {
{ {
GroupDatabase database = DatabaseFactory.getGroupDatabase(context); GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
String id = GroupUtil.getEncodedId(group); String id = GroupUtil.getEncodedId(group);
Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group));
Recipient recipient = Recipient.from(context, address, false);
String userMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context); String userMasterDevice = TextSecurePreferences.getMasterHexEncodedPublicKey(context);
if (userMasterDevice == null) { userMasterDevice = TextSecurePreferences.getLocalNumber(context); } if (userMasterDevice == null) { userMasterDevice = TextSecurePreferences.getLocalNumber(context); }
if (content.getSender().equals(userMasterDevice)) {
long threadId = threadDatabase.getThreadIdIfExistsFor(recipient);
return threadId == -1 ? null : threadId;
}
if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) { if (group.getGroupType() == SignalServiceGroup.GroupType.SIGNAL) {
// Loki - Only update the group if the group admin sent the message // Loki - Only update the group if the group admin sent the message
String masterDevice = MultiDeviceProtocol.shared.getMasterDevice(content.getSender()); String masterDevice = MultiDeviceProtocol.shared.getMasterDevice(content.getSender());
@ -260,9 +270,16 @@ public class GroupMessageProcessor {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group)); Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group));
Recipient recipient = Recipient.from(context, address, false); Recipient recipient = Recipient.from(context, address, false);
OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, content.getTimestamp(), 0, null, Collections.emptyList(), Collections.emptyList()); OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(recipient, storage, null, 0, null, Collections.emptyList(), Collections.emptyList());
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient); long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); Address senderAddress = Address.Companion.fromExternal(context, content.getSender());
MessageRecord existingMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(content.getTimestamp(), senderAddress);
long messageId;
if (existingMessage != null) {
messageId = existingMessage.getId();
} else {
messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
}
mmsDatabase.markAsSent(messageId, true); mmsDatabase.markAsSent(messageId, true);

View File

@ -47,7 +47,6 @@ import org.session.libsession.utilities.TextSecurePreferences;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper; import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl; import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
@ -80,6 +79,7 @@ import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase;
import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol;
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2;
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol;
import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol;
import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol;
import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation;
@ -96,7 +96,6 @@ import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage;
import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage; import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.session.libsignal.utilities.Hex; import org.session.libsignal.utilities.Hex;
import org.session.libsignal.libsignal.InvalidMessageException; import org.session.libsignal.libsignal.InvalidMessageException;
@ -264,8 +263,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content); SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content);
if (content.getDeviceLink().isPresent()) { if (content.configurationMessageProto.isPresent()) {
throw new UnsupportedOperationException("Device link operations are not supported!"); MultiDeviceProtocol.handleConfigurationMessage(context, content.configurationMessageProto.get(), content.getSender(), content.getTimestamp());
} 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();
@ -277,7 +276,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (message.getClosedGroupUpdateV2().isPresent()) { if (message.getClosedGroupUpdateV2().isPresent()) {
ClosedGroupsProtocolV2.handleMessage(context, message.getClosedGroupUpdateV2().get(), message.getTimestamp(), envelope.getSource(), content.getSender()); ClosedGroupsProtocolV2.handleMessage(context, message.getClosedGroupUpdateV2().get(), message.getTimestamp(), envelope.getSource(), content.getSender());
} }
if (message.isEndSession()) { if (message.isEndSession()) {
handleEndSessionMessage(content, smsMessageId); handleEndSessionMessage(content, smsMessageId);
} else if (message.isGroupUpdate()) { } else if (message.isGroupUpdate()) {
@ -398,33 +396,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
} }
private long handleSynchronizeSentEndSessionMessage(@NonNull SentTranscriptMessage message)
{
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
Recipient recipient = getSyncMessageDestination(message);
OutgoingTextMessage outgoingTextMessage = new OutgoingTextMessage(recipient, "", -1);
OutgoingEndSessionMessage outgoingEndSessionMessage = new OutgoingEndSessionMessage(outgoingTextMessage);
long threadId = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(recipient);
if (!recipient.isGroupRecipient()) {
// TODO: Handle session reset on sync messages
/*
SessionStore sessionStore = new TextSecureSessionStore(context);
sessionStore.deleteAllSessions(recipient.getAddress().toPhoneString());
*/
SecurityEvent.broadcastSecurityUpdateEvent(context);
long messageId = database.insertMessageOutbox(threadId, outgoingEndSessionMessage,
false, message.getTimestamp(),
null);
database.markAsSent(messageId, true);
}
return threadId;
}
private void handleGroupMessage(@NonNull SignalServiceContent content, private void handleGroupMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message, @NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId) @NonNull Optional<Long> smsMessageId)
@ -483,98 +454,23 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
} }
} }
private void handleSynchronizeStickerPackOperation(@NonNull List<StickerPackOperationMessage> stickerPackOperations) {
JobManager jobManager = ApplicationContext.getInstance(context).getJobManager();
for (StickerPackOperationMessage operation : stickerPackOperations) {
if (operation.getPackId().isPresent() && operation.getPackKey().isPresent() && operation.getType().isPresent()) {
String packId = Hex.toStringCondensed(operation.getPackId().get());
String packKey = Hex.toStringCondensed(operation.getPackKey().get());
switch (operation.getType().get()) {
case INSTALL:
jobManager.add(new StickerPackDownloadJob(packId, packKey, false));
break;
case REMOVE:
DatabaseFactory.getStickerDatabase(context).uninstallPack(packId);
break;
}
} else {
Log.w(TAG, "Received incomplete sticker pack operation sync.");
}
}
}
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
@NonNull SentTranscriptMessage message)
throws StorageFailedException
{
try {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
Long threadId;
if (message.getMessage().isEndSession()) {
threadId = handleSynchronizeSentEndSessionMessage(message);
} else if (message.getMessage().isGroupUpdate()) {
threadId = GroupMessageProcessor.process(context, content, message.getMessage(), true);
} else if (message.getMessage().isExpirationUpdate()) {
threadId = handleSynchronizeSentExpirationUpdate(message);
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent()) {
threadId = handleSynchronizeSentMediaMessage(message);
} else {
threadId = handleSynchronizeSentTextMessage(message);
}
if (threadId == -1L) { threadId = null; }
if (message.getMessage().getGroupInfo().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get()))) {
handleUnknownGroupMessage(content, message.getMessage().getGroupInfo().get());
}
if (message.getMessage().getProfileKey().isPresent()) {
Recipient recipient = null;
if (message.getDestination().isPresent()) recipient = Recipient.from(context, Address.Companion.fromSerialized(message.getDestination().get()), false);
else if (message.getMessage().getGroupInfo().isPresent()) recipient = Recipient.from(context, Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getMessage().getGroupInfo().get())), false);
if (recipient != null && !recipient.isSystemContact() && !recipient.isProfileSharing()) {
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true);
}
SessionMetaProtocol.handleProfileKeyUpdate(context, content);
}
SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content);
if (threadId != null) {
DatabaseFactory.getThreadDatabase(context).setRead(threadId, true);
messageNotifier.updateNotification(context);
}
messageNotifier.setLastDesktopActivityTimestamp(message.getTimestamp());
} catch (MmsException e) {
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
}
}
public void handleMediaMessage(@NonNull SignalServiceContent content, public void handleMediaMessage(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message, @NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId, @NonNull Optional<Long> smsMessageId,
@NonNull Optional<Long> messageServerIDOrNull) @NonNull Optional<Long> messageServerIDOrNull)
throws StorageFailedException throws StorageFailedException
{ {
Recipient originalRecipient = getMessageDestination(content, message); Recipient originalRecipient = getMessageDestination(content, message);
Recipient masterRecipient = getMessageMasterDestination(content.getSender()); Recipient masterRecipient = getMessageMasterDestination(content.getSender());
String syncTarget = message.getSyncTarget().orNull();
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice()); notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote()); Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts()); Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
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 masterAddress = masterRecipient.getAddress(); Address masterAddress = masterRecipient.getAddress();
@ -582,75 +478,140 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
masterAddress = getMessageMasterDestination(content.getSender()).getAddress(); masterAddress = getMessageMasterDestination(content.getSender()).getAddress();
} }
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1, // Handle sync message from ourselves
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(), if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) {
quote, sharedContacts, linkPreviews, sticker); Address targetAddress = masterRecipient.getAddress();
if (message.getGroupInfo().isPresent()) {
targetAddress = Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get()));
} else if (syncTarget != null && !syncTarget.isEmpty()) {
targetAddress = Address.fromSerialized(syncTarget);
}
List<Attachment> attachments = PointerAttachment.forPointers(message.getAttachments());
MmsDatabase database = DatabaseFactory.getMmsDatabase(context); OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(masterRecipient, message.getBody().orNull(),
database.beginTransaction(); attachments,
message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000,
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(),
sharedContacts.or(Collections.emptyList()),
linkPreviews.or(Collections.emptyList()),
Collections.emptyList(), Collections.emptyList());
// Ignore message if it has no body and no attachments if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) {
if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) { Log.d("Loki","Message already exists, don't insert again");
return; return;
} }
Optional<InsertResult> insertResult; MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.beginTransaction();
try { // Ignore message if it has no body and no attachments
if (message.isGroupMessage()) { if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) {
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp()); return;
} else { }
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
Optional<InsertResult> insertResult;
try {
// Check if we have the thread already
long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize());
insertResult = database.insertSecureDecryptedMessageOutbox(mediaMessage, threadID, content.getTimestamp());
if (insertResult.isPresent()) {
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
List<DatabaseAttachment> stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
List<DatabaseAttachment> dbAttachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
forceStickerDownloadIfNecessary(stickerAttachments);
for (DatabaseAttachment attachment : dbAttachments) {
ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false));
}
if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
}
database.setTransactionSuccessful();
}
} catch (MmsException e) {
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
} finally {
database.endTransaction();
}
} else {
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
quote, sharedContacts, linkPreviews, sticker);
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.beginTransaction();
// Ignore message if it has no body and no attachments
if (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) {
return;
}
Optional<InsertResult> insertResult;
try {
if (message.isGroupMessage()) {
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp());
} else {
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
}
if (insertResult.isPresent()) {
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId());
List<DatabaseAttachment> stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
List<DatabaseAttachment> attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
forceStickerDownloadIfNecessary(stickerAttachments);
for (DatabaseAttachment attachment : attachments) {
ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false));
}
if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
}
database.setTransactionSuccessful();
}
} catch (MmsException e) {
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
} finally {
database.endTransaction();
} }
if (insertResult.isPresent()) { if (insertResult.isPresent()) {
List<DatabaseAttachment> allAttachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(insertResult.get().getMessageId()); messageNotifier.updateNotification(context, insertResult.get().getThreadId());
List<DatabaseAttachment> stickerAttachments = Stream.of(allAttachments).filter(Attachment::isSticker).toList();
List<DatabaseAttachment> attachments = Stream.of(allAttachments).filterNot(Attachment::isSticker).toList();
forceStickerDownloadIfNecessary(stickerAttachments);
for (DatabaseAttachment attachment : attachments) {
ApplicationContext.getInstance(context).getJobManager().add(new AttachmentDownloadJob(insertResult.get().getMessageId(), attachment.getAttachmentId(), false));
}
if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
}
database.setTransactionSuccessful();
}
} catch (MmsException e) {
throw new StorageFailedException(e, content.getSender(), content.getSenderDevice());
} finally {
database.endTransaction();
}
if (insertResult.isPresent()) {
messageNotifier.updateNotification(context, insertResult.get().getThreadId());
}
if (insertResult.isPresent()) {
InsertResult result = insertResult.get();
// Loki - Cache the user hex encoded public key (for mentions)
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
// Loki - Store message open group server ID if needed
if (messageServerIDOrNull.isPresent()) {
long messageID = result.getMessageId();
long messageServerID = messageServerIDOrNull.get();
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
lokiMessageDatabase.setServerID(messageID, messageServerID);
} }
// Loki - Update mapping of message ID to original thread ID if (insertResult.isPresent()) {
if (result.getMessageId() > -1) { InsertResult result = insertResult.get();
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context); // Loki - Cache the user hex encoded public key (for mentions)
long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient); MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId); MentionsManager.shared.cache(content.getSender(), result.getThreadId());
// Loki - Store message open group server ID if needed
if (messageServerIDOrNull.isPresent()) {
long messageID = result.getMessageId();
long messageServerID = messageServerIDOrNull.get();
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
lokiMessageDatabase.setServerID(messageID, messageServerID);
}
// Loki - Update mapping of message ID to original thread ID
if (result.getMessageId() > -1) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
LokiMessageDatabase lokiMessageDatabase = DatabaseFactory.getLokiMessageDatabase(context);
long originalThreadId = threadDatabase.getOrCreateThreadIdFor(originalRecipient);
lokiMessageDatabase.setOriginalThreadID(result.getMessageId(), originalThreadId);
}
} }
} }
} }
@ -769,6 +730,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
String body = message.getBody().isPresent() ? message.getBody().get() : ""; String body = message.getBody().isPresent() ? message.getBody().get() : "";
Recipient originalRecipient = getMessageDestination(content, message); Recipient originalRecipient = getMessageDestination(content, message);
Recipient masterRecipient = getMessageMasterDestination(content.getSender()); Recipient masterRecipient = getMessageMasterDestination(content.getSender());
String syncTarget = message.getSyncTarget().orNull();
if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) { if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) {
handleExpirationUpdate(content, message, Optional.absent()); handleExpirationUpdate(content, message, Optional.absent());
@ -778,15 +740,56 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) { if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second; threadId = database.updateBundleMessageBody(smsMessageId.get(), body).second;
} else if (syncTarget != null && !syncTarget.isEmpty() || TextSecurePreferences.getLocalNumber(context).equals(content.getSender())) {
Address targetAddress = masterRecipient.getAddress();
if (message.getGroupInfo().isPresent()) {
targetAddress = Address.Companion.fromSerialized(GroupUtil.getEncodedId(message.getGroupInfo().get()));
} else if (syncTarget != null && !syncTarget.isEmpty()) {
targetAddress = Address.fromSerialized(syncTarget);
}
if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) {
Log.d("Loki","Message already exists, don't insert again");
return;
}
OutgoingTextMessage tm = new OutgoingTextMessage(Recipient.from(context, targetAddress, false),
body, message.getExpiresInSeconds(), -1);
// Ignore the message if it has no body
if (tm.getMessageBody().length() == 0) { return; }
// Check if we have the thread already
long threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(targetAddress.serialize());
// Insert the message into the database
Optional<InsertResult> insertResult;
insertResult = database.insertMessageOutbox(threadID, tm, content.getTimestamp());
if (insertResult.isPresent()) {
threadId = insertResult.get().getThreadId();
}
if (smsMessageId.isPresent()) database.deleteMessage(smsMessageId.get());
if (threadId != null) {
messageNotifier.updateNotification(context, threadId);
}
if (insertResult.isPresent()) {
InsertResult result = insertResult.get();
// Loki - Cache the user hex encoded public key (for mentions)
MentionManagerUtilities.INSTANCE.populateUserPublicKeyCacheIfNeeded(result.getThreadId(), context);
MentionsManager.shared.cache(content.getSender(), result.getThreadId());
}
} else { } else {
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice()); notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
Address masterAddress = masterRecipient.getAddress(); Address masterAddress = masterRecipient.getAddress();
if (message.isGroupMessage()) {
masterAddress = getMessageMasterDestination(content.getSender()).getAddress();
}
IncomingTextMessage tm = new IncomingTextMessage(masterAddress, IncomingTextMessage tm = new IncomingTextMessage(masterAddress,
content.getSenderDevice(), content.getSenderDevice(),
message.getTimestamp(), body, message.getTimestamp(), body,
@ -1319,6 +1322,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
return true; return true;
} }
if (content.getSender().equals(TextSecurePreferences.getLocalNumber(context)) &&
DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(content.getTimestamp(), content.getSender()) != null) {
Log.d("Loki", "Skipping message from self we already have");
return true;
}
Recipient sender = Recipient.from(context, Address.Companion.fromSerialized(content.getSender()), false); Recipient sender = Recipient.from(context, Address.Companion.fromSerialized(content.getSender()), false);
if (content.getDeviceLink().isPresent()) { if (content.getDeviceLink().isPresent()) {

View File

@ -15,6 +15,7 @@ import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.Address;
import org.session.libsession.utilities.GroupUtil; import org.session.libsession.utilities.GroupUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -143,6 +144,9 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
List<NetworkFailure> existingNetworkFailures = message.getNetworkFailures(); List<NetworkFailure> existingNetworkFailures = message.getNetworkFailures();
List<IdentityKeyMismatch> existingIdentityMismatches = message.getIdentityKeyMismatches(); List<IdentityKeyMismatch> existingIdentityMismatches = message.getIdentityKeyMismatches();
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
if (database.isSent(messageId)) { if (database.isSent(messageId)) {
log(TAG, "Message " + messageId + " was already sent. Ignoring."); log(TAG, "Message " + messageId + " was already sent. Ignoring.");
return; return;
@ -238,25 +242,18 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
// return results; // return results;
// } // }
String groupId = address.toGroupString();
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
Optional<Quote> quote = getQuoteFor(message);
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
List<SharedContact> sharedContacts = getSharedContactsFor(message);
List<Preview> previews = getPreviewsFor(message);
List<SignalServiceAddress> addresses = Stream.of(destinations).map(this::getPushAddress).toList(); List<SignalServiceAddress> addresses = Stream.of(destinations).map(this::getPushAddress).toList();
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(addresses) List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(addresses)
.map(a -> Address.Companion.fromSerialized(a.getNumber())) .map(a -> Address.Companion.fromSerialized(a.getNumber()))
.map(a -> Recipient.from(context, a, false)) .map(a -> Recipient.from(context, a, false))
.map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)) .map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient))
.toList(); .toList();
SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL;
if (message.isGroup() && address.isClosedGroup()) { if (message.isGroup() && address.isClosedGroup()) {
SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL;
String groupId = address.toGroupString();
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
// Loki - Only send GroupUpdate or GroupQuit messages to closed groups // Loki - Only send GroupUpdate or GroupQuit messages to closed groups
OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message; OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
GroupContext groupContext = groupMessage.getGroupContext(); GroupContext groupContext = groupMessage.getGroupContext();
@ -271,25 +268,40 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage); return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage);
} else { } else {
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupId), groupType); SignalServiceDataMessage groupMessage = getDataMessage(address, message).build();
SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getSentTimeMillis())
.asGroupMessage(group)
.withAttachments(attachmentPointers)
.withBody(message.getBody())
.withExpiration((int)(message.getExpiresIn() / 1000))
.asExpirationUpdate(message.isExpirationUpdate())
.withProfileKey(profileKey.orNull())
.withQuote(quote.orNull())
.withSticker(sticker.orNull())
.withSharedContacts(sharedContacts)
.withPreviews(previews)
.build();
return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupMessage); return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupMessage);
} }
} }
public SignalServiceDataMessage.Builder getDataMessage(Address address, OutgoingMediaMessage message) throws IOException {
SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL;
String groupId = address.toGroupString();
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
Optional<Quote> quote = getQuoteFor(message);
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
List<SharedContact> sharedContacts = getSharedContactsFor(message);
List<Preview> previews = getPreviewsFor(message);
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupId), groupType);
return SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getSentTimeMillis())
.asGroupMessage(group)
.withAttachments(attachmentPointers)
.withBody(message.getBody())
.withExpiration((int)(message.getExpiresIn() / 1000))
.asExpirationUpdate(message.isExpirationUpdate())
.withProfileKey(profileKey.orNull())
.withQuote(quote.orNull())
.withSticker(sticker.orNull())
.withSharedContacts(sharedContacts)
.withPreviews(previews);
}
public static class Factory implements Job.Factory<PushGroupSendJob> { public static class Factory implements Job.Factory<PushGroupSendJob> {
@Override @Override
public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull Data data) { public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull Data data) {

View File

@ -245,7 +245,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
{ {
try { try {
Recipient recipient = Recipient.from(context, destination, false); Recipient recipient = Recipient.from(context, destination, false);
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
SignalServiceAddress address = getPushAddress(recipient.getAddress()); SignalServiceAddress address = getPushAddress(recipient.getAddress());
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList(); List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments); List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments);
Optional<byte[]> profileKey = getProfileKey(message.getRecipient()); Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
@ -254,6 +256,8 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
List<SharedContact> sharedContacts = getSharedContactsFor(message); List<SharedContact> sharedContacts = getSharedContactsFor(message);
List<Preview> previews = getPreviewsFor(message); List<Preview> previews = getPreviewsFor(message);
Optional<UnidentifiedAccessPair> unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(context, recipient);
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody()) .withBody(message.getBody())
.withAttachments(serviceAttachments) .withAttachments(serviceAttachments)
@ -267,20 +271,48 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
.asExpirationUpdate(message.isExpirationUpdate()) .asExpirationUpdate(message.isExpirationUpdate())
.build(); .build();
SignalServiceDataMessage mediaSelfSendMessage = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody())
.withAttachments(serviceAttachments)
.withTimestamp(message.getSentTimeMillis())
.withSyncTarget(destination.serialize())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
.withQuote(quote.orNull())
.withSticker(sticker.orNull())
.withSharedContacts(sharedContacts)
.withPreviews(previews)
.asExpirationUpdate(message.isExpirationUpdate())
.build();
if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) { if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) {
// Loki - Device link messages don't go through here // Loki - Device link messages don't go through here
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context); SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage);
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, mediaMessage, syncAccess);
messageSender.sendMessage(syncMessage, syncAccess);
return syncAccess.isPresent();
} else {
SendMessageResult result = messageSender.sendMessage(messageId, address, UnidentifiedAccessUtil.getAccessFor(context, recipient), mediaMessage);
if (result.getLokiAPIError() != null) { if (result.getLokiAPIError() != null) {
throw result.getLokiAPIError(); throw result.getLokiAPIError();
} else { } else {
return result.getSuccess().isUnidentified(); return result.getSuccess().isUnidentified();
} }
} else {
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage);
if (result.getLokiAPIError() != null) {
throw result.getLokiAPIError();
} else {
boolean isUnidentified = result.getSuccess().isUnidentified();
try {
// send to ourselves to sync multi-device
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, mediaSelfSendMessage);
if (selfSendResult.getLokiAPIError() != null) {
throw selfSendResult.getLokiAPIError();
}
} catch (Exception e) {
Log.e("Loki", "Error sending message to ourselves", e);
}
return isUnidentified;
}
} }
} catch (UnregisteredUserException e) { } catch (UnregisteredUserException e) {
warn(TAG, e); warn(TAG, e);

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.session.libsession.messaging.jobs.Data; import org.session.libsession.messaging.jobs.Data;
import org.session.libsignal.service.api.crypto.UnidentifiedAccess;
import org.session.libsignal.utilities.logging.Log; import org.session.libsignal.utilities.logging.Log;
import org.session.libsession.messaging.threads.Address; import org.session.libsession.messaging.threads.Address;
@ -192,8 +193,10 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
throws UntrustedIdentityException, InsecureFallbackApprovalException, RetryLaterException, SnodeAPI.Error throws UntrustedIdentityException, InsecureFallbackApprovalException, RetryLaterException, SnodeAPI.Error
{ {
try { try {
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
Recipient recipient = Recipient.from(context, destination, false); Recipient recipient = Recipient.from(context, destination, false);
SignalServiceAddress address = getPushAddress(recipient.getAddress()); SignalServiceAddress address = getPushAddress(recipient.getAddress());
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
Optional<byte[]> profileKey = getProfileKey(recipient); Optional<byte[]> profileKey = getProfileKey(recipient);
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient); Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
@ -205,28 +208,49 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
// } // }
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder() SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getDateSent()) .withTimestamp(message.getDateSent())
.withBody(message.getBody()) .withBody(message.getBody())
.withExpiration((int)(message.getExpiresIn() / 1000)) .withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull()) .withProfileKey(profileKey.orNull())
// .withPreKeyBundle(preKeyBundle) .asEndSessionMessage(message.isEndSession())
.asEndSessionMessage(message.isEndSession()) .build();
.build();
SignalServiceDataMessage textSecureSelfSendMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getDateSent())
.withBody(message.getBody())
.withSyncTarget(destination.serialize())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
.asEndSessionMessage(message.isEndSession())
.build();
if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) { if (SessionMetaProtocol.shared.isNoteToSelf(address.getNumber())) {
// Loki - Device link messages don't go through here // Loki - Device link messages don't go through here
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SignalServiceSyncMessage syncMessage = buildSelfSendSyncMessage(context, textSecureMessage, syncAccess);
messageSender.sendMessage(syncMessage, syncAccess);
return syncAccess.isPresent();
} else {
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage); SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage);
if (result.getLokiAPIError() != null) { if (result.getLokiAPIError() != null) {
throw result.getLokiAPIError(); throw result.getLokiAPIError();
} else { } else {
return result.getSuccess().isUnidentified(); return result.getSuccess().isUnidentified();
} }
} else {
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccess, textSecureMessage);
if (result.getLokiAPIError() != null) {
throw result.getLokiAPIError();
} else {
boolean isUnidentified = result.getSuccess().isUnidentified();
try {
// send to ourselves to sync multi-device
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
SendMessageResult selfSendResult = messageSender.sendMessage(messageId, localAddress, syncAccess, textSecureSelfSendMessage);
if (selfSendResult.getLokiAPIError() != null) {
throw selfSendResult.getLokiAPIError();
}
} catch (Exception e) {
Log.e("Loki", "Error sending message to ourselves", e);
}
return isUnidentified;
}
} }
} catch (UnregisteredUserException e) { } catch (UnregisteredUserException e) {
warn(TAG, "Failure", e); warn(TAG, "Failure", e);

View File

@ -2,8 +2,6 @@ package org.thoughtcrime.securesms.loki.activities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.os.AsyncTask
import android.os.Bundle import android.os.Bundle
import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
@ -20,15 +18,13 @@ import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.loki.utilities.fadeIn import org.thoughtcrime.securesms.loki.utilities.fadeIn
import org.thoughtcrime.securesms.loki.utilities.fadeOut import org.thoughtcrime.securesms.loki.utilities.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.guava.Optional
import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2 import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocolV2
import java.lang.ref.WeakReference import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
//TODO Refactor to avoid using kotlinx.android.synthetic //TODO Refactor to avoid using kotlinx.android.synthetic
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> { class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
@ -122,6 +118,7 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM
openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false)) openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false))
finish() finish()
} }
} }
} }
// endregion // endregion

View File

@ -277,24 +277,22 @@ class EditClosedGroupActivity : PassphraseRequiredActionBarActivity() {
isLoading = true isLoading = true
loaderContainer.fadeIn() loaderContainer.fadeIn()
val promise: Promise<Any, Exception> = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) { val promise: Promise<Any, Exception> = if (!members.contains(Recipient.from(this, Address.fromSerialized(userPublicKey), false))) {
ClosedGroupsProtocolV2.leave(this, groupPublicKey!!) ClosedGroupsProtocolV2.explicitLeave(this, groupPublicKey!!)
} else { } else {
// TODO: uncomment when we switch to sending new explicit updates after clients update task {
// task { val name =
// val name = if (hasNameChanged) ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity,groupPublicKey!!,name)
// if (hasNameChanged) ClosedGroupsProtocolV2.explicitNameChange(this@EditClosedGroupActivity,groupPublicKey!!,name) else Promise.of(Unit)
// else Promise.of(Unit) name.get()
// name.get() members.filterNot { it in originalMembers }.let { adds ->
// members.filterNot { it in originalMembers }.let { adds -> if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() })
// if (adds.isNotEmpty()) ClosedGroupsProtocolV2.explicitAddMembers(this@EditClosedGroupActivity, groupPublicKey!!, adds.map { it.address.serialize() }) else Promise.of(Unit)
// else Promise.of(Unit) }.get()
// }.get() originalMembers.filterNot { it in members }.let { removes ->
// originalMembers.filterNot { it in members }.let { removes -> if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() })
// if (removes.isNotEmpty()) ClosedGroupsProtocolV2.explicitRemoveMembers(this@EditClosedGroupActivity, groupPublicKey!!, removes.map { it.address.serialize() }) else Promise.of(Unit)
// else Promise.of(Unit) }.get()
// }.get() }
// }
ClosedGroupsProtocolV2.update(this, groupPublicKey!!, members.map { it.address.serialize() }, name)
} }
promise.successUi { promise.successUi {
loaderContainer.fadeOut() loaderContainer.fadeOut()

View File

@ -358,7 +358,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe
isSSKBasedClosedGroup = false isSSKBasedClosedGroup = false
} }
if (isSSKBasedClosedGroup) { if (isSSKBasedClosedGroup) {
ClosedGroupsProtocolV2.leave(context, groupPublicKey!!) ClosedGroupsProtocolV2.explicitLeave(context, groupPublicKey!!)
} else if (!ClosedGroupsProtocol.leaveLegacyGroup(context, recipient)) { } else if (!ClosedGroupsProtocol.leaveLegacyGroup(context, recipient)) {
Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show()
return@launch return@launch

View File

@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment
import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
@ -73,6 +74,7 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel) OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, url, channel)
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("JoinPublicChatActivity", "Fialed to join open group.", e) Log.e("JoinPublicChatActivity", "Fialed to join open group.", e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View File

@ -18,10 +18,11 @@ import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
class SessionProtocolImpl(private val context: Context) : SessionProtocol { class SessionProtocolImpl(private val context: Context) : SessionProtocol {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
override fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray { override fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray {
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw SessionProtocol.Exception.NoUserED25519KeyPair val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw SessionProtocol.Exception.NoUserED25519KeyPair
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded()) val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
val sodium = LazySodiumAndroid(SodiumAndroid())
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
val signature = ByteArray(Sign.BYTES) val signature = ByteArray(Sign.BYTES)
@ -47,7 +48,6 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol {
val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize() val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize()
val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded()) val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded())
Log.d("Test", "recipientX25519PublicKey: $recipientX25519PublicKey") Log.d("Test", "recipientX25519PublicKey: $recipientX25519PublicKey")
val sodium = LazySodiumAndroid(SodiumAndroid())
val signatureSize = Sign.BYTES val signatureSize = Sign.BYTES
val ed25519PublicKeySize = Sign.PUBLICKEYBYTES val ed25519PublicKeySize = Sign.PUBLICKEYBYTES

View File

@ -29,7 +29,7 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab
} }
override fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long? { override fun getQuoteServerID(quoteID: Long, quoteePublicKey: String): Long? {
val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, Address.fromSerialized(quoteePublicKey)) val message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteID, quoteePublicKey)
return if (message != null) getServerID(message.getId()) else null return if (message != null) getServerID(message.getId()) else null
} }

View File

@ -10,6 +10,7 @@ import android.view.LayoutInflater
import kotlinx.android.synthetic.main.dialog_clear_all_data.view.* import kotlinx.android.synthetic.main.dialog_clear_all_data.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol
import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities import org.thoughtcrime.securesms.loki.utilities.KeyPairUtilities
class ClearAllDataDialog : DialogFragment() { class ClearAllDataDialog : DialogFragment() {
@ -27,6 +28,7 @@ class ClearAllDataDialog : DialogFragment() {
private fun clearAllData() { private fun clearAllData() {
if (KeyPairUtilities.hasV2KeyPair(requireContext())) { if (KeyPairUtilities.hasV2KeyPair(requireContext())) {
MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(requireContext())
ApplicationContext.getInstance(context).clearAllData(false) ApplicationContext.getInstance(context).clearAllData(false)
} else { } else {
val dialog = AlertDialog.Builder(requireContext()) val dialog = AlertDialog.Builder(requireContext())

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.protocol
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import org.session.libsession.messaging.jobs.Data import org.session.libsession.messaging.jobs.Data
import org.session.libsignal.libsignal.util.guava.Optional
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.Job
@ -128,7 +129,7 @@ class ClosedGroupUpdateMessageSendJob private constructor(parameters: Parameters
// isClosedGroup can always be false as it's only used in the context of legacy closed groups // isClosedGroup can always be false as it's only used in the context of legacy closed groups
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess, messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedContentMessage, false, ttl, false, Date().time, serializedContentMessage, false, ttl, false,
useFallbackEncryption, false, false, false) useFallbackEncryption, false, false, Optional.absent())
} catch (e: Exception) { } catch (e: Exception) {
Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.")
} }

View File

@ -9,6 +9,7 @@ import org.session.libsession.messaging.jobs.Data
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.push.SignalServiceAddress import org.session.libsignal.service.api.push.SignalServiceAddress
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.protocol.meta.TTLUtilities import org.session.libsignal.service.loki.protocol.meta.TTLUtilities
@ -26,7 +27,7 @@ import org.session.libsignal.utilities.Hex
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, private val destination: String, private val kind: Kind) : BaseJob(parameters) { class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Parameters, private val destination: String, private val kind: Kind, private val sentTime: Long) : BaseJob(parameters) {
sealed class Kind { sealed class Kind {
class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind() class New(val publicKey: ByteArray, val name: String, val encryptionKeyPair: ECKeyPair, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind()
@ -60,20 +61,22 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
} }
} }
constructor(destination: String, kind: Kind) : this(Parameters.Builder() constructor(destination: String, kind: Kind, sentTime: Long) : this(Parameters.Builder()
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.setQueue(KEY) .setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1)) .setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(20) .setMaxAttempts(20)
.build(), .build(),
destination, destination,
kind) kind,
sentTime)
override fun getFactoryKey(): String { return KEY } override fun getFactoryKey(): String { return KEY }
override fun serialize(): Data { override fun serialize(): Data {
val builder = Data.Builder() val builder = Data.Builder()
builder.putString("destination", destination) builder.putString("destination", destination)
builder.putLong("sentTime", sentTime)
when (kind) { when (kind) {
is Kind.New -> { is Kind.New -> {
builder.putString("kind", "New") builder.putString("kind", "New")
@ -123,6 +126,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 { override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 {
val destination = data.getString("destination") val destination = data.getString("destination")
val rawKind = data.getString("kind") val rawKind = data.getString("kind")
val sentTime = data.getLong("sentTime")
val kind: Kind val kind: Kind
when (rawKind) { when (rawKind) {
"New" -> { "New" -> {
@ -161,7 +165,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
} }
else -> throw Exception("Invalid closed group update message kind: $rawKind.") else -> throw Exception("Invalid closed group update message kind: $rawKind.")
} }
return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind) return ClosedGroupUpdateMessageSendJobV2(parameters, destination, kind, sentTime)
} }
} }
@ -220,8 +224,8 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
try { try {
// isClosedGroup can always be false as it's only used in the context of legacy closed groups // isClosedGroup can always be false as it's only used in the context of legacy closed groups
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess, messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedContentMessage, false, ttl, false, sentTime, serializedContentMessage, false, ttl, false,
true, false, false, false) true, false, false, Optional.absent())
} catch (e: Exception) { } catch (e: Exception) {
Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.")
} }

View File

@ -285,7 +285,7 @@ object ClosedGroupsProtocol {
.setName(name) .setName(name)
.addAllMembers(members) .addAllMembers(members)
.addAllAdmins(admins) .addAllAdmins(admins)
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf()) val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, 0, null, listOf(), listOf())
val mmsDB = DatabaseFactory.getMmsDatabase(context) val mmsDB = DatabaseFactory.getMmsDatabase(context)
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
mmsDB.markAsSent(infoMessageID, true) mmsDB.markAsSent(infoMessageID, true)
@ -324,6 +324,6 @@ object ClosedGroupsProtocol {
.setId(decodedGroupId) .setId(decodedGroupId)
.setType(GroupContext.Type.QUIT) .setType(GroupContext.Type.QUIT)
.build() .build()
return OutgoingGroupMediaMessage(groupRecipient, groupContext, null, System.currentTimeMillis(), 0, null, emptyList(), emptyList()) return OutgoingGroupMediaMessage(groupRecipient, groupContext, null, 0, null, emptyList(), emptyList())
} }
} }

View File

@ -38,11 +38,14 @@ import org.session.libsession.utilities.TextSecurePreferences
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.jvm.Throws import kotlin.jvm.Throws
object ClosedGroupsProtocolV2 { object ClosedGroupsProtocolV2 {
const val groupSizeLimit = 100 const val groupSizeLimit = 100
private val pendingKeyPair = ConcurrentHashMap<String,Optional<ECKeyPair>>()
sealed class Error(val description: String) : Exception() { sealed class Error(val description: String) : Exception() {
object NoThread : Error("Couldn't find a thread associated with the given group public key") object NoThread : Error("Couldn't find a thread associated with the given group public key")
object NoKeyPair : Error("Couldn't find an encryption key pair associated with the given group public key.") object NoKeyPair : Error("Couldn't find an encryption key pair associated with the given group public key.")
@ -58,6 +61,7 @@ object ClosedGroupsProtocolV2 {
val apiDB = DatabaseFactory.getLokiAPIDatabase(context) val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
// Generate the group's public key // Generate the group's public key
val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix
val sentTime = System.currentTimeMillis()
// Generate the key pair that'll be used for encryption and decryption // Generate the key pair that'll be used for encryption and decryption
val encryptionKeyPair = Curve.generateKeyPair() val encryptionKeyPair = Curve.generateKeyPair()
// Create the group // Create the group
@ -68,20 +72,20 @@ object ClosedGroupsProtocolV2 {
null, null, LinkedList(admins.map { Address.fromSerialized(it!!) })) null, null, LinkedList(admins.map { Address.fromSerialized(it!!) }))
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
// Send a closed group update message to all members individually // Send a closed group update message to all members individually
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
for (member in members) {
if (member == userPublicKey) { continue }
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind)
job.setContext(context)
job.onRun() // Run the job immediately to make all of this sync
}
// Add the group to the user's set of public keys to poll for // Add the group to the user's set of public keys to poll for
apiDB.addClosedGroupPublicKey(groupPublicKey) apiDB.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair // Store the encryption key pair
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user // Notify the user
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
for (member in members) {
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime)
job.setContext(context)
job.onRun() // Run the job immediately to make all of this sync
}
// Notify the PN server // Notify the PN server
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Fulfill the promise // Fulfill the promise
@ -103,21 +107,22 @@ object ClosedGroupsProtocolV2 {
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
val admins = group.admins.map { it.serialize() } val admins = group.admins.map { it.serialize() }
val name = group.title val name = group.title
val sentTime = System.currentTimeMillis()
if (group == null) { if (group == null) {
Log.d("Loki", "Can't leave nonexistent closed group.") Log.d("Loki", "Can't leave nonexistent closed group.")
return@queue deferred.reject(Error.NoThread) return@queue deferred.reject(Error.NoThread)
} }
// Send the update to the group // Send the update to the group
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave) val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime)
job.setContext(context) job.setContext(context)
job.onRun() // Run the job immediately job.onRun() // Run the job immediately
// Remove the group private key and unsubscribe from PNs
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
// Notify the user // Notify the user
val infoType = GroupContext.Type.QUIT val infoType = GroupContext.Type.QUIT
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID) insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
// Remove the group private key and unsubscribe from PNs
disableLocalGroupAndUnsubscribe(context, apiDB, groupPublicKey, groupDB, groupID, userPublicKey)
deferred.resolve(Unit) deferred.resolve(Unit)
} }
return deferred.promise return deferred.promise
@ -141,7 +146,10 @@ object ClosedGroupsProtocolV2 {
val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) } val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) }
val admins = group.admins.map { it.serialize() } val admins = group.admins.map { it.serialize() }
val adminsAsData = admins.map { Hex.fromStringCondensed(it) } val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) val sentTime = System.currentTimeMillis()
val encryptionKeyPair = pendingKeyPair.getOrElse(groupPublicKey) {
Optional.fromNullable(apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey))
}.orNull()
if (encryptionKeyPair == null) { if (encryptionKeyPair == null) {
Log.d("Loki", "Couldn't get encryption key pair for closed group.") Log.d("Loki", "Couldn't get encryption key pair for closed group.")
return@task Error.NoKeyPair return@task Error.NoKeyPair
@ -149,21 +157,21 @@ object ClosedGroupsProtocolV2 {
val name = group.title val name = group.title
// Send the update to the group // Send the update to the group
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData) val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind) val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
job.setContext(context) job.setContext(context)
job.onRun() // Run the job immediately job.onRun() // Run the job immediately
// Notify the user
val infoType = GroupContext.Type.UPDATE
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
// Send closed group update messages to any new members individually // Send closed group update messages to any new members individually
for (member in membersToAdd) { for (member in membersToAdd) {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind) val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime)
ApplicationContext.getInstance(context).jobManager.add(newMemberJob) ApplicationContext.getInstance(context).jobManager.add(newMemberJob)
} }
// Notify the user
val infoType = GroupContext.Type.UPDATE
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
} }
} }
@ -184,6 +192,7 @@ object ClosedGroupsProtocolV2 {
groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) }) groupDB.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) } val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) }
val admins = group.admins.map { it.serialize() } val admins = group.admins.map { it.serialize() }
val sentTime = System.currentTimeMillis()
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) { if (encryptionKeyPair == null) {
Log.d("Loki", "Couldn't get encryption key pair for closed group.") Log.d("Loki", "Couldn't get encryption key pair for closed group.")
@ -196,17 +205,18 @@ object ClosedGroupsProtocolV2 {
val name = group.title val name = group.title
// Send the update to the group // Send the update to the group
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData) val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind) val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
job.setContext(context) job.setContext(context)
job.onRun() // Run the job immediately job.onRun() // Run the job immediately
// Notify the user
val infoType = GroupContext.Type.UPDATE
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
val isCurrentUserAdmin = admins.contains(userPublicKey) val isCurrentUserAdmin = admins.contains(userPublicKey)
if (isCurrentUserAdmin) { if (isCurrentUserAdmin) {
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers) generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers)
} }
// Notify the user return@task Unit
val infoType = GroupContext.Type.UPDATE
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
} }
} }
@ -219,21 +229,22 @@ object ClosedGroupsProtocolV2 {
val group = groupDB.getGroup(groupID).orNull() val group = groupDB.getGroup(groupID).orNull()
val members = group.members.map { it.serialize() }.toSet() val members = group.members.map { it.serialize() }.toSet()
val admins = group.admins.map { it.serialize() } val admins = group.admins.map { it.serialize() }
val sentTime = System.currentTimeMillis()
if (group == null) { if (group == null) {
Log.d("Loki", "Can't leave nonexistent closed group.") Log.d("Loki", "Can't leave nonexistent closed group.")
return@queue deferred.reject(Error.NoThread) return@queue deferred.reject(Error.NoThread)
} }
// Send the update to the group // Send the update to the group
val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName) val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind) val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime)
job.setContext(context) job.setContext(context)
job.onRun() // Run the job immediately job.onRun() // Run the job immediately
// Update the group
groupDB.updateTitle(groupID, newName)
// Notify the user // Notify the user
val infoType = GroupContext.Type.UPDATE val infoType = GroupContext.Type.UPDATE
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID) insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
// Update the group
groupDB.updateTitle(groupID, newName)
deferred.resolve(Unit) deferred.resolve(Unit)
} }
return deferred.promise return deferred.promise
@ -273,6 +284,7 @@ object ClosedGroupsProtocolV2 {
Log.d("Loki", "Can't update nonexistent closed group.") Log.d("Loki", "Can't update nonexistent closed group.")
return@queue deferred.reject(Error.NoThread) return@queue deferred.reject(Error.NoThread)
} }
val sentTime = System.currentTimeMillis()
val oldMembers = group.members.map { it.serialize() }.toSet() val oldMembers = group.members.map { it.serialize() }.toSet()
val newMembers = members.minus(oldMembers) val newMembers = members.minus(oldMembers)
val membersAsData = members.map { Hex.fromStringCondensed(it) } val membersAsData = members.map { Hex.fromStringCondensed(it) }
@ -299,7 +311,7 @@ object ClosedGroupsProtocolV2 {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.Update(name, membersAsData) val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.Update(name, membersAsData)
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind) val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind, sentTime)
job.setContext(context) job.setContext(context)
job.onRun() // Run the job immediately job.onRun() // Run the job immediately
if (isUserLeaving) { if (isUserLeaving) {
@ -323,7 +335,7 @@ object ClosedGroupsProtocolV2 {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData) val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind) val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime)
ApplicationContext.getInstance(context).jobManager.add(job) ApplicationContext.getInstance(context).jobManager.add(job)
} }
} }
@ -336,7 +348,7 @@ object ClosedGroupsProtocolV2 {
// Notify the user // Notify the user
val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) val threadID = DatabaseFactory.getThreadDatabase(context).getOrCreateThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false))
insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID) insertOutgoingInfoMessage(context, groupID, infoType, name, members, admins, threadID, sentTime)
deferred.resolve(Unit) deferred.resolve(Unit)
} }
return deferred.promise return deferred.promise
@ -359,7 +371,19 @@ object ClosedGroupsProtocolV2 {
} }
// Generate the new encryption key pair // Generate the new encryption key pair
val newKeyPair = Curve.generateKeyPair() val newKeyPair = Curve.generateKeyPair()
// replace call will not succeed if no value already set
pendingKeyPair.putIfAbsent(groupPublicKey,Optional.absent())
do {
// make sure we set the pendingKeyPair or wait until it is not null
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
// Distribute it // Distribute it
sendEncryptionKeyPair(context, groupPublicKey, newKeyPair, targetMembers)
// Store it * after * having sent out the message to the group
apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
pendingKeyPair[groupPublicKey] = Optional.absent()
}
private fun sendEncryptionKeyPair(context: Context, groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, force: Boolean = true) {
val proto = SignalServiceProtos.KeyPair.newBuilder() val proto = SignalServiceProtos.KeyPair.newBuilder()
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize()) proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
@ -368,18 +392,20 @@ object ClosedGroupsProtocolV2 {
val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey) val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey)
ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext) ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext)
} }
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers)) val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers), System.currentTimeMillis())
job.setContext(context) if (force) {
job.onRun() // Run the job immediately job.setContext(context)
// Store it * after * having sent out the message to the group job.onRun() // Run the job immediately
apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey) } else {
ApplicationContext.getInstance(context).jobManager.add(job)
}
} }
@JvmStatic @JvmStatic
fun handleMessage(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { fun handleMessage(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
if (!isValid(closedGroupUpdate, senderPublicKey)) { return } if (!isValid(context, closedGroupUpdate, senderPublicKey, sentTimestamp)) { return }
when (closedGroupUpdate.type) { when (closedGroupUpdate.type) {
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey) SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate, senderPublicKey, sentTimestamp)
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED -> handleClosedGroupMembersRemoved(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_REMOVED -> handleClosedGroupMembersRemoved(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBERS_ADDED -> handleClosedGroupMembersAdded(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey) SignalServiceProtos.ClosedGroupUpdateV2.Type.NAME_CHANGE -> handleClosedGroupNameChange(context, closedGroupUpdate, sentTimestamp, groupPublicKey, senderPublicKey)
@ -392,7 +418,10 @@ object ClosedGroupsProtocolV2 {
} }
} }
private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String): Boolean { private fun isValid(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String, sentTimestamp: Long): Boolean {
val record = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(sentTimestamp, senderPublicKey)
if (record != null) return false
return when (closedGroupUpdate.type) { return when (closedGroupUpdate.type) {
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> { SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> {
(!closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty (!closedGroupUpdate.publicKey.isEmpty && !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.encryptionKeyPair.privateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty
@ -414,7 +443,7 @@ object ClosedGroupsProtocolV2 {
} }
} }
public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String) { public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, senderPublicKey: String, sentTimestamp: Long) {
// Prepare // Prepare
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val apiDB = DatabaseFactory.getLokiAPIDatabase(context) val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
@ -427,7 +456,8 @@ object ClosedGroupsProtocolV2 {
// Create the group // Create the group
val groupID = doubleEncodeGroupID(groupPublicKey) val groupID = doubleEncodeGroupID(groupPublicKey)
val groupDB = DatabaseFactory.getGroupDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context)
if (groupDB.getGroup(groupID).orNull() != null) { val prevGroup = groupDB.getGroup(groupID).orNull()
if (prevGroup != null) {
// Update the group // Update the group
groupDB.updateTitle(groupID, name) groupDB.updateTitle(groupID, name)
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
@ -441,8 +471,14 @@ object ClosedGroupsProtocolV2 {
// Store the encryption key pair // Store the encryption key pair
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray())) val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey) apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user // Notify the user (if we didn't make the group)
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) if (userPublicKey != senderPublicKey) {
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
} else if (prevGroup == null) {
// only notify if we created this group
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
}
// Notify the PN server // Notify the PN server
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
} }
@ -452,8 +488,8 @@ object ClosedGroupsProtocolV2 {
val groupDB = DatabaseFactory.getGroupDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey) val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull() val group = groupDB.getGroup(groupID).orNull()
if (group == null) { if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.") Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return return
} }
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
@ -492,16 +528,22 @@ object ClosedGroupsProtocolV2 {
val (contextType, signalType) = val (contextType, signalType) =
if (senderLeft) GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT if (senderLeft) GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
if (userPublicKey == senderPublicKey) {
insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins) val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, contextType, name, members, admins, threadID, sentTimestamp)
} else {
insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins)
}
} }
fun handleClosedGroupMembersAdded(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { fun handleClosedGroupMembersAdded(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey) val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull() val group = groupDB.getGroup(groupID).orNull()
if (group == null) { if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.") Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return return
} }
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) { if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
@ -518,16 +560,32 @@ object ClosedGroupsProtocolV2 {
val newMembers = members + updateMembers val newMembers = members + updateMembers
groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) groupDB.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
} else {
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
}
if (userPublicKey in admins) {
// send current encryption key to the latest added members
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) {
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
} else {
sendEncryptionKeyPair(context, groupPublicKey, encryptionKeyPair, newMembers, false)
}
}
} }
fun handleClosedGroupNameChange(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { fun handleClosedGroupNameChange(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
// Check that the sender is a member of the group (before the update) // Check that the sender is a member of the group (before the update)
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val groupDB = DatabaseFactory.getGroupDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey) val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull() val group = groupDB.getGroup(groupID).orNull()
if (group == null) { if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.") Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return return
} }
// Check common group update logic // Check common group update logic
@ -539,21 +597,23 @@ object ClosedGroupsProtocolV2 {
val name = closedGroupUpdate.name val name = closedGroupUpdate.name
groupDB.updateTitle(groupID, name) groupDB.updateTitle(groupID, name)
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID, sentTimestamp)
} else {
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
}
} }
private fun handleClosedGroupMemberLeft(context: Context, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { private fun handleClosedGroupMemberLeft(context: Context, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
// Check the user leaving isn't us, will already be handled // Check the user leaving isn't us, will already be handled
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!! val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
if (senderPublicKey == userPublicKey) {
return
}
val apiDB = DatabaseFactory.getLokiAPIDatabase(context) val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey) val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull() val group = groupDB.getGroup(groupID).orNull()
if (group == null) { if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.") Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return return
} }
val name = group.title val name = group.title
@ -576,7 +636,12 @@ object ClosedGroupsProtocolV2 {
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList) generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMemberList)
} }
} }
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins) if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID, sentTimestamp)
} else {
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins)
}
} }
private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) { private fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdateV2, sentTimestamp: Long, groupPublicKey: String, senderPublicKey: String) {
@ -589,8 +654,8 @@ object ClosedGroupsProtocolV2 {
val groupDB = DatabaseFactory.getGroupDatabase(context) val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey) val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull() val group = groupDB.getGroup(groupID).orNull()
if (group == null) { if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.") Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return return
} }
val oldMembers = group.members.map { it.serialize() } val oldMembers = group.members.map { it.serialize() }
@ -624,7 +689,13 @@ object ClosedGroupsProtocolV2 {
val wasSenderRemoved = !members.contains(senderPublicKey) val wasSenderRemoved = !members.contains(senderPublicKey)
val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() }) val admins = group.admins.map { it.toString() }
if (userPublicKey == senderPublicKey) {
val threadID = DatabaseFactory.getLokiThreadDatabase(context).getThreadID(groupID)
insertOutgoingInfoMessage(context, groupID, type0, name, members, admins, threadID, sentTimestamp)
} else {
insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, admins)
}
} }
private fun disableLocalGroupAndUnsubscribe(context: Context, apiDB: LokiAPIDatabase, groupPublicKey: String, groupDB: GroupDatabase, groupID: String, userPublicKey: String) { private fun disableLocalGroupAndUnsubscribe(context: Context, apiDB: LokiAPIDatabase, groupPublicKey: String, groupDB: GroupDatabase, groupID: String, userPublicKey: String) {
@ -700,7 +771,9 @@ object ClosedGroupsProtocolV2 {
} }
private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String, private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String,
members: Collection<String>, admins: Collection<String>, threadID: Long) { members: Collection<String>, admins: Collection<String>, threadID: Long,
sentTime: Long) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) val recipient = Recipient.from(context, Address.fromSerialized(groupID), false)
val groupContextBuilder = GroupContext.newBuilder() val groupContextBuilder = GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))) .setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
@ -708,9 +781,11 @@ object ClosedGroupsProtocolV2 {
.setName(name) .setName(name)
.addAllMembers(members) .addAllMembers(members)
.addAllAdmins(admins) .addAllAdmins(admins)
val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf()) val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, sentTime, 0, null, listOf(), listOf())
val mmsDB = DatabaseFactory.getMmsDatabase(context) val mmsDB = DatabaseFactory.getMmsDatabase(context)
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) val mmsSmsDB = DatabaseFactory.getMmsSmsDatabase(context)
if (mmsSmsDB.getMessageFor(sentTime,userPublicKey) != null) return
val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null, sentTime)
mmsDB.markAsSent(infoMessageID, true) mmsDB.markAsSent(infoMessageID, true)
} }

View File

@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.loki.protocol
import android.content.Context
import com.google.protobuf.ByteString
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.push.SignalServiceAddress
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.logging.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities
import org.thoughtcrime.securesms.loki.utilities.recipient
import java.util.*
object MultiDeviceProtocol {
@JvmStatic
fun syncConfigurationIfNeeded(context: Context) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
val lastSyncTime = TextSecurePreferences.getLastConfigurationSyncTime(context)
val now = System.currentTimeMillis()
if (now - lastSyncTime < 2 * 24 * 60 * 60 * 1000) return
val configurationMessage = ConfigurationMessage.getCurrent()
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(userPublicKey)
val recipient = recipient(context, userPublicKey)
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
try {
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false,
true, false, true, Optional.absent())
TextSecurePreferences.setLastConfigurationSyncTime(context, now)
} catch (e: Exception) {
Log.d("Loki", "Failed to send configuration message due to error: $e.")
}
}
fun forceSyncConfigurationNowIfNeeded(context: Context) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
val configurationMessage = ConfigurationMessage.getCurrent()
val serializedMessage = configurationMessage.toProto()!!.toByteArray()
val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender()
val address = SignalServiceAddress(userPublicKey)
val recipient = recipient(context, userPublicKey)
val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient)
try {
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false,
true, false, true, Optional.absent())
} catch (e: Exception) {
Log.d("Loki", "Failed to send configuration message due to error: $e.")
}
}
@JvmStatic
fun handleConfigurationMessage(context: Context, content: SignalServiceProtos.Content, senderPublicKey: String, timestamp: Long) {
if (TextSecurePreferences.getConfigurationMessageSynced(context)) return
val configurationMessage = ConfigurationMessage.fromProto(content) ?: return
val userPublicKey = TextSecurePreferences.getLocalNumber(context) ?: return
if (senderPublicKey != userPublicKey) return
val storage = MessagingConfiguration.shared.storage
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
for (closedGroup in configurationMessage.closedGroups) {
if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue
val closedGroupUpdate = SignalServiceProtos.ClosedGroupUpdateV2.newBuilder()
closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW
closedGroupUpdate.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(closedGroup.publicKey))
closedGroupUpdate.name = closedGroup.name
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPair.publicKey = ByteString.copyFrom(closedGroup.encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPair.privateKey = ByteString.copyFrom(closedGroup.encryptionKeyPair.privateKey.serialize())
closedGroupUpdate.encryptionKeyPair = encryptionKeyPair.build()
closedGroupUpdate.addAllMembers(closedGroup.members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
closedGroupUpdate.addAllAdmins(closedGroup.admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
ClosedGroupsProtocolV2.handleNewClosedGroup(context, closedGroupUpdate.build(), userPublicKey, timestamp)
}
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
for (openGroup in configurationMessage.openGroups) {
if (allOpenGroups.contains(openGroup)) continue
OpenGroupUtilities.addGroup(context, openGroup, 1)
}
TextSecurePreferences.setConfigurationMessageSynced(context, true)
}
}

View File

@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.BaseJob import org.thoughtcrime.securesms.jobs.BaseJob
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.push.SignalServiceAddress import org.session.libsignal.service.api.push.SignalServiceAddress
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.protocol.meta.TTLUtilities import org.session.libsignal.service.loki.protocol.meta.TTLUtilities
@ -56,7 +57,7 @@ class NullMessageSendJob private constructor(parameters: Parameters, private val
try { try {
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess, messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedContentMessage, false, ttl, false, Date().time, serializedContentMessage, false, ttl, false,
false, false, false, false) false, false, false, Optional.absent())
} catch (e: Exception) { } catch (e: Exception) {
Log.d("Loki", "Failed to send null message to: $publicKey due to error: $e.") Log.d("Loki", "Failed to send null message to: $publicKey due to error: $e.")
throw e throw e

View File

@ -14,7 +14,7 @@ import org.session.libsignal.libsignal.ecc.ECKeyPair
object KeyPairUtilities { object KeyPairUtilities {
private val sodium = LazySodiumAndroid(SodiumAndroid()) private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
fun generate(): KeyPairGenerationResult { fun generate(): KeyPairGenerationResult {
val seed = sodium.randomBytesBuf(16) val seed = sodium.randomBytesBuf(16)

View File

@ -27,7 +27,7 @@ object OpenGroupUtilities {
val groupID = PublicChat.getId(channel, url) val groupID = PublicChat.getId(channel, url)
val threadID = GroupManager.getOpenGroupThreadID(groupID, context) val threadID = GroupManager.getOpenGroupThreadID(groupID, context)
val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) val openGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
if (openGroup != null) return openGroup if (openGroup != null) { return openGroup }
// Add the new group. // Add the new group.
val application = ApplicationContext.getInstance(context) val application = ApplicationContext.getInstance(context)

View File

@ -40,7 +40,23 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
public OutgoingGroupMediaMessage(@NonNull Recipient recipient, public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
@NonNull GroupContext group, @NonNull GroupContext group,
@Nullable final Attachment avatar, @Nullable final Attachment avatar,
long sentTimeMillis, long expireIn,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> previews)
{
super(recipient, Base64.encodeBytes(group.toByteArray()),
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
System.currentTimeMillis(),
ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
this.group = group;
}
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
@NonNull GroupContext group,
@Nullable final Attachment avatar,
long sentTime,
long expireIn, long expireIn,
@Nullable QuoteModel quote, @Nullable QuoteModel quote,
@NonNull List<Contact> contacts, @NonNull List<Contact> contacts,
@ -48,7 +64,7 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
{ {
super(recipient, Base64.encodeBytes(group.toByteArray()), super(recipient, Base64.encodeBytes(group.toByteArray()),
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}}, new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
System.currentTimeMillis(), sentTime,
ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews); ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
this.group = group; this.group = group;

View File

@ -326,7 +326,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
private static Drawable getPlaceholderDrawable(Context context, Recipient recipient) { private static Drawable getPlaceholderDrawable(Context context, Recipient recipient) {
String publicKey = recipient.getAddress().serialize(); String publicKey = recipient.getAddress().serialize();
String hepk = (recipient.isLocalNumber() && publicKey != null) String hepk = (recipient.isLocalNumber() && publicKey == null)
? TextSecurePreferences.getMasterHexEncodedPublicKey(context) ? TextSecurePreferences.getMasterHexEncodedPublicKey(context)
: publicKey; : publicKey;
String displayName = recipient.getName(); String displayName = recipient.getName();

View File

@ -6,12 +6,7 @@ public class IncomingEncryptedMessage extends IncomingTextMessage {
super(base, newBody); super(base, newBody);
} }
@Override @Override
public IncomingTextMessage withMessageBody(String body) {
return new IncomingEncryptedMessage(this, body);
}
@Override
public boolean isSecureMessage() { public boolean isSecureMessage() {
return true; return true;
} }

View File

@ -10,12 +10,7 @@ public class IncomingEndSessionMessage extends IncomingTextMessage {
super(base, newBody); super(base, newBody);
} }
@Override @Override
public IncomingEndSessionMessage withMessageBody(String messageBody) {
return new IncomingEndSessionMessage(this, messageBody);
}
@Override
public boolean isEndSession() { public boolean isEndSession() {
return true; return true;
} }

View File

@ -11,12 +11,7 @@ public class IncomingGroupMessage extends IncomingTextMessage {
this.groupContext = groupContext; this.groupContext = groupContext;
} }
@Override @Override
public IncomingGroupMessage withMessageBody(String body) {
return new IncomingGroupMessage(this, groupContext, body);
}
@Override
public boolean isGroup() { public boolean isGroup() {
return true; return true;
} }

View File

@ -9,12 +9,7 @@ public class IncomingPreKeyBundleMessage extends IncomingTextMessage {
this.legacy = legacy; this.legacy = legacy;
} }
@Override @Override
public IncomingPreKeyBundleMessage withMessageBody(String messageBody) {
return new IncomingPreKeyBundleMessage(this, messageBody, legacy);
}
@Override
public boolean isLegacyPreKeyBundle() { public boolean isLegacyPreKeyBundle() {
return legacy; return legacy;
} }

View File

@ -175,10 +175,6 @@ public class IncomingTextMessage implements Parcelable {
return message; return message;
} }
public IncomingTextMessage withMessageBody(String message) {
return new IncomingTextMessage(this, message);
}
public Address getSender() { public Address getSender() {
return sender; return sender;
} }
@ -250,7 +246,6 @@ public class IncomingTextMessage implements Parcelable {
public boolean isUnidentified() { public boolean isUnidentified() {
return unidentified; return unidentified;
} }
@Override @Override
public int describeContents() { public int describeContents() {
return 0; return 0;

View File

@ -21,6 +21,7 @@ import org.session.libsignal.libsignal.ecc.ECPrivateKey
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.api.opengroups.PublicChat
interface StorageProtocol { interface StorageProtocol {
@ -56,6 +57,8 @@ interface StorageProtocol {
// Open Groups // Open Groups
fun getOpenGroup(threadID: String): OpenGroup? fun getOpenGroup(threadID: String): OpenGroup?
fun getThreadID(openGroupID: String): String? fun getThreadID(openGroupID: String): String?
fun getAllOpenGroups(): Map<Long, PublicChat>
fun addOpenGroup(server: String, channel: Long)
// Open Group Public Keys // Open Group Public Keys
fun getOpenGroupPublicKey(server: String): String? fun getOpenGroupPublicKey(server: String): String?
@ -65,6 +68,13 @@ interface StorageProtocol {
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String) fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: String)
fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String? fun getOpenGroupDisplayName(publicKey: String, channel: Long, server: String): String?
// Open Group Metadata
fun setUserCount(group: Long, server: String, newValue: Int)
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
fun updateTitle(groupID: String, newValue: String)
fun updateProfilePicture(groupID: String, newValue: ByteArray)
// Last Message Server ID // Last Message Server ID
fun getLastMessageServerID(group: Long, server: String): Long? fun getLastMessageServerID(group: Long, server: String): Long?
fun setLastMessageServerID(group: Long, server: String, newValue: Long) fun setLastMessageServerID(group: Long, server: String, newValue: Long)
@ -75,13 +85,6 @@ interface StorageProtocol {
fun setLastDeletionServerID(group: Long, server: String, newValue: Long) fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
fun removeLastDeletionServerID(group: Long, server: String) fun removeLastDeletionServerID(group: Long, server: String)
// Open Group Metadata
fun setUserCount(group: Long, server: String, newValue: Int)
fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String)
fun getOpenGroupProfilePictureURL(group: Long, server: String): String?
fun updateTitle(groupID: String, newValue: String)
fun updateProfilePicture(groupID: String, newValue: ByteArray)
// Message Handling // Message Handling
fun getReceivedMessageTimestamps(): Set<Long> fun getReceivedMessageTimestamps(): Set<Long>
fun addReceivedMessageTimestamp(timestamp: Long) fun addReceivedMessageTimestamp(timestamp: Long)
@ -101,6 +104,11 @@ interface StorageProtocol {
fun removeMember(groupID: String, member: Address) fun removeMember(groupID: String, member: Address)
fun updateMembers(groupID: String, members: List<Address>) fun updateMembers(groupID: String, members: List<Address>)
// Closed Group // Closed Group
fun getAllClosedGroupPublicKeys(): Set<String>
fun addClosedGroupPublicKey(groupPublicKey: String)
fun removeClosedGroupPublicKey(groupPublicKey: String)
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type, fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type,
name: String, members: Collection<String>, admins: Collection<String>) name: String, members: Collection<String>, admins: Collection<String>)
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String, fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
@ -109,6 +117,9 @@ interface StorageProtocol {
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair> fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair? fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
// Groups
fun getAllGroups(): List<GroupRecord>
// Settings // Settings
fun setProfileSharing(address: Address, value: Boolean) fun setProfileSharing(address: Address, value: Boolean)

View File

@ -6,6 +6,8 @@ import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.utilities.toHexString
import org.session.libsignal.utilities.Hex
class ClosedGroupControlMessage() : ControlMessage() { class ClosedGroupControlMessage() : ControlMessage() {
@ -33,11 +35,23 @@ class ClosedGroupControlMessage() : ControlMessage() {
class NameChange(val name: String) : Kind() class NameChange(val name: String) : Kind()
class MembersAdded(val members: List<ByteString>) : Kind() class MembersAdded(val members: List<ByteString>) : Kind()
class MembersRemoved( val members: List<ByteString>) : Kind() class MembersRemoved( val members: List<ByteString>) : Kind()
class MemberLeft() : Kind() object MemberLeft : Kind()
val description: String = run {
when(this) {
is New -> "new"
is Update -> "update"
is EncryptionKeyPair -> "encryptionKeyPair"
is NameChange -> "nameChange"
is MembersAdded -> "membersAdded"
is MembersRemoved -> "membersRemoved"
MemberLeft -> "memberLeft"
}
}
} }
companion object { companion object {
const val TAG = "ClosedGroupUpdateV2" const val TAG = "ClosedGroupControlMessage"
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? { fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
val closedGroupUpdateProto = proto.dataMessage?.closedGroupUpdateV2 ?: return null val closedGroupUpdateProto = proto.dataMessage?.closedGroupUpdateV2 ?: return null
@ -75,7 +89,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
kind = Kind.MembersRemoved(closedGroupUpdateProto.membersList) kind = Kind.MembersRemoved(closedGroupUpdateProto.membersList)
} }
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT -> { SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT -> {
kind = Kind.MemberLeft() kind = Kind.MemberLeft
} }
} }
return ClosedGroupControlMessage(kind) return ClosedGroupControlMessage(kind)
@ -168,10 +182,15 @@ class ClosedGroupControlMessage() : ControlMessage() {
} }
} }
final class KeyPairWrapper(val publicKey: String?, private val encryptedKeyPair: ByteString?) { class KeyPairWrapper(val publicKey: String?, val encryptedKeyPair: ByteString?) {
val isValid: Boolean = run {
this.publicKey != null && this.encryptedKeyPair != null
}
companion object { companion object {
fun fromProto(proto: SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper): KeyPairWrapper { fun fromProto(proto: SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper): KeyPairWrapper {
return KeyPairWrapper(proto.publicKey.toString(), proto.encryptedKeyPair) return KeyPairWrapper(proto.publicKey.toByteArray().toHexString(), proto.encryptedKeyPair)
} }
} }
@ -179,7 +198,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
val publicKey = publicKey ?: return null val publicKey = publicKey ?: return null
val encryptedKeyPair = encryptedKeyPair ?: return null val encryptedKeyPair = encryptedKeyPair ?: return null
val result = SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper.newBuilder() val result = SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper.newBuilder()
result.publicKey = ByteString.copyFrom(publicKey.toByteArray()) result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
result.encryptedKeyPair = encryptedKeyPair result.encryptedKeyPair = encryptedKeyPair
return try { return try {

View File

@ -0,0 +1,106 @@
package org.session.libsession.messaging.messages.control
import com.google.protobuf.ByteString
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.threads.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.session.libsignal.utilities.Hex
class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups: List<String>): ControlMessage() {
class ClosedGroup(val publicKey: String, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<String>, val admins: List<String>) {
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
override fun toString(): String {
return name
}
companion object {
fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.ClosedGroup): ClosedGroup? {
if (!proto.hasPublicKey() || !proto.hasName() || !proto.hasEncryptionKeyPair()) return null
val publicKey = proto.publicKey.toByteArray().toHexString()
val name = proto.name
val encryptionKeyPairAsProto = proto.encryptionKeyPair
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()),
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
val members = proto.membersList.map { it.toByteArray().toHexString() }
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins)
}
}
fun toProto(): SignalServiceProtos.ConfigurationMessage.ClosedGroup? {
val result = SignalServiceProtos.ConfigurationMessage.ClosedGroup.newBuilder()
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
result.name = name
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
return result.build()
}
}
override val ttl: Long = 4 * 24 * 60 * 60 * 1000
override val isSelfSendValid: Boolean = true
companion object {
fun getCurrent(): ConfigurationMessage {
val closedGroups = mutableListOf<ClosedGroup>()
val openGroups = mutableListOf<String>()
val storage = MessagingConfiguration.shared.storage
val groups = storage.getAllGroups()
for (groupRecord in groups) {
if (groupRecord.isClosedGroup) {
if (!groupRecord.members.contains(Address.fromSerialized(storage.getUserPublicKey()!!))) continue
val groupPublicKey = GroupUtil.getDecodedGroupIDAsData(groupRecord.encodedId).toHexString()
if (!storage.isClosedGroup(groupPublicKey)) continue
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: continue
val closedGroup = ClosedGroup(groupPublicKey, groupRecord.title, encryptionKeyPair, groupRecord.members.map { it.serialize() }, groupRecord.admins.map { it.serialize() })
closedGroups.add(closedGroup)
}
if (groupRecord.isOpenGroup) {
val threadID = storage.getThreadID(groupRecord.encodedId) ?: continue
val openGroup = storage.getOpenGroup(threadID) ?: continue
openGroups.add(openGroup.server)
}
}
return ConfigurationMessage(closedGroups, openGroups)
}
fun fromProto(proto: SignalServiceProtos.Content): ConfigurationMessage? {
if (!proto.hasConfigurationMessage()) return null
val configurationProto = proto.configurationMessage
val closedGroups = configurationProto.closedGroupsList.mapNotNull { ClosedGroup.fromProto(it) }
val openGroups = configurationProto.openGroupsList
return ConfigurationMessage(closedGroups, openGroups)
}
}
override fun toProto(): SignalServiceProtos.Content? {
val configurationProto = SignalServiceProtos.ConfigurationMessage.newBuilder()
configurationProto.addAllClosedGroups(closedGroups.mapNotNull { it.toProto() })
configurationProto.addAllOpenGroups(openGroups)
val contentProto = SignalServiceProtos.Content.newBuilder()
contentProto.configurationMessage = configurationProto.build()
return contentProto.build()
}
override fun toString(): String {
return """
ConfigurationMessage(
closedGroups: ${(closedGroups)}
openGroups: ${(openGroups)}
)
""".trimIndent()
}
}

View File

@ -4,12 +4,8 @@ import android.text.TextUtils
import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.jobs.AttachmentDownloadJob import org.session.libsession.messaging.jobs.AttachmentDownloadJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate import org.session.libsession.messaging.messages.control.*
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.control.ReadReceipt
import org.session.libsession.messaging.messages.control.TypingIndicator
import org.session.libsession.messaging.messages.visible.Attachment import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment
@ -17,19 +13,19 @@ import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPrevie
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.GroupRecord
import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.utilities.Hex
import org.session.libsession.utilities.SSKEnvironment import org.session.libsession.utilities.SSKEnvironment
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.utilities.logging.Log import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.libsignal.util.guava.Optional import org.session.libsignal.libsignal.util.guava.Optional
import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchet import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchetCollectionType
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation
import org.session.libsignal.service.loki.utilities.toHexString import org.session.libsignal.service.loki.utilities.toHexString
import java.security.MessageDigest import java.security.MessageDigest
import java.util.* import java.util.*
@ -45,8 +41,9 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
when (message) { when (message) {
is ReadReceipt -> handleReadReceipt(message) is ReadReceipt -> handleReadReceipt(message)
is TypingIndicator -> handleTypingIndicator(message) is TypingIndicator -> handleTypingIndicator(message)
is ClosedGroupUpdate -> handleClosedGroupUpdate(message) is ClosedGroupControlMessage -> handleClosedGroupControlMessage(message)
is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message, proto) is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message, proto)
is ConfigurationMessage -> handleConfigurationMessage(message)
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID) is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
} }
} }
@ -105,6 +102,21 @@ fun MessageReceiver.disableExpirationTimer(message: ExpirationTimerUpdate, proto
SSKEnvironment.shared.messageExpirationManager.disableExpirationTimer(id, senderPublicKey, proto) SSKEnvironment.shared.messageExpirationManager.disableExpirationTimer(id, senderPublicKey, proto)
} }
private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMessage) {
val storage = MessagingConfiguration.shared.storage
if (message.sender != storage.getUserPublicKey()) return
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
for (closeGroup in message.closedGroups) {
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue
handleNewClosedGroup(message.sender!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair, closeGroup.members, closeGroup.admins)
}
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
for (openGroup in message.openGroups) {
if (allOpenGroups.contains(openGroup)) continue
storage.addOpenGroup(openGroup, 1)
}
}
fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalServiceProtos.Content, openGroupID: String?) { fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalServiceProtos.Content, openGroupID: String?) {
val storage = MessagingConfiguration.shared.storage val storage = MessagingConfiguration.shared.storage
val context = MessagingConfiguration.shared.context val context = MessagingConfiguration.shared.context
@ -188,173 +200,293 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
SSKEnvironment.shared.notificationManager.updateNotification(context, threadID) SSKEnvironment.shared.notificationManager.updateNotification(context, threadID)
} }
private fun MessageReceiver.handleClosedGroupUpdate(message: ClosedGroupUpdate) { private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroupControlMessage) {
when (message.kind!!) { when (message.kind!!) {
is ClosedGroupUpdate.Kind.New -> handleNewGroup(message) is ClosedGroupControlMessage.Kind.New -> handleNewClosedGroup(message)
is ClosedGroupUpdate.Kind.Info -> handleGroupUpdate(message) is ClosedGroupControlMessage.Kind.Update -> handleClosedGroupUpdated(message)
is ClosedGroupUpdate.Kind.SenderKeyRequest -> handleSenderKeyRequest(message) is ClosedGroupControlMessage.Kind.EncryptionKeyPair -> handleClosedGroupEncryptionKeyPair(message)
is ClosedGroupUpdate.Kind.SenderKey -> handleSenderKey(message) is ClosedGroupControlMessage.Kind.NameChange -> handleClosedGroupNameChanged(message)
is ClosedGroupControlMessage.Kind.MembersAdded -> handleClosedGroupMembersAdded(message)
is ClosedGroupControlMessage.Kind.MembersRemoved -> handleClosedGroupMembersRemoved(message)
ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message)
} }
} }
private fun MessageReceiver.handleNewGroup(message: ClosedGroupUpdate) { private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMessage) {
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.New ?: return
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
val members = kind.members.map { it.toByteArray().toHexString() }
val admins = kind.admins.map { it.toByteArray().toHexString() }
handleNewClosedGroup(message.sender!!, groupPublicKey, kind.name, kind.encryptionKeyPair, members, admins)
}
// Parameter @sender:String is just for inserting incoming info message
private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>) {
val context = MessagingConfiguration.shared.context val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage val storage = MessagingConfiguration.shared.storage
val sskDatabase = MessagingConfiguration.shared.sskDatabase
if (message.kind !is ClosedGroupUpdate.Kind.New) { return }
val kind = message.kind!! as ClosedGroupUpdate.Kind.New
val groupPublicKey = kind.groupPublicKey.toHexString()
val name = kind.name
val groupPrivateKey = kind.groupPrivateKey
val senderKeys = kind.senderKeys
val members = kind.members.map { it.toHexString() }
val admins = kind.admins.map { it.toHexString() }
// Persist the ratchets
senderKeys.forEach { senderKey ->
if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach }
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet, ClosedGroupRatchetCollectionType.Current)
}
// Sort out any discrepancies between the provided sender keys and what's required
val missingSenderKeys = members.toSet().subtract(senderKeys.map { Hex.toStringCondensed(it.publicKey) })
val userPublicKey = storage.getUserPublicKey()!!
if (missingSenderKeys.contains(userPublicKey)) {
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
members.forEach { member ->
if (member == userPublicKey) return@forEach
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(groupPublicKey.toByteArray(), userSenderKey)
val closedGroupUpdate = ClosedGroupUpdate()
closedGroupUpdate.kind = closedGroupUpdateKind
MessageSender.send(closedGroupUpdate, Destination.ClosedGroup(groupPublicKey))
}
}
missingSenderKeys.minus(userPublicKey).forEach { publicKey ->
MessageSender.requestSenderKey(groupPublicKey, publicKey)
}
// Create the group // Create the group
val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
if (storage.getGroup(groupID) != null) { if (storage.getGroup(groupID) != null) {
// Update the group // Update the group
storage.updateTitle(groupID, name) storage.updateTitle(groupID, name)
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
} else { } else {
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it) })) null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
} }
storage.setProfileSharing(Address.fromSerialized(groupID), true) storage.setProfileSharing(Address.fromSerialized(groupID), true)
// Add the group to the user's set of public keys to poll for // Add the group to the user's set of public keys to poll for
sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString()) storage.addClosedGroupPublicKey(groupPublicKey)
// Notify the PN server // Store the encryption key pair
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey) storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user // Notify the user
storage.insertIncomingInfoMessage(context, message.sender!!, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins) storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, storage.getUserPublicKey()!!)
} }
private fun MessageReceiver.handleGroupUpdate(message: ClosedGroupUpdate) { private fun MessageReceiver.handleClosedGroupUpdated(message: ClosedGroupControlMessage) {
// Prepare
val context = MessagingConfiguration.shared.context val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage val storage = MessagingConfiguration.shared.storage
val sskDatabase = MessagingConfiguration.shared.sskDatabase val senderPublicKey = message.sender ?: return
if (message.kind !is ClosedGroupUpdate.Kind.Info) { return } val kind = message.kind!! as? ClosedGroupControlMessage.Kind.Update ?: return
val kind = message.kind!! as ClosedGroupUpdate.Kind.Info val groupPublicKey = message.groupPublicKey ?: return
val groupPublicKey = kind.groupPublicKey.toHexString()
val name = kind.name
val senderKeys = kind.senderKeys
val members = kind.members.map { it.toHexString() }
val admins = kind.admins.map { it.toHexString() }
// Get the group
val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded
val group = storage.getGroup(groupID) ?: return Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
// Check that the sender is a member of the group (before the update)
if (!group.members.contains(Address.fromSerialized(message.sender!!))) { return Log.d("Loki", "Ignoring closed group info message from non-member.") }
// Store the ratchets for any new members (it's important that this happens before the code below)
senderKeys.forEach { senderKey ->
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet, ClosedGroupRatchetCollectionType.Current)
}
// Delete all ratchets and either:
// • Send out the user's new ratchet using established channels if other members of the group left or were removed
// • Remove the group from the user's set of public keys to poll for if the current user was among the members that were removed
val oldMembers = group.members.map { it.serialize() }.toSet()
val userPublicKey = storage.getUserPublicKey()!! val userPublicKey = storage.getUserPublicKey()!!
val wasUserRemoved = !members.contains(userPublicKey) // Unwrap the message
val wasSenderRemoved = !members.contains(message.sender!!) val name = kind.name
if (members.toSet().intersect(oldMembers) != oldMembers.toSet()) { val members = kind.members.map { it.toByteArray().toHexString() }
val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current) val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
for (pair in allOldRatchets) { val group = storage.getGroup(groupID) ?: run {
val senderPublicKey = pair.first Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
val ratchet = pair.second return
val collection = ClosedGroupRatchetCollectionType.Old }
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet, collection) val oldMembers = group.members.map { it.serialize() }
} // Check common group update logic
sskDatabase.removeAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current) if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
if (wasUserRemoved) { return
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) }
storage.setActive(groupID, false) // Check that the admin wasn't removed unless the group was destroyed entirely
storage.removeMember(groupID, Address.fromSerialized(userPublicKey)) if (!members.contains(group.admins.first().toString()) && members.isNotEmpty()) {
// Notify the PN server android.util.Log.d("Loki", "Ignoring invalid closed group update message.")
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey) return
} else { }
val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) // Remove the group from the user's set of public keys to poll for if the current user was removed
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) val wasCurrentUserRemoved = !members.contains(userPublicKey)
members.forEach { member -> if (wasCurrentUserRemoved) {
if (member == userPublicKey) return@forEach disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
val address = Address.fromSerialized(member) }
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) // Generate and distribute a new encryption key pair if needed
val closedGroupUpdate = ClosedGroupUpdate() val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
closedGroupUpdate.kind = closedGroupUpdateKind val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey)
MessageSender.send(closedGroupUpdate, address) if (wasAnyUserRemoved && isCurrentUserAdmin) {
} MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members)
}
} }
// Update the group // Update the group
storage.updateTitle(groupID, name) storage.updateTitle(groupID, name)
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) if (!wasCurrentUserRemoved) {
// Notify the user if needed // The call below sets isActive to true, so if the user is leaving we have to use groupDB.remove(...) instead
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
}
// Notify the user
val wasSenderRemoved = !members.contains(senderPublicKey)
val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
storage.insertIncomingInfoMessage(context, message.sender!!, groupID, type0, type1, name, members, admins) storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() })
} }
private fun MessageReceiver.handleSenderKeyRequest(message: ClosedGroupUpdate) { private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) {
if (message.kind !is ClosedGroupUpdate.Kind.SenderKeyRequest) { return } // Prepare
val kind = message.kind!! as ClosedGroupUpdate.Kind.SenderKeyRequest
val storage = MessagingConfiguration.shared.storage val storage = MessagingConfiguration.shared.storage
val sskDatabase = MessagingConfiguration.shared.sskDatabase val senderPublicKey = message.sender ?: return
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.EncryptionKeyPair ?: return
val groupPublicKey = message.groupPublicKey ?: return
val userPublicKey = storage.getUserPublicKey()!! val userPublicKey = storage.getUserPublicKey()!!
val groupPublicKey = kind.groupPublicKey.toHexString() val userKeyPair = storage.getUserX25519KeyPair()
val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded // Unwrap the message
val group = storage.getGroup(groupID) val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
if (group == null) { val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group sender key request for nonexistent group.") Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
return return
} }
// Check that the requesting user is a member of the group if (!group.admins.map { it.toString() }.contains(senderPublicKey)) {
if (!group.members.map { it.serialize() }.contains(message.sender!!)) { android.util.Log.d("Loki", "Ignoring closed group encryption key pair from non-admin.")
Log.d("Loki", "Ignoring closed group sender key request from non-member.")
return return
} }
// Respond to the request // Find our wrapper and decrypt it if possible
Log.d("Loki", "Responding to sender key request from: ${message.sender!!}.") val wrapper = kind.wrappers.firstOrNull { it.publicKey!!.toByteArray().toHexString() == userPublicKey } ?: return
val userRatchet = sskDatabase.getClosedGroupRatchet(groupPublicKey, userPublicKey, ClosedGroupRatchetCollectionType.Current) val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray()
?: SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) val plaintext = MessageReceiverDecryption.decryptWithSessionProtocol(encryptedKeyPair, userKeyPair).first
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) // Parse it
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
val closedGroupUpdate = ClosedGroupUpdate() val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
closedGroupUpdate.kind = closedGroupUpdateKind // Store it
MessageSender.send(closedGroupUpdate, Address.fromSerialized(groupID)) storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey)
Log.d("Loki", "Received a new closed group encryption key pair")
} }
private fun MessageReceiver.handleSenderKey(message: ClosedGroupUpdate) { private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupControlMessage) {
if (message.kind !is ClosedGroupUpdate.Kind.SenderKey) { return } val context = MessagingConfiguration.shared.context
val kind = message.kind!! as ClosedGroupUpdate.Kind.SenderKey val storage = MessagingConfiguration.shared.storage
val groupPublicKey = kind.groupPublicKey.toHexString() val senderPublicKey = message.sender ?: return
val senderKey = kind.senderKey val kind = message.kind!! as? ClosedGroupControlMessage.Kind.NameChange ?: return
if (senderKey.publicKey.toHexString() != message.sender!!) { val groupPublicKey = message.groupPublicKey ?: return
Log.d("Loki", "Ignoring invalid closed group sender key.") // Check that the sender is a member of the group (before the update)
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
return return
} }
Log.d("Loki", "Received a sender key from: ${message.sender!!}.") // Check common group update logic
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
MessagingConfiguration.shared.sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet, ClosedGroupRatchetCollectionType.Current) return
}
val members = group.members.map { it.serialize() }
val admins = group.admins.map { it.serialize() }
val name = kind.name
storage.updateTitle(groupID, name)
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
}
private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupControlMessage) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val senderPublicKey = message.sender ?: return
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return
val groupPublicKey = message.groupPublicKey ?: return
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
return
}
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
return
}
val name = group.title
// Check common group update logic
val members = group.members.map { it.serialize() }
val admins = group.admins.map { it.serialize() }
// Users that are part of this remove update
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
// newMembers to save is old members minus removed members
val newMembers = members + updateMembers
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
}
private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
val senderPublicKey = message.sender ?: return
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersRemoved ?: return
val groupPublicKey = message.groupPublicKey ?: return
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
return
}
val name = group.title
// Check common group update logic
val members = group.members.map { it.serialize() }
val admins = group.admins.map { it.toString() }
// Users that are part of this remove update
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
// If admin leaves the group is disbanded
val didAdminLeave = admins.any { it in updateMembers }
// newMembers to save is old members minus removed members
val newMembers = members - updateMembers
// user should be posting MEMBERS_LEFT so this should not be encountered
val senderLeft = senderPublicKey in updateMembers
if (senderLeft) {
android.util.Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey")
}
val wasCurrentUserRemoved = userPublicKey in updateMembers
// admin should send a MEMBERS_LEFT message but handled here in case
if (didAdminLeave || wasCurrentUserRemoved) {
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
} else {
val isCurrentUserAdmin = admins.contains(userPublicKey)
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
if (isCurrentUserAdmin) {
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers)
}
}
val (contextType, signalType) =
if (senderLeft) SignalServiceProtos.GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
else SignalServiceProtos.GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins)
}
private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val senderPublicKey = message.sender ?: return
val userPublicKey = storage.getUserPublicKey()!!
if (senderPublicKey == userPublicKey) { return } // Check the user leaving isn't us, will already be handled
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return
val groupPublicKey = message.groupPublicKey ?: return
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
return
}
val name = group.title
// Check common group update logic
val members = group.members.map { it.serialize() }
val admins = group.admins.map { it.toString() }
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
return
}
// If admin leaves the group is disbanded
val didAdminLeave = admins.contains(senderPublicKey)
val updatedMemberList = members - senderPublicKey
if (didAdminLeave) {
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
} else {
val isCurrentUserAdmin = admins.contains(userPublicKey)
storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) })
if (isCurrentUserAdmin) {
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
}
}
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins)
}
private fun isValidGroupUpdate(group: GroupRecord,
sentTimestamp: Long,
senderPublicKey: String): Boolean {
val oldMembers = group.members.map { it.serialize() }
// Check that the message isn't from before the group was created
if (group.createdAt > sentTimestamp) {
android.util.Log.d("Loki", "Ignoring closed group update from before thread was created.")
return false
}
// Check that the sender is a member of the group (before the update)
if (senderPublicKey !in oldMembers) {
android.util.Log.d("Loki", "Ignoring closed group info message from non-member.")
return false
}
return true
}
fun MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey: String, groupID: String, userPublicKey: String) {
val storage = MessagingConfiguration.shared.storage
storage.removeClosedGroupPublicKey(groupPublicKey)
// Remove the key pairs
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
// Mark the group as inactive
storage.setActive(groupID, false)
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
} }

View File

@ -41,6 +41,7 @@ object MessageSender {
// Closed groups // Closed groups
object NoThread : Error("Couldn't find a thread associated with the given group public key.") object NoThread : Error("Couldn't find a thread associated with the given group public key.")
object NoKeyPair: Error("Couldn't find a private key associated with the given group public key.")
object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.") object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.")
object InvalidClosedGroupUpdate : Error("Invalid group update.") object InvalidClosedGroupUpdate : Error("Invalid group update.")

View File

@ -3,15 +3,19 @@
package org.session.libsession.messaging.sending_receiving package org.session.libsession.messaging.sending_receiving
import android.util.Log import android.util.Log
import com.google.protobuf.ByteString
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred import nl.komponents.kovenant.deferred
import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
import org.session.libsession.messaging.messages.control.ClosedGroupUpdate import org.session.libsession.messaging.messages.control.ClosedGroupUpdate
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.MessageSender.Error import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import org.session.libsignal.libsignal.ecc.Curve import org.session.libsignal.libsignal.ecc.Curve
@ -21,56 +25,193 @@ import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSende
import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation
import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey import org.session.libsignal.service.loki.utilities.hexEncodedPrivateKey
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.ThreadUtils
import java.util.* import java.util.*
fun MessageSender.createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> { fun MessageSender.createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
val deferred = deferred<String, Exception>() val deferred = deferred<String, Exception>()
// Prepare ThreadUtils.queue {
val context = MessagingConfiguration.shared.context // Prepare
val storage = MessagingConfiguration.shared.storage val context = MessagingConfiguration.shared.context
val members = members val storage = MessagingConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!! val userPublicKey = storage.getUserPublicKey()!!
// Generate a key pair for the group val membersAsData = members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
val groupKeyPair = Curve.generateKeyPair() // Generate the group's public key
val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix
members.plus(userPublicKey) // Generate the key pair that'll be used for encryption and decryption
val membersAsData = members.map { Hex.fromStringCondensed(it) } val encryptionKeyPair = Curve.generateKeyPair()
// Create ratchets for all members // Create the group
val senderKeys: List<ClosedGroupSenderKey> = members.map { publicKey -> val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) val admins = setOf( userPublicKey )
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
storage.setProfileSharing(Address.fromSerialized(groupID), true)
// Send a closed group update message to all members individually
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
for (member in members) {
if (member == userPublicKey) { continue }
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).get()
}
// Add the group to the user's set of public keys to poll for
storage.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID)
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Fulfill the promise
deferred.resolve(groupID)
} }
// Create the group
val admins = setOf( userPublicKey )
val adminsAsData = admins.map { Hex.fromStringCondensed(it) }
val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), null, null, LinkedList(admins.map { Address.fromSerialized(it) }))
storage.setProfileSharing(Address.fromSerialized(groupID), true)
// Send a closed group update message to all members using established channels
val promises = mutableListOf<Promise<Unit, Exception>>()
for (member in members) {
if (member == userPublicKey) { continue }
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(),
senderKeys, membersAsData, adminsAsData)
val closedGroupUpdate = ClosedGroupUpdate()
closedGroupUpdate.kind = closedGroupUpdateKind
val address = Address.fromSerialized(member)
val promise = MessageSender.sendNonDurably(closedGroupUpdate, address)
promises.add(promise)
}
// Add the group to the user's set of public keys to poll for
MessagingConfiguration.shared.sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey)
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Notify the user
val threadID =storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID)
// Fulfill the promise
deferred.resolve(groupPublicKey)
// Return // Return
return deferred.promise return deferred.promise
} }
fun MessageSender.v2_update(groupPublicKey: String, members: List<String>, name: String) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Can't update nonexistent closed group.")
throw Error.NoThread
}
// Update name if needed
if (name != group.title) { setName(groupPublicKey, name) }
// Add members if needed
val addedMembers = members - group.members.map { it.serialize() }
if (!addedMembers.isEmpty()) { addMembers(groupPublicKey, addedMembers) }
// Remove members if needed
val removedMembers = group.members.map { it.serialize() } - members
if (removedMembers.isEmpty()) { removeMembers(groupPublicKey, removedMembers) }
}
fun MessageSender.setName(groupPublicKey: String, newName: String) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Can't change name for nonexistent closed group.")
throw Error.NoThread
}
val members = group.members.map { it.serialize() }.toSet()
val admins = group.admins.map { it.serialize() }
// Send the update to the group
val kind = ClosedGroupControlMessage.Kind.NameChange(newName)
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
send(closedGroupControlMessage, Address.fromSerialized(groupID))
// Update the group
storage.updateTitle(groupID, newName)
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID)
}
fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Can't add members to nonexistent closed group.")
throw Error.NoThread
}
if (membersToAdd.isEmpty()) {
Log.d("Loki", "Invalid closed group update.")
throw Error.InvalidClosedGroupUpdate
}
val updatedMembers = group.members.map { it.serialize() }.toSet() + membersToAdd
// Save the new group members
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
val membersAsData = updatedMembers.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
val newMembersAsData = membersToAdd.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
val admins = group.admins.map { it.serialize() }
val adminsAsData = admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
val encryptionKeyPair = storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: run {
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
throw Error.NoKeyPair
}
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData)
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
send(closedGroupControlMessage, Address.fromSerialized(groupID))
// Send closed group update messages to any new members individually
for (member in membersToAdd) {
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind)
send(closedGroupControlMessage, Address.fromSerialized(member))
}
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
}
fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<String>) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Can't remove members from nonexistent closed group.")
throw Error.NoThread
}
if (membersToRemove.isEmpty()) {
Log.d("Loki", "Invalid closed group update.")
throw Error.InvalidClosedGroupUpdate
}
val updatedMembers = group.members.map { it.serialize() }.toSet() - membersToRemove
// Save the new group members
storage.updateMembers(groupID, updatedMembers.map { Address.fromSerialized(it) })
val removeMembersAsData = membersToRemove.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
val admins = group.admins.map { it.serialize() }
if (membersToRemove.any { it in admins } && updatedMembers.isNotEmpty()) {
Log.d("Loki", "Can't remove admin from closed group unless the group is destroyed entirely.")
throw Error.InvalidClosedGroupUpdate
}
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData)
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
send(closedGroupControlMessage, Address.fromSerialized(groupID))
val isCurrentUserAdmin = admins.contains(userPublicKey)
if (isCurrentUserAdmin) {
generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMembers)
}
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
}
fun MessageSender.v2_leave(groupPublicKey: String) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Can't leave nonexistent closed group.")
throw Error.NoThread
}
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
val admins = group.admins.map { it.serialize() }
val name = group.title
// Send the update to the group
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft)
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
// Remove the group private key and unsubscribe from PNs
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
}
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.QUIT
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
}
fun MessageSender.update(groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> { fun MessageSender.update(groupPublicKey: String, members: Collection<String>, name: String): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>() val deferred = deferred<Unit, Exception>()
val context = MessagingConfiguration.shared.context val context = MessagingConfiguration.shared.context
@ -216,11 +357,34 @@ fun MessageSender.leave(groupPublicKey: String) {
return update(groupPublicKey, newMembers, name).get() return update(groupPublicKey, newMembers, name).get()
} }
fun MessageSender.requestSenderKey(groupPublicKey: String, senderPublicKey: String) { fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, targetMembers: Collection<String>) {
Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.") // Prepare
val address = Address.fromSerialized(senderPublicKey) val storage = MessagingConfiguration.shared.storage
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey)) val userPublicKey = storage.getUserPublicKey()!!
val closedGroupUpdate = ClosedGroupUpdate() val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
closedGroupUpdate.kind = closedGroupUpdateKind val group = storage.getGroup(groupID) ?: run {
MessageSender.send(closedGroupUpdate, address) Log.d("Loki", "Can't update nonexistent closed group.")
throw Error.NoThread
}
if (!group.admins.map { it.toString() }.contains(userPublicKey)) {
Log.d("Loki", "Can't distribute new encryption key pair as non-admin.")
throw Error.InvalidClosedGroupUpdate
}
// Generate the new encryption key pair
val newKeyPair = Curve.generateKeyPair()
// Distribute it
val proto = SignalServiceProtos.KeyPair.newBuilder()
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
val plaintext = proto.build().toByteArray()
val wrappers = targetMembers.map { publicKey ->
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
}
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(wrappers)
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
// Store it * after * having sent out the message to the group
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
}
} }

View File

@ -1,5 +1,6 @@
package org.session.libsession.messaging.sending_receiving.notifications package org.session.libsession.messaging.sending_receiving.notifications
import android.annotation.SuppressLint
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.* import okhttp3.*
import org.session.libsession.messaging.MessagingConfiguration import org.session.libsession.messaging.MessagingConfiguration

View File

@ -152,6 +152,7 @@ class Address private constructor(address: String) : Parcelable, Comparable<Addr
val UNKNOWN = Address("Unknown") val UNKNOWN = Address("Unknown")
private val TAG = Address::class.java.simpleName private val TAG = Address::class.java.simpleName
private val cachedFormatter = AtomicReference<Pair<String, ExternalAddressFormatter>>() private val cachedFormatter = AtomicReference<Pair<String, ExternalAddressFormatter>>()
@JvmStatic
fun fromSerialized(serialized: String): Address { fun fromSerialized(serialized: String): Address {
return Address(serialized) return Address(serialized)
} }

View File

@ -13,7 +13,7 @@ import org.session.libsignal.service.api.crypto.UnidentifiedAccessPair
object UnidentifiedAccessUtil { object UnidentifiedAccessUtil {
private val TAG = UnidentifiedAccessUtil::class.simpleName private val TAG = UnidentifiedAccessUtil::class.simpleName
private val sodium = LazySodiumAndroid(SodiumAndroid()) private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
fun getAccessFor(recipientPublicKey: String): UnidentifiedAccessPair? { fun getAccessFor(recipientPublicKey: String): UnidentifiedAccessPair? {
try { try {

View File

@ -2,6 +2,8 @@ package org.session.libsession.utilities
import org.session.libsignal.service.api.messages.SignalServiceGroup import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.Hex
import java.io.IOException
import kotlin.jvm.Throws
object GroupUtil { object GroupUtil {
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!" const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
@ -58,11 +60,27 @@ object GroupUtil {
return groupId.startsWith(MMS_GROUP_PREFIX) return groupId.startsWith(MMS_GROUP_PREFIX)
} }
@JvmStatic
fun isOpenGroup(groupId: String): Boolean { fun isOpenGroup(groupId: String): Boolean {
return groupId.startsWith(OPEN_GROUP_PREFIX) return groupId.startsWith(OPEN_GROUP_PREFIX)
} }
@JvmStatic
fun isClosedGroup(groupId: String): Boolean { fun isClosedGroup(groupId: String): Boolean {
return groupId.startsWith(CLOSED_GROUP_PREFIX) return groupId.startsWith(CLOSED_GROUP_PREFIX)
} }
// NOTE: Signal group ID handling is weird. The ID is double encoded in the database, but not in a `GroupContext`.
@JvmStatic
@Throws(IOException::class)
fun doubleEncodeGroupID(groupPublicKey: String): String {
return getEncodedClosedGroupID(getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray())
}
@JvmStatic
@Throws(IOException::class)
fun doubleDecodeGroupID(groupID: String): ByteArray {
return getDecodedGroupIDAsData(getDecodedGroupID(groupID))
}
} }

View File

@ -128,6 +128,40 @@ object TextSecurePreferences {
private const val FCM_TOKEN = "pref_fcm_token" private const val FCM_TOKEN = "pref_fcm_token"
private const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2" private const val LAST_FCM_TOKEN_UPLOAD_TIME = "pref_last_fcm_token_upload_time_2"
// region Multi Device
private const val IS_USING_MULTI_DEVICE = "pref_is_using_multi_device"
private const val LAST_CONFIGURATION_SYNC_TIME = "pref_last_configuration_sync_time"
private const val CONFIGURATION_SYNCED = "pref_configuration_synced"
@JvmStatic
fun isUsingMultiDevice(context: Context): Boolean {
return getBooleanPreference(context, IS_USING_MULTI_DEVICE, false)
}
@JvmStatic
fun setIsUsingMultiDevice(context: Context, value: Boolean) {
setBooleanPreference(context, IS_USING_MULTI_DEVICE, value)
}
@JvmStatic
fun getLastConfigurationSyncTime(context: Context): Long {
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
}
@JvmStatic
fun setLastConfigurationSyncTime(context: Context, value: Long) {
setLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, value)
}
@JvmStatic
fun getConfigurationMessageSynced(context: Context): Boolean {
return getBooleanPreference(context, CONFIGURATION_SYNCED, false)
}
@JvmStatic
fun setConfigurationMessageSynced(context: Context, value: Boolean) {
setBooleanPreference(context, CONFIGURATION_SYNCED, value)
}
@JvmStatic @JvmStatic
fun isUsingFCM(context: Context): Boolean { fun isUsingFCM(context: Context): Boolean {

View File

@ -26,10 +26,6 @@ import org.session.libsignal.service.api.messages.SignalServiceDataMessage;
import org.session.libsignal.service.api.messages.SignalServiceGroup; import org.session.libsignal.service.api.messages.SignalServiceGroup;
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage; import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage; import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
import org.session.libsignal.service.api.messages.calls.AnswerMessage;
import org.session.libsignal.service.api.messages.calls.IceUpdateMessage;
import org.session.libsignal.service.api.messages.calls.OfferMessage;
import org.session.libsignal.service.api.messages.calls.SignalServiceCallMessage;
import org.session.libsignal.service.api.messages.multidevice.BlockedListMessage; import org.session.libsignal.service.api.messages.multidevice.BlockedListMessage;
import org.session.libsignal.service.api.messages.multidevice.ConfigurationMessage; import org.session.libsignal.service.api.messages.multidevice.ConfigurationMessage;
import org.session.libsignal.service.api.messages.multidevice.ReadMessage; import org.session.libsignal.service.api.messages.multidevice.ReadMessage;
@ -51,7 +47,6 @@ import org.session.libsignal.service.internal.push.PushServiceSocket;
import org.session.libsignal.service.internal.push.PushTransportDetails; import org.session.libsignal.service.internal.push.PushTransportDetails;
import org.session.libsignal.service.internal.push.SignalServiceProtos; import org.session.libsignal.service.internal.push.SignalServiceProtos;
import org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer; import org.session.libsignal.service.internal.push.SignalServiceProtos.AttachmentPointer;
import org.session.libsignal.service.internal.push.SignalServiceProtos.CallMessage;
import org.session.libsignal.service.internal.push.SignalServiceProtos.Content; import org.session.libsignal.service.internal.push.SignalServiceProtos.Content;
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage;
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext; import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
@ -217,34 +212,14 @@ public class SignalServiceMessageSender {
* @param recipient The sender of the received message you're acknowledging. * @param recipient The sender of the received message you're acknowledging.
* @param message The read receipt to deliver. * @param message The read receipt to deliver.
* @throws IOException * @throws IOException
* @throws UntrustedIdentityException
*/ */
public void sendReceipt(SignalServiceAddress recipient, public void sendReceipt(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess, Optional<UnidentifiedAccessPair> unidentifiedAccess,
SignalServiceReceiptMessage message) SignalServiceReceiptMessage message)
throws IOException, UntrustedIdentityException throws IOException {
{
byte[] content = createReceiptContent(message); byte[] content = createReceiptContent(message);
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store);
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, message.getTTL(), useFallbackEncryption, false); sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, message.getTTL(), useFallbackEncryption);
}
/**
* Send a typing indicator.
*
* @param recipient The destination
* @param message The typing indicator to deliver
* @throws IOException
* @throws UntrustedIdentityException
*/
public void sendTyping(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
SignalServiceTypingMessage message)
throws IOException, UntrustedIdentityException
{
byte[] content = createTypingContent(message);
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store);
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, message.getTTL(), useFallbackEncryption, false);
} }
public void sendTyping(List<SignalServiceAddress> recipients, public void sendTyping(List<SignalServiceAddress> recipients,
@ -256,42 +231,24 @@ public class SignalServiceMessageSender {
sendMessage(0, recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, message.getTTL(), false, false); sendMessage(0, recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, message.getTTL(), false, false);
} }
/**
* Send a call setup message to a single recipient.
*
* @param recipient The message's destination.
* @param message The call message.
* @throws IOException
*/
public void sendCallMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
SignalServiceCallMessage message)
throws IOException, UntrustedIdentityException
{
byte[] content = createCallContent(message);
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store);
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), System.currentTimeMillis(), content, false, message.getTTL(), useFallbackEncryption, false);
}
/** /**
* Send a message to a single recipient. * Send a message to a single recipient.
* *
* @param recipient The message's destination. * @param recipient The message's destination.
* @param message The message. * @param message The message.
* @throws UntrustedIdentityException
* @throws IOException * @throws IOException
*/ */
public SendMessageResult sendMessage(long messageID, public SendMessageResult sendMessage(long messageID,
SignalServiceAddress recipient, SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess, Optional<UnidentifiedAccessPair> unidentifiedAccess,
SignalServiceDataMessage message) SignalServiceDataMessage message)
throws UntrustedIdentityException, IOException throws IOException
{ {
byte[] content = createMessageContent(message, recipient); byte[] content = createMessageContent(message, recipient);
long timestamp = message.getTimestamp(); long timestamp = message.getTimestamp();
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store);
boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL; boolean isClosedGroup = message.group.isPresent() && message.group.get().getGroupType() == SignalServiceGroup.GroupType.SIGNAL;
SendMessageResult result = sendMessage(messageID, recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, message.getTTL(), message.getDeviceLink().isPresent(), useFallbackEncryption, isClosedGroup, false, message.hasVisibleContent()); SendMessageResult result = sendMessage(messageID, recipient, getTargetUnidentifiedAccess(unidentifiedAccess), timestamp, content, false, message.getTTL(), message.getDeviceLink().isPresent(), useFallbackEncryption, isClosedGroup, message.hasVisibleContent(), message.getSyncTarget());
// // Loki - This shouldn't get invoked for note to self // // Loki - This shouldn't get invoked for note to self
// boolean wouldSignalSendSyncMessage = (result.getSuccess() != null && result.getSuccess().isNeedsSync()) || unidentifiedAccess.isPresent(); // boolean wouldSignalSendSyncMessage = (result.getSuccess() != null && result.getSuccess().isNeedsSync()) || unidentifiedAccess.isPresent();
@ -325,8 +282,7 @@ public class SignalServiceMessageSender {
List<SignalServiceAddress> recipients, List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess, List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
SignalServiceDataMessage message) SignalServiceDataMessage message)
throws IOException, UntrustedIdentityException throws IOException {
{
// Loki - We only need the first recipient in the line below. This is because the recipient is only used to determine // Loki - We only need the first recipient in the line below. This is because the recipient is only used to determine
// whether an attachment is being sent to an open group or not. // whether an attachment is being sent to an open group or not.
byte[] content = createMessageContent(message, recipients.get(0)); byte[] content = createMessageContent(message, recipients.get(0));
@ -350,7 +306,7 @@ public class SignalServiceMessageSender {
for (String device : linkedDevices) { for (String device : linkedDevices) {
SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device); SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device);
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(syncMessage, device, store); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(syncMessage, device, store);
sendMessage(deviceAsAddress, Optional.<UnidentifiedAccess>absent(), timestamp, syncMessage, false, message.getTTL(), useFallbackEncryption, true); sendMessage(deviceAsAddress, Optional.<UnidentifiedAccess>absent(), timestamp, syncMessage, false, message.getTTL(), useFallbackEncryption);
} }
} }
@ -381,7 +337,6 @@ public class SignalServiceMessageSender {
} else if (message.getStickerPackOperations().isPresent()) { } else if (message.getStickerPackOperations().isPresent()) {
content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get()); content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get());
} else if (message.getVerified().isPresent()) { } else if (message.getVerified().isPresent()) {
sendMessage(message.getVerified().get(), unidentifiedAccess);
return; return;
} else { } else {
throw new IOException("Unsupported sync message!"); throw new IOException("Unsupported sync message!");
@ -392,18 +347,10 @@ public class SignalServiceMessageSender {
for (String device : linkedDevices) { for (String device : linkedDevices) {
SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device); SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device);
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, device, store); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, device, store);
sendMessageToPrivateChat(0, deviceAsAddress, Optional.<UnidentifiedAccess>absent(), timestamp, content, false, message.getTTL(), useFallbackEncryption, false, false); sendMessageToPrivateChat(0, deviceAsAddress, Optional.absent(), timestamp, content, false, message.getTTL(), useFallbackEncryption, false, false, Optional.absent());
} }
} }
public void setSoTimeoutMillis(long soTimeoutMillis) {
socket.setSoTimeoutMillis(soTimeoutMillis);
}
public void cancelInFlightRequests() {
socket.cancelInFlightRequests();
}
public void setMessagePipe(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe) { public void setMessagePipe(SignalServiceMessagePipe pipe, SignalServiceMessagePipe unidentifiedPipe) {
this.pipe.set(Optional.fromNullable(pipe)); this.pipe.set(Optional.fromNullable(pipe));
this.unidentifiedPipe.set(Optional.fromNullable(unidentifiedPipe)); this.unidentifiedPipe.set(Optional.fromNullable(unidentifiedPipe));
@ -452,12 +399,6 @@ public class SignalServiceMessageSender {
result.getUrl()); result.getUrl());
} }
private void sendMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
throws IOException, UntrustedIdentityException
{
}
private byte[] createTypingContent(SignalServiceTypingMessage message) { private byte[] createTypingContent(SignalServiceTypingMessage message) {
Content.Builder container = Content.newBuilder(); Content.Builder container = Content.newBuilder();
TypingMessage.Builder builder = TypingMessage.newBuilder(); TypingMessage.Builder builder = TypingMessage.newBuilder();
@ -494,32 +435,6 @@ public class SignalServiceMessageSender {
{ {
Content.Builder container = Content.newBuilder(); Content.Builder container = Content.newBuilder();
// if (message.getPreKeyBundle().isPresent()) {
// PreKeyBundle preKeyBundle = message.getPreKeyBundle().get();
// PreKeyBundleMessage.Builder preKeyBundleMessageBuilder = PreKeyBundleMessage.newBuilder()
// .setDeviceId(preKeyBundle.getDeviceId())
// .setIdentityKey(ByteString.copyFrom(preKeyBundle.getIdentityKey().serialize()))
// .setPreKeyId(preKeyBundle.getPreKeyId())
// .setPreKey(ByteString.copyFrom(preKeyBundle.getPreKey().serialize()))
// .setSignedKeyId(preKeyBundle.getSignedPreKeyId())
// .setSignedKey(ByteString.copyFrom(preKeyBundle.getSignedPreKey().serialize()))
// .setSignature(ByteString.copyFrom(preKeyBundle.getSignedPreKeySignature()))
// .setIdentityKey(ByteString.copyFrom(preKeyBundle.getIdentityKey().serialize()));
// container.setPreKeyBundleMessage(preKeyBundleMessageBuilder);
// }
// if (message.getDeviceLink().isPresent()) {
// DeviceLink deviceLink = message.getDeviceLink().get();
// SignalServiceProtos.DeviceLinkMessage.Builder deviceLinkMessageBuilder = SignalServiceProtos.DeviceLinkMessage.newBuilder()
// .setPrimaryPublicKey(deviceLink.getMasterPublicKey())
// .setSecondaryPublicKey(deviceLink.getSlavePublicKey())
// .setRequestSignature(ByteString.copyFrom(Objects.requireNonNull(deviceLink.getRequestSignature())));
// if (deviceLink.getAuthorizationSignature() != null) {
// deviceLinkMessageBuilder.setAuthorizationSignature(ByteString.copyFrom(deviceLink.getAuthorizationSignature()));
// }
// container.setDeviceLinkMessage(deviceLinkMessageBuilder.build());
// }
DataMessage.Builder builder = DataMessage.newBuilder(); DataMessage.Builder builder = DataMessage.newBuilder();
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments(), recipient); List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments(), recipient);
@ -559,6 +474,10 @@ public class SignalServiceMessageSender {
builder.setProfileKey(ByteString.copyFrom(message.getProfileKey().get())); builder.setProfileKey(ByteString.copyFrom(message.getProfileKey().get()));
} }
if (message.getSyncTarget().isPresent()) {
builder.setSyncTarget(message.getSyncTarget().get());
}
if (message.getQuote().isPresent()) { if (message.getQuote().isPresent()) {
DataMessage.Quote.Builder quoteBuilder = DataMessage.Quote.newBuilder() DataMessage.Quote.Builder quoteBuilder = DataMessage.Quote.newBuilder()
.setId(message.getQuote().get().getId()) .setId(message.getQuote().get().getId())
@ -636,40 +555,6 @@ public class SignalServiceMessageSender {
return container.build().toByteArray(); return container.build().toByteArray();
} }
private byte[] createCallContent(SignalServiceCallMessage callMessage) {
Content.Builder container = Content.newBuilder();
CallMessage.Builder builder = CallMessage.newBuilder();
if (callMessage.getOfferMessage().isPresent()) {
OfferMessage offer = callMessage.getOfferMessage().get();
builder.setOffer(CallMessage.Offer.newBuilder()
.setId(offer.getId())
.setDescription(offer.getDescription()));
} else if (callMessage.getAnswerMessage().isPresent()) {
AnswerMessage answer = callMessage.getAnswerMessage().get();
builder.setAnswer(CallMessage.Answer.newBuilder()
.setId(answer.getId())
.setDescription(answer.getDescription()));
} else if (callMessage.getIceUpdateMessages().isPresent()) {
List<IceUpdateMessage> updates = callMessage.getIceUpdateMessages().get();
for (IceUpdateMessage update : updates) {
builder.addIceUpdate(CallMessage.IceUpdate.newBuilder()
.setId(update.getId())
.setSdp(update.getSdp())
.setSdpMid(update.getSdpMid())
.setSdpMLineIndex(update.getSdpMLineIndex()));
}
} else if (callMessage.getHangupMessage().isPresent()) {
builder.setHangup(CallMessage.Hangup.newBuilder().setId(callMessage.getHangupMessage().get().getId()));
} else if (callMessage.getBusyMessage().isPresent()) {
builder.setBusy(CallMessage.Busy.newBuilder().setId(callMessage.getBusyMessage().get().getId()));
}
container.setCallMessage(builder);
return container.build().toByteArray();
}
private byte[] createMultiDeviceContactsContent(SignalServiceAttachmentStream contacts, boolean complete) private byte[] createMultiDeviceContactsContent(SignalServiceAttachmentStream contacts, boolean complete)
throws IOException throws IOException
{ {
@ -987,6 +872,7 @@ public class SignalServiceMessageSender {
throws IOException throws IOException
{ {
List<SendMessageResult> results = new LinkedList<>(); List<SendMessageResult> results = new LinkedList<>();
SignalServiceAddress ownAddress = localAddress;
Iterator<SignalServiceAddress> recipientIterator = recipients.iterator(); Iterator<SignalServiceAddress> recipientIterator = recipients.iterator();
Iterator<Optional<UnidentifiedAccess>> unidentifiedAccessIterator = unidentifiedAccess.iterator(); Iterator<Optional<UnidentifiedAccess>> unidentifiedAccessIterator = unidentifiedAccess.iterator();
@ -995,7 +881,7 @@ public class SignalServiceMessageSender {
try { try {
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(content, recipient.getNumber(), store); boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(content, recipient.getNumber(), store);
SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, false, useFallbackEncryption, isClosedGroup, false, notifyPNServer); SendMessageResult result = sendMessage(messageID, recipient, unidentifiedAccessIterator.next(), timestamp, content, online, ttl, false, useFallbackEncryption, isClosedGroup, notifyPNServer, Optional.absent());
results.add(result); results.add(result);
} catch (UnregisteredUserException e) { } catch (UnregisteredUserException e) {
Log.w(TAG, e); Log.w(TAG, e);
@ -1009,41 +895,46 @@ public class SignalServiceMessageSender {
return results; return results;
} }
private SendMessageResult sendMessage(SignalServiceAddress recipient, private SendMessageResult sendMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess, Optional<UnidentifiedAccess> unidentifiedAccess,
long timestamp, long timestamp,
byte[] content, byte[] content,
boolean online, boolean online,
int ttl, int ttl,
boolean useFallbackEncryption, boolean useFallbackEncryption)
boolean isSyncMessage)
throws IOException throws IOException
{ {
// Loki - This method is only invoked for various types of control messages // Loki - This method is only invoked for various types of control messages
return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, false, useFallbackEncryption, isSyncMessage, false); return sendMessage(0, recipient, unidentifiedAccess, timestamp, content, online, ttl, false, false, useFallbackEncryption, false,Optional.absent());
} }
public SendMessageResult sendMessage(final long messageID, public SendMessageResult sendMessage(final long messageID,
final SignalServiceAddress recipient, final SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess, Optional<UnidentifiedAccess> unidentifiedAccess,
long timestamp, long timestamp,
byte[] content, byte[] content,
boolean online, boolean online,
int ttl, int ttl,
boolean isDeviceLinkMessage, boolean isDeviceLinkMessage,
boolean useFallbackEncryption, boolean useFallbackEncryption,
boolean isClosedGroup, boolean isClosedGroup,
boolean isSyncMessage, boolean notifyPNServer,
boolean notifyPNServer) Optional<String> syncTarget)
throws IOException throws IOException
{ {
long threadID = threadDatabase.getThreadID(recipient.getNumber()); boolean isSelfSend = syncTarget.isPresent() && !syncTarget.get().isEmpty();
long threadID;
if (isSelfSend) {
threadID = threadDatabase.getThreadID(syncTarget.get());
} else {
threadID = threadDatabase.getThreadID(recipient.getNumber());
}
PublicChat publicChat = threadDatabase.getPublicChat(threadID); PublicChat publicChat = threadDatabase.getPublicChat(threadID);
try { try {
if (publicChat != null) { if (publicChat != null) {
return sendMessageToPublicChat(messageID, recipient, timestamp, content, publicChat); return sendMessageToPublicChat(messageID, recipient, timestamp, content, publicChat);
} else { } else {
return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer); return sendMessageToPrivateChat(messageID, recipient, unidentifiedAccess, timestamp, content, online, ttl, useFallbackEncryption, isClosedGroup, notifyPNServer, syncTarget);
} }
} catch (PushNetworkException e) { } catch (PushNetworkException e) {
return SendMessageResult.networkFailure(recipient); return SendMessageResult.networkFailure(recipient);
@ -1152,10 +1043,10 @@ public class SignalServiceMessageSender {
int ttl, int ttl,
boolean useFallbackEncryption, boolean useFallbackEncryption,
boolean isClosedGroup, boolean isClosedGroup,
final boolean notifyPNServer) final boolean notifyPNServer,
Optional<String> syncTarget)
throws IOException, UntrustedIdentityException throws IOException, UntrustedIdentityException
{ {
if (recipient.getNumber().equals(userPublicKey)) { return SendMessageResult.success(recipient, false, false); }
final SettableFuture<?>[] future = { new SettableFuture<Unit>() }; final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content); OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content);
// Loki - Remove this when we have shared sender keys // Loki - Remove this when we have shared sender keys
@ -1221,14 +1112,10 @@ public class SignalServiceMessageSender {
} }
return Unit.INSTANCE; return Unit.INSTANCE;
} }
}).fail(new Function1<Exception, Unit>() { }).fail(exception -> {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
@Override f.setException(exception);
public Unit invoke(Exception exception) { return Unit.INSTANCE;
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
f.setException(exception);
return Unit.INSTANCE;
}
}); });
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0]; @SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
@ -1304,12 +1191,6 @@ public class SignalServiceMessageSender {
return builder.build(); return builder.build();
} }
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment)
throws IOException
{
return createAttachmentPointer(attachment, false, null);
}
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, SignalServiceAddress recipient) private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, SignalServiceAddress recipient)
throws IOException throws IOException
{ {

View File

@ -216,7 +216,18 @@ public class SignalServiceCipher {
); );
} }
if (message.hasDeviceLinkMessage()) { if (message.hasConfigurationMessage()) {
SignalServiceCipher.Metadata metadata = plaintext.getMetadata();
SignalServiceContent content = new SignalServiceContent(message, metadata.getSender(), metadata.getSenderDevice(), metadata.getTimestamp());
if (message.hasDataMessage()) {
setProfile(message.getDataMessage(), content);
SignalServiceDataMessage signalServiceDataMessage = createSignalServiceMessage(metadata, message.getDataMessage());
content.setDataMessage(signalServiceDataMessage);
}
return content;
} else if (message.hasDeviceLinkMessage()) {
SignalServiceProtos.DeviceLinkMessage protoDeviceLinkMessage = message.getDeviceLinkMessage(); SignalServiceProtos.DeviceLinkMessage protoDeviceLinkMessage = message.getDeviceLinkMessage();
String masterPublicKey = protoDeviceLinkMessage.getPrimaryPublicKey(); String masterPublicKey = protoDeviceLinkMessage.getPrimaryPublicKey();
String slavePublicKey = protoDeviceLinkMessage.getSecondaryPublicKey(); String slavePublicKey = protoDeviceLinkMessage.getSecondaryPublicKey();
@ -331,7 +342,6 @@ public class SignalServiceCipher {
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = SessionProtocolUtilities.INSTANCE.decryptClosedGroupCiphertext(ciphertext, groupPublicKey, apiDB, sessionProtocolImpl); kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = SessionProtocolUtilities.INSTANCE.decryptClosedGroupCiphertext(ciphertext, groupPublicKey, apiDB, sessionProtocolImpl);
paddedMessage = plaintextAndSenderPublicKey.getFirst(); paddedMessage = plaintextAndSenderPublicKey.getFirst();
String senderPublicKey = plaintextAndSenderPublicKey.getSecond(); String senderPublicKey = plaintextAndSenderPublicKey.getSecond();
if (senderPublicKey.equals(localAddress.getNumber())) { throw new SelfSendException(); } // Will be caught and ignored in PushDecryptJob
metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false); metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false);
sessionVersion = sessionCipher.getSessionVersion(); sessionVersion = sessionCipher.getSessionVersion();
} else if (envelope.isPreKeySignalMessage()) { } else if (envelope.isPreKeySignalMessage()) {
@ -389,6 +399,7 @@ public class SignalServiceCipher {
ClosedGroupUpdate closedGroupUpdate = content.getClosedGroupUpdate(); ClosedGroupUpdate closedGroupUpdate = content.getClosedGroupUpdate();
ClosedGroupUpdateV2 closedGroupUpdateV2 = content.getClosedGroupUpdateV2(); ClosedGroupUpdateV2 closedGroupUpdateV2 = content.getClosedGroupUpdateV2();
boolean isDeviceUnlinkingRequest = ((content.getFlags() & DataMessage.Flags.DEVICE_UNLINKING_REQUEST_VALUE) != 0); boolean isDeviceUnlinkingRequest = ((content.getFlags() & DataMessage.Flags.DEVICE_UNLINKING_REQUEST_VALUE) != 0);
String syncTarget = content.getSyncTarget();
for (AttachmentPointer pointer : content.getAttachmentsList()) { for (AttachmentPointer pointer : content.getAttachmentsList()) {
attachments.add(createAttachmentPointer(pointer)); attachments.add(createAttachmentPointer(pointer));
@ -417,7 +428,8 @@ public class SignalServiceCipher {
null, null,
closedGroupUpdate, closedGroupUpdate,
closedGroupUpdateV2, closedGroupUpdateV2,
isDeviceUnlinkingRequest); isDeviceUnlinkingRequest,
syncTarget);
} }
private SignalServiceSyncMessage createSynchronizeMessage(Metadata metadata, SyncMessage content) private SignalServiceSyncMessage createSynchronizeMessage(Metadata metadata, SyncMessage content)

View File

@ -12,7 +12,9 @@ import org.session.libsignal.service.api.messages.SignalServiceNullMessage;
import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage; import org.session.libsignal.service.api.messages.SignalServiceReceiptMessage;
import org.session.libsignal.service.api.messages.SignalServiceTypingMessage; import org.session.libsignal.service.api.messages.SignalServiceTypingMessage;
import org.session.libsignal.service.api.messages.calls.SignalServiceCallMessage; import org.session.libsignal.service.api.messages.calls.SignalServiceCallMessage;
import org.session.libsignal.service.api.messages.multidevice.ConfigurationMessage;
import org.session.libsignal.service.api.messages.multidevice.SignalServiceSyncMessage; import org.session.libsignal.service.api.messages.multidevice.SignalServiceSyncMessage;
import org.session.libsignal.service.internal.push.SignalServiceProtos;
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink; import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink;
import org.session.libsignal.service.loki.protocol.sessionmanagement.PreKeyBundleMessage; import org.session.libsignal.service.loki.protocol.sessionmanagement.PreKeyBundleMessage;
@ -33,10 +35,11 @@ public class SignalServiceContent {
private final Optional<SignalServiceTypingMessage> typingMessage; private final Optional<SignalServiceTypingMessage> typingMessage;
// Loki // Loki
private final Optional<DeviceLink> deviceLink; private final Optional<DeviceLink> deviceLink;
public Optional<PreKeyBundleMessage> preKeyBundleMessage = Optional.absent(); public Optional<SignalServiceProtos.Content> configurationMessageProto = Optional.absent();
public Optional<String> senderDisplayName = Optional.absent(); public Optional<PreKeyBundleMessage> preKeyBundleMessage = Optional.absent();
public Optional<String> senderProfilePictureURL = Optional.absent(); public Optional<String> senderDisplayName = Optional.absent();
public Optional<String> senderProfilePictureURL = Optional.absent();
public SignalServiceContent(SignalServiceDataMessage message, String sender, int senderDevice, long timestamp, boolean needsReceipt, boolean isDeviceUnlinkingRequest) { public SignalServiceContent(SignalServiceDataMessage message, String sender, int senderDevice, long timestamp, boolean needsReceipt, boolean isDeviceUnlinkingRequest) {
this.sender = sender; this.sender = sender;
@ -128,6 +131,22 @@ public class SignalServiceContent {
this.isDeviceUnlinkingRequest = false; this.isDeviceUnlinkingRequest = false;
} }
public SignalServiceContent(SignalServiceProtos.Content configurationMessageProto, String sender, int senderDevice, long timestamp) {
this.sender = sender;
this.senderDevice = senderDevice;
this.timestamp = timestamp;
this.needsReceipt = false;
this.message = Optional.absent();
this.synchronizeMessage = Optional.absent();
this.callMessage = Optional.absent();
this.nullMessage = Optional.absent();
this.readMessage = Optional.absent();
this.typingMessage = Optional.absent();
this.deviceLink = Optional.absent();
this.configurationMessageProto = Optional.fromNullable(configurationMessageProto);
this.isDeviceUnlinkingRequest = false;
}
public SignalServiceContent(SignalServiceNullMessage nullMessage, String sender, int senderDevice, long timestamp) { public SignalServiceContent(SignalServiceNullMessage nullMessage, String sender, int senderDevice, long timestamp) {
this.sender = sender; this.sender = sender;
this.senderDevice = senderDevice; this.senderDevice = senderDevice;

View File

@ -41,6 +41,7 @@ public class SignalServiceDataMessage {
private final Optional<ClosedGroupUpdate> closedGroupUpdate; private final Optional<ClosedGroupUpdate> closedGroupUpdate;
private final Optional<ClosedGroupUpdateV2> closedGroupUpdateV2; private final Optional<ClosedGroupUpdateV2> closedGroupUpdateV2;
private final boolean isDeviceUnlinkingRequest; private final boolean isDeviceUnlinkingRequest;
private final Optional<String> syncTarget;
/** /**
* Construct a SignalServiceDataMessage with a body and no attachments. * Construct a SignalServiceDataMessage with a body and no attachments.
@ -134,7 +135,7 @@ public class SignalServiceDataMessage {
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews, Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
Sticker sticker) Sticker sticker)
{ {
this(timestamp, group, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, null, null, null, null, false); this(timestamp, group, attachments, body, endSession, expiresInSeconds, expirationUpdate, profileKey, profileKeyUpdate, quote, sharedContacts, previews, sticker, null, null, null, null, false, null);
} }
/** /**
@ -155,7 +156,7 @@ public class SignalServiceDataMessage {
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews, Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
Sticker sticker, PreKeyBundle preKeyBundle, DeviceLink deviceLink, Sticker sticker, PreKeyBundle preKeyBundle, DeviceLink deviceLink,
ClosedGroupUpdate closedGroupUpdate, ClosedGroupUpdateV2 closedGroupUpdateV2, ClosedGroupUpdate closedGroupUpdate, ClosedGroupUpdateV2 closedGroupUpdateV2,
boolean isDeviceUnlinkingRequest) boolean isDeviceUnlinkingRequest, String syncTarget)
{ {
this.timestamp = timestamp; this.timestamp = timestamp;
this.body = Optional.fromNullable(body); this.body = Optional.fromNullable(body);
@ -172,6 +173,7 @@ public class SignalServiceDataMessage {
this.closedGroupUpdate = Optional.fromNullable(closedGroupUpdate); this.closedGroupUpdate = Optional.fromNullable(closedGroupUpdate);
this.closedGroupUpdateV2 = Optional.fromNullable(closedGroupUpdateV2); this.closedGroupUpdateV2 = Optional.fromNullable(closedGroupUpdateV2);
this.isDeviceUnlinkingRequest = isDeviceUnlinkingRequest; this.isDeviceUnlinkingRequest = isDeviceUnlinkingRequest;
this.syncTarget = Optional.fromNullable(syncTarget);
if (attachments != null && !attachments.isEmpty()) { if (attachments != null && !attachments.isEmpty()) {
this.attachments = Optional.of(attachments); this.attachments = Optional.of(attachments);
@ -250,6 +252,10 @@ public class SignalServiceDataMessage {
return profileKey; return profileKey;
} }
public Optional<String> getSyncTarget() {
return syncTarget;
}
public Optional<Quote> getQuote() { public Optional<Quote> getQuote() {
return quote; return quote;
} }
@ -307,6 +313,7 @@ public class SignalServiceDataMessage {
private Sticker sticker; private Sticker sticker;
private PreKeyBundle preKeyBundle; private PreKeyBundle preKeyBundle;
private DeviceLink deviceLink; private DeviceLink deviceLink;
private String syncTarget;
private boolean isDeviceUnlinkingRequest; private boolean isDeviceUnlinkingRequest;
private Builder() {} private Builder() {}
@ -336,6 +343,11 @@ public class SignalServiceDataMessage {
return this; return this;
} }
public Builder withSyncTarget(String syncTarget) {
this.syncTarget = syncTarget;
return this;
}
public Builder asEndSessionMessage() { public Builder asEndSessionMessage() {
return asEndSessionMessage(true); return asEndSessionMessage(true);
} }
@ -417,7 +429,7 @@ public class SignalServiceDataMessage {
profileKeyUpdate, quote, sharedContacts, previews, profileKeyUpdate, quote, sharedContacts, previews,
sticker, preKeyBundle, deviceLink, sticker, preKeyBundle, deviceLink,
null, null, null, null,
isDeviceUnlinkingRequest); isDeviceUnlinkingRequest, syncTarget);
} }
} }

View File

@ -65,19 +65,20 @@ object DownloadUtilities {
Log.d("Loki", "Attachment size limit exceeded.") Log.d("Loki", "Attachment size limit exceeded.")
throw PushNetworkException("Max response size exceeded.") throw PushNetworkException("Max response size exceeded.")
} }
val input = body.inputStream() body.inputStream().use { input ->
val buffer = ByteArray(32768) val buffer = ByteArray(32768)
var count = 0 var count = 0
var bytes = input.read(buffer) var bytes = input.read(buffer)
while (bytes >= 0) { while (bytes >= 0) {
outputStream.write(buffer, 0, bytes) outputStream.write(buffer, 0, bytes)
count += bytes count += bytes
if (count > maxSize) { if (count > maxSize) {
Log.d("Loki", "Attachment size limit exceeded.") Log.d("Loki", "Attachment size limit exceeded.")
throw PushNetworkException("Max response size exceeded.") throw PushNetworkException("Max response size exceeded.")
}
listener?.onAttachmentProgress(body.size.toLong(), count.toLong())
bytes = input.read(buffer)
} }
listener?.onAttachmentProgress(body.size.toLong(), count.toLong())
bytes = input.read(buffer)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.d("Loki", "Couldn't download attachment due to error: $e.") Log.d("Loki", "Couldn't download attachment due to error: $e.")