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'
}
def canonicalVersionCode = 121
def canonicalVersionName = "1.6.4"
def canonicalVersionCode = 135
def canonicalVersionName = "1.6.12"
def postFixSize = 10
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.SharedSenderKeysDatabase;
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.utilities.Broadcaster;
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.SessionManagementProtocolDelegate;
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 java.io.File;
@ -206,7 +206,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
SessionMetaProtocol.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);
setUpP2PAPIIfNeeded();
PushNotificationAPI.Companion.configureIfNeeded(BuildConfig.DEBUG);
@ -249,6 +249,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
startPollingIfNeeded();
publicChatManager.markAllAsNotCaughtUp();
publicChatManager.startPollersIfNeeded();
MultiDeviceProtocol.syncConfigurationIfNeeded(this);
}
@Override

View File

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

View File

@ -150,6 +150,17 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
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) {
List<Address> members = getCurrentMembers(groupId);
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)
{
Collections.sort(members);
@ -211,7 +222,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
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.setName(title);
@ -220,6 +231,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
});
notifyConversationListListeners();
return threadId;
}
public boolean delete(@NonNull String groupId) {

View File

@ -902,6 +902,17 @@ public class MmsDatabase extends MessagingDatabase {
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)
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);
try (Cursor cursor = queryTables(PROJECTION, MmsSmsColumns.NORMALIZED_DATE_SENT + " = " + timestamp, null, null)) {
@ -96,8 +96,10 @@ public class MmsSmsDatabase extends Database {
MessageRecord messageRecord;
while ((messageRecord = reader.getNext()) != null) {
if ((Util.isOwnNumber(context, author.serialize()) && messageRecord.isOutgoing()) ||
(!Util.isOwnNumber(context, author.serialize()) && messageRecord.getIndividualRecipient().getAddress().equals(author)))
if ((Util.isOwnNumber(context, serializedAuthor) && messageRecord.isOutgoing()) ||
(!Util.isOwnNumber(context, serializedAuthor)
&& messageRecord.getIndividualRecipient().getAddress().serialize().equals(serializedAuthor)
))
{
return messageRecord;
}
@ -107,6 +109,10 @@ public class MmsSmsDatabase extends Database {
return null;
}
public @Nullable MessageRecord getMessageFor(long timestamp, Address author) {
return getMessageFor(timestamp, author.serialize());
}
public Cursor getConversation(long threadId, long offset, long limit) {
String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC";
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);
}
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,
boolean forceSms, long date, InsertListener insertListener)
{
@ -718,7 +727,6 @@ public class SmsDatabase extends MessagingDatabase {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long messageId = db.insert(TABLE_NAME, ADDRESS, contentValues);
if (insertListener != null) {
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.JobQueue
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.VisibleMessage
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.SignalServiceGroup
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.database.helpers.SQLCipherOpenHelper
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.getString
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
@ -351,7 +352,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
.setName(name)
.addAllMembers(members)
.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 infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
mmsDB.markAsSent(infoMessageID, true)
@ -371,6 +372,38 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
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) {
val recipient = Recipient.from(context, address, false)
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);
}
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);
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.MmsDatabase;
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.PushGroupUpdateJob;
import org.session.libsignal.utilities.logging.Log;
@ -123,11 +125,19 @@ public class GroupMessageProcessor {
{
GroupDatabase database = DatabaseFactory.getGroupDatabase(context);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
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);
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) {
// Loki - Only update the group if the group admin sent the message
String masterDevice = MultiDeviceProtocol.shared.getMasterDevice(content.getSender());
@ -260,9 +270,16 @@ public class GroupMessageProcessor {
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
Address address = Address.Companion.fromExternal(context, GroupUtil.getEncodedId(group));
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 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);

View File

@ -47,7 +47,6 @@ import org.session.libsession.utilities.TextSecurePreferences;
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
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.protocol.ClosedGroupsProtocol;
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.SessionMetaProtocol;
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.IncomingTextMessage;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.session.libsignal.utilities.Hex;
import org.session.libsignal.libsignal.InvalidMessageException;
@ -264,8 +263,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content);
if (content.getDeviceLink().isPresent()) {
throw new UnsupportedOperationException("Device link operations are not supported!");
if (content.configurationMessageProto.isPresent()) {
MultiDeviceProtocol.handleConfigurationMessage(context, content.configurationMessageProto.get(), content.getSender(), content.getTimestamp());
} else if (content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
@ -277,7 +276,6 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (message.getClosedGroupUpdateV2().isPresent()) {
ClosedGroupsProtocolV2.handleMessage(context, message.getClosedGroupUpdateV2().get(), message.getTimestamp(), envelope.getSource(), content.getSender());
}
if (message.isEndSession()) {
handleEndSessionMessage(content, smsMessageId);
} 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,
@NonNull SignalServiceDataMessage message,
@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,
@NonNull SignalServiceDataMessage message,
@NonNull Optional<Long> smsMessageId,
@NonNull Optional<Long> messageServerIDOrNull)
throws StorageFailedException
{
Recipient originalRecipient = getMessageDestination(content, message);
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
Recipient originalRecipient = getMessageDestination(content, message);
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
String syncTarget = message.getSyncTarget().orNull();
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
Address masterAddress = masterRecipient.getAddress();
@ -582,75 +478,140 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
masterAddress = getMessageMasterDestination(content.getSender()).getAddress();
}
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(masterAddress, message.getTimestamp(), -1,
message.getExpiresInSeconds() * 1000L, false, content.isNeedsReceipt(), message.getBody(), message.getGroupInfo(), message.getAttachments(),
quote, sharedContacts, linkPreviews, sticker);
// Handle sync message from ourselves
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);
}
List<Attachment> attachments = PointerAttachment.forPointers(message.getAttachments());
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.beginTransaction();
OutgoingMediaMessage mediaMessage = new OutgoingMediaMessage(masterRecipient, message.getBody().orNull(),
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 (mediaMessage.getBody().isEmpty() && mediaMessage.getAttachments().isEmpty() && mediaMessage.getLinkPreviews().isEmpty()) {
return;
}
if (DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(message.getTimestamp(), targetAddress) != null) {
Log.d("Loki","Message already exists, don't insert again");
return;
}
Optional<InsertResult> insertResult;
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
database.beginTransaction();
try {
if (message.isGroupMessage()) {
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1, content.getTimestamp());
} else {
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
// 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 {
// 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()) {
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()) {
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);
messageNotifier.updateNotification(context, insertResult.get().getThreadId());
}
// 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);
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 (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() : "";
Recipient originalRecipient = getMessageDestination(content, message);
Recipient masterRecipient = getMessageMasterDestination(content.getSender());
String syncTarget = message.getSyncTarget().orNull();
if (message.getExpiresInSeconds() != originalRecipient.getExpireMessages()) {
handleExpirationUpdate(content, message, Optional.absent());
@ -778,15 +740,56 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
if (smsMessageId.isPresent() && !message.getGroupInfo().isPresent()) {
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 {
notifyTypingStoppedFromIncomingMessage(masterRecipient, content.getSender(), content.getSenderDevice());
Address masterAddress = masterRecipient.getAddress();
if (message.isGroupMessage()) {
masterAddress = getMessageMasterDestination(content.getSender()).getAddress();
}
IncomingTextMessage tm = new IncomingTextMessage(masterAddress,
content.getSenderDevice(),
message.getTimestamp(), body,
@ -1319,6 +1322,12 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
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);
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.utilities.GroupUtil;
import org.session.libsession.utilities.TextSecurePreferences;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -143,6 +144,9 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
List<NetworkFailure> existingNetworkFailures = message.getNetworkFailures();
List<IdentityKeyMismatch> existingIdentityMismatches = message.getIdentityKeyMismatches();
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
if (database.isSent(messageId)) {
log(TAG, "Message " + messageId + " was already sent. Ignoring.");
return;
@ -238,25 +242,18 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
// 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<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(addresses)
.map(a -> Address.Companion.fromSerialized(a.getNumber()))
.map(a -> Recipient.from(context, a, false))
.map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient))
.toList();
SignalServiceGroup.GroupType groupType = address.isOpenGroup() ? SignalServiceGroup.GroupType.PUBLIC_CHAT : SignalServiceGroup.GroupType.SIGNAL;
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
OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
GroupContext groupContext = groupMessage.getGroupContext();
@ -271,25 +268,40 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType {
return messageSender.sendMessage(messageId, addresses, unidentifiedAccess, groupDataMessage);
} else {
SignalServiceGroup group = new SignalServiceGroup(GroupUtil.getDecodedGroupIDAsData(groupId), groupType);
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();
SignalServiceDataMessage groupMessage = getDataMessage(address, message).build();
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> {
@Override
public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull Data data) {

View File

@ -245,7 +245,9 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
{
try {
Recipient recipient = Recipient.from(context, destination, false);
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
SignalServiceAddress address = getPushAddress(recipient.getAddress());
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
List<SignalServiceAttachment> serviceAttachments = getAttachmentPointersFor(attachments);
Optional<byte[]> profileKey = getProfileKey(message.getRecipient());
@ -254,6 +256,8 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
List<SharedContact> sharedContacts = getSharedContactsFor(message);
List<Preview> previews = getPreviewsFor(message);
Optional<UnidentifiedAccessPair> unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(context, recipient);
SignalServiceDataMessage mediaMessage = SignalServiceDataMessage.newBuilder()
.withBody(message.getBody())
.withAttachments(serviceAttachments)
@ -267,20 +271,48 @@ public class PushMediaSendJob extends PushSendJob implements InjectableType {
.asExpirationUpdate(message.isExpirationUpdate())
.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())) {
// Loki - Device link messages don't go through here
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
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);
SendMessageResult result = messageSender.sendMessage(messageId, address, unidentifiedAccessPair, mediaMessage);
if (result.getLokiAPIError() != null) {
throw result.getLokiAPIError();
} else {
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) {
warn(TAG, e);

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
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.libsession.messaging.threads.Address;
@ -192,8 +193,10 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
throws UntrustedIdentityException, InsecureFallbackApprovalException, RetryLaterException, SnodeAPI.Error
{
try {
String userPublicKey = TextSecurePreferences.getLocalNumber(context);
Recipient recipient = Recipient.from(context, destination, false);
SignalServiceAddress address = getPushAddress(recipient.getAddress());
SignalServiceAddress localAddress = new SignalServiceAddress(userPublicKey);
Optional<byte[]> profileKey = getProfileKey(recipient);
Optional<UnidentifiedAccessPair> unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient);
@ -205,28 +208,49 @@ public class PushTextSendJob extends PushSendJob implements InjectableType {
// }
SignalServiceDataMessage textSecureMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getDateSent())
.withBody(message.getBody())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
// .withPreKeyBundle(preKeyBundle)
.asEndSessionMessage(message.isEndSession())
.build();
.withTimestamp(message.getDateSent())
.withBody(message.getBody())
.withExpiration((int)(message.getExpiresIn() / 1000))
.withProfileKey(profileKey.orNull())
.asEndSessionMessage(message.isEndSession())
.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())) {
// 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);
if (result.getLokiAPIError() != null) {
throw result.getLokiAPIError();
} else {
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) {
warn(TAG, "Failure", e);

View File

@ -2,8 +2,6 @@ package org.thoughtcrime.securesms.loki.activities
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.AsyncTask
import android.os.Bundle
import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader
@ -20,15 +18,13 @@ import org.thoughtcrime.securesms.conversation.ConversationActivity
import org.session.libsession.messaging.threads.Address
import org.thoughtcrime.securesms.database.DatabaseFactory
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.fadeOut
import org.thoughtcrime.securesms.mms.GlideApp
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.libsignal.util.guava.Optional
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
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))
finish()
}
}
}
// endregion

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.loki.protocol
import com.google.protobuf.ByteString
import org.session.libsession.messaging.jobs.Data
import org.session.libsignal.libsignal.util.guava.Optional
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
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
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedContentMessage, false, ttl, false,
useFallbackEncryption, false, false, false)
useFallbackEncryption, false, false, Optional.absent())
} catch (e: Exception) {
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.DjbECPublicKey
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.internal.push.SignalServiceProtos
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.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 {
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)
.setQueue(KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(20)
.build(),
destination,
kind)
kind,
sentTime)
override fun getFactoryKey(): String { return KEY }
override fun serialize(): Data {
val builder = Data.Builder()
builder.putString("destination", destination)
builder.putLong("sentTime", sentTime)
when (kind) {
is Kind.New -> {
builder.putString("kind", "New")
@ -123,6 +126,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJobV2 {
val destination = data.getString("destination")
val rawKind = data.getString("kind")
val sentTime = data.getLong("sentTime")
val kind: Kind
when (rawKind) {
"New" -> {
@ -161,7 +165,7 @@ class ClosedGroupUpdateMessageSendJobV2 private constructor(parameters: Paramete
}
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 {
// isClosedGroup can always be false as it's only used in the context of legacy closed groups
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedContentMessage, false, ttl, false,
true, false, false, false)
sentTime, serializedContentMessage, false, ttl, false,
true, false, false, Optional.absent())
} catch (e: Exception) {
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)
.addAllMembers(members)
.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 infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null)
mmsDB.markAsSent(infoMessageID, true)
@ -324,6 +324,6 @@ object ClosedGroupsProtocol {
.setId(decodedGroupId)
.setType(GroupContext.Type.QUIT)
.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.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.jvm.Throws
object ClosedGroupsProtocolV2 {
const val groupSizeLimit = 100
private val pendingKeyPair = ConcurrentHashMap<String,Optional<ECKeyPair>>()
sealed class Error(val description: String) : Exception() {
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.")
@ -58,6 +61,7 @@ object ClosedGroupsProtocolV2 {
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
// Generate the group's public key
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
val encryptionKeyPair = Curve.generateKeyPair()
// Create the group
@ -68,20 +72,20 @@ object ClosedGroupsProtocolV2 {
null, null, LinkedList(admins.map { Address.fromSerialized(it!!) }))
DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true)
// 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
apiDB.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user
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
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Fulfill the promise
@ -103,21 +107,22 @@ object ClosedGroupsProtocolV2 {
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
val admins = group.admins.map { it.serialize() }
val name = group.title
val sentTime = System.currentTimeMillis()
if (group == null) {
Log.d("Loki", "Can't leave nonexistent closed group.")
return@queue deferred.reject(Error.NoThread)
}
// Send the update to the group
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.Leave, sentTime)
job.setContext(context)
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
val infoType = GroupContext.Type.QUIT
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)
}
return deferred.promise
@ -141,7 +146,10 @@ object ClosedGroupsProtocolV2 {
val newMembersAsData = membersToAdd.map { Hex.fromStringCondensed(it) }
val admins = group.admins.map { it.serialize() }
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) {
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
return@task Error.NoKeyPair
@ -149,21 +157,21 @@ object ClosedGroupsProtocolV2 {
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.AddMembers(newMembersAsData)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
job.setContext(context)
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
for (member in membersToAdd) {
@Suppress("NAME_SHADOWING")
val closedGroupNewKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING")
val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind)
val newMemberJob = ClosedGroupUpdateMessageSendJobV2(member, closedGroupNewKind, sentTime)
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) })
val removeMembersAsData = membersToRemove.map { Hex.fromStringCondensed(it) }
val admins = group.admins.map { it.serialize() }
val sentTime = System.currentTimeMillis()
val encryptionKeyPair = apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) {
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
@ -196,17 +205,18 @@ object ClosedGroupsProtocolV2 {
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.RemoveMembers(removeMembersAsData)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, memberUpdateKind, sentTime)
job.setContext(context)
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)
if (isCurrentUserAdmin) {
generateAndSendNewEncryptionKeyPair(context, groupPublicKey, updatedMembers)
}
// 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)
return@task Unit
}
}
@ -219,21 +229,22 @@ object ClosedGroupsProtocolV2 {
val group = groupDB.getGroup(groupID).orNull()
val members = group.members.map { it.serialize() }.toSet()
val admins = group.admins.map { it.serialize() }
val sentTime = System.currentTimeMillis()
if (group == null) {
Log.d("Loki", "Can't leave nonexistent closed group.")
return@queue deferred.reject(Error.NoThread)
}
// Send the update to the group
val kind = ClosedGroupUpdateMessageSendJobV2.Kind.NameChange(newName)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, kind, sentTime)
job.setContext(context)
job.onRun() // Run the job immediately
// Update the group
groupDB.updateTitle(groupID, newName)
// 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, newName, members, admins, threadID)
insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
// Update the group
groupDB.updateTitle(groupID, newName)
deferred.resolve(Unit)
}
return deferred.promise
@ -273,6 +284,7 @@ object ClosedGroupsProtocolV2 {
Log.d("Loki", "Can't update nonexistent closed group.")
return@queue deferred.reject(Error.NoThread)
}
val sentTime = System.currentTimeMillis()
val oldMembers = group.members.map { it.serialize() }.toSet()
val newMembers = members.minus(oldMembers)
val membersAsData = members.map { Hex.fromStringCondensed(it) }
@ -299,7 +311,7 @@ object ClosedGroupsProtocolV2 {
@Suppress("NAME_SHADOWING")
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.Update(name, membersAsData)
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, closedGroupUpdateKind, sentTime)
job.setContext(context)
job.onRun() // Run the job immediately
if (isUserLeaving) {
@ -323,7 +335,7 @@ object ClosedGroupsProtocolV2 {
@Suppress("NAME_SHADOWING")
val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJobV2.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, encryptionKeyPair, membersAsData, adminsAsData)
@Suppress("NAME_SHADOWING")
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind)
val job = ClosedGroupUpdateMessageSendJobV2(member, closedGroupUpdateKind, sentTime)
ApplicationContext.getInstance(context).jobManager.add(job)
}
}
@ -336,7 +348,7 @@ object ClosedGroupsProtocolV2 {
// Notify the user
val infoType = if (isUserLeaving) GroupContext.Type.QUIT else GroupContext.Type.UPDATE
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)
}
return deferred.promise
@ -359,7 +371,19 @@ object ClosedGroupsProtocolV2 {
}
// Generate the new encryption key pair
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
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()
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
@ -368,18 +392,20 @@ object ClosedGroupsProtocolV2 {
val ciphertext = SessionProtocolImpl(context).encrypt(plaintext, publicKey)
ClosedGroupUpdateMessageSendJobV2.KeyPairWrapper(publicKey, ciphertext)
}
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers))
job.setContext(context)
job.onRun() // Run the job immediately
// Store it * after * having sent out the message to the group
apiDB.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
val job = ClosedGroupUpdateMessageSendJobV2(groupPublicKey, ClosedGroupUpdateMessageSendJobV2.Kind.EncryptionKeyPair(wrappers), System.currentTimeMillis())
if (force) {
job.setContext(context)
job.onRun() // Run the job immediately
} else {
ApplicationContext.getInstance(context).jobManager.add(job)
}
}
@JvmStatic
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) {
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_ADDED -> handleClosedGroupMembersAdded(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) {
SignalServiceProtos.ClosedGroupUpdateV2.Type.NEW -> {
(!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
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
@ -427,7 +456,8 @@ object ClosedGroupsProtocolV2 {
// Create the group
val groupID = doubleEncodeGroupID(groupPublicKey)
val groupDB = DatabaseFactory.getGroupDatabase(context)
if (groupDB.getGroup(groupID).orNull() != null) {
val prevGroup = groupDB.getGroup(groupID).orNull()
if (prevGroup != null) {
// Update the group
groupDB.updateTitle(groupID, name)
groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) })
@ -441,8 +471,14 @@ object ClosedGroupsProtocolV2 {
// Store the encryption key pair
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
apiDB.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user
insertIncomingInfoMessage(context, senderPublicKey, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
// Notify the user (if we didn't make the group)
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
LokiPushNotificationManager.performOperation(context, ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
}
@ -452,8 +488,8 @@ object ClosedGroupsProtocolV2 {
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return
}
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
@ -492,16 +528,22 @@ object ClosedGroupsProtocolV2 {
val (contextType, signalType) =
if (senderLeft) GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
else GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins)
if (userPublicKey == senderPublicKey) {
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) {
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return
}
if (!isValidGroupUpdate(group, sentTimestamp, senderPublicKey)) {
@ -518,16 +560,32 @@ object ClosedGroupsProtocolV2 {
val newMembers = members + updateMembers
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) {
// Check that the sender is a member of the group (before the update)
val userPublicKey = TextSecurePreferences.getLocalNumber(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return
}
// Check common group update logic
@ -539,21 +597,23 @@ object ClosedGroupsProtocolV2 {
val name = closedGroupUpdate.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) {
// Check the user leaving isn't us, will already be handled
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
if (senderPublicKey == userPublicKey) {
return
}
val apiDB = DatabaseFactory.getLokiAPIDatabase(context)
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return
}
val name = group.title
@ -576,7 +636,12 @@ object ClosedGroupsProtocolV2 {
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) {
@ -589,8 +654,8 @@ object ClosedGroupsProtocolV2 {
val groupDB = DatabaseFactory.getGroupDatabase(context)
val groupID = doubleEncodeGroupID(groupPublicKey)
val group = groupDB.getGroup(groupID).orNull()
if (group == null) {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
if (group == null || !group.isActive) {
Log.d("Loki", "Ignoring closed group info message for nonexistent or inactive group.")
return
}
val oldMembers = group.members.map { it.serialize() }
@ -624,7 +689,13 @@ object ClosedGroupsProtocolV2 {
val wasSenderRemoved = !members.contains(senderPublicKey)
val type0 = if (wasSenderRemoved) GroupContext.Type.QUIT else GroupContext.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) {
@ -700,7 +771,9 @@ object ClosedGroupsProtocolV2 {
}
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 groupContextBuilder = GroupContext.newBuilder()
.setId(ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID)))
@ -708,9 +781,11 @@ object ClosedGroupsProtocolV2 {
.setName(name)
.addAllMembers(members)
.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 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)
}

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.session.libsignal.utilities.logging.Log
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.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.protocol.meta.TTLUtilities
@ -56,7 +57,7 @@ class NullMessageSendJob private constructor(parameters: Parameters, private val
try {
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedContentMessage, false, ttl, false,
false, false, false, false)
false, false, false, Optional.absent())
} catch (e: Exception) {
Log.d("Loki", "Failed to send null message to: $publicKey due to error: $e.")
throw e

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -175,10 +175,6 @@ public class IncomingTextMessage implements Parcelable {
return message;
}
public IncomingTextMessage withMessageBody(String message) {
return new IncomingTextMessage(this, message);
}
public Address getSender() {
return sender;
}
@ -250,7 +246,6 @@ public class IncomingTextMessage implements Parcelable {
public boolean isUnidentified() {
return unidentified;
}
@Override
public int describeContents() {
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.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.api.opengroups.PublicChat
interface StorageProtocol {
@ -56,6 +57,8 @@ interface StorageProtocol {
// Open Groups
fun getOpenGroup(threadID: String): OpenGroup?
fun getThreadID(openGroupID: String): String?
fun getAllOpenGroups(): Map<Long, PublicChat>
fun addOpenGroup(server: String, channel: Long)
// Open Group Public Keys
fun getOpenGroupPublicKey(server: String): String?
@ -65,6 +68,13 @@ interface StorageProtocol {
fun setOpenGroupDisplayName(publicKey: String, channel: Long, server: String, displayName: 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
fun getLastMessageServerID(group: Long, server: String): Long?
fun setLastMessageServerID(group: Long, server: String, newValue: Long)
@ -75,13 +85,6 @@ interface StorageProtocol {
fun setLastDeletionServerID(group: Long, server: String, newValue: Long)
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
fun getReceivedMessageTimestamps(): Set<Long>
fun addReceivedMessageTimestamp(timestamp: Long)
@ -101,6 +104,11 @@ interface StorageProtocol {
fun removeMember(groupID: String, member: Address)
fun updateMembers(groupID: String, members: List<Address>)
// 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,
name: String, members: Collection<String>, admins: Collection<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 getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?
// Groups
fun getAllGroups(): List<GroupRecord>
// Settings
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.utilities.logging.Log
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() {
@ -33,11 +35,23 @@ class ClosedGroupControlMessage() : ControlMessage() {
class NameChange(val name: String) : Kind()
class MembersAdded(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 {
const val TAG = "ClosedGroupUpdateV2"
const val TAG = "ClosedGroupControlMessage"
fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupControlMessage? {
val closedGroupUpdateProto = proto.dataMessage?.closedGroupUpdateV2 ?: return null
@ -75,7 +89,7 @@ class ClosedGroupControlMessage() : ControlMessage() {
kind = Kind.MembersRemoved(closedGroupUpdateProto.membersList)
}
SignalServiceProtos.ClosedGroupUpdateV2.Type.MEMBER_LEFT -> {
kind = Kind.MemberLeft()
kind = Kind.MemberLeft
}
}
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 {
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 encryptedKeyPair = encryptedKeyPair ?: return null
val result = SignalServiceProtos.ClosedGroupUpdateV2.KeyPairWrapper.newBuilder()
result.publicKey = ByteString.copyFrom(publicKey.toByteArray())
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
result.encryptedKeyPair = encryptedKeyPair
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.jobs.AttachmentDownloadJob
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.control.ClosedGroupUpdate
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.control.*
import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage
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.quotes.QuoteModel
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.utilities.GroupUtil
import org.session.libsignal.utilities.Hex
import org.session.libsession.utilities.SSKEnvironment
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.libsignal.util.guava.Optional
import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupRatchet
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.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import java.security.MessageDigest
import java.util.*
@ -45,8 +41,9 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
when (message) {
is ReadReceipt -> handleReadReceipt(message)
is TypingIndicator -> handleTypingIndicator(message)
is ClosedGroupUpdate -> handleClosedGroupUpdate(message)
is ClosedGroupControlMessage -> handleClosedGroupControlMessage(message)
is ExpirationTimerUpdate -> handleExpirationTimerUpdate(message, proto)
is ConfigurationMessage -> handleConfigurationMessage(message)
is VisibleMessage -> handleVisibleMessage(message, proto, openGroupID)
}
}
@ -105,6 +102,21 @@ fun MessageReceiver.disableExpirationTimer(message: ExpirationTimerUpdate, 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?) {
val storage = MessagingConfiguration.shared.storage
val context = MessagingConfiguration.shared.context
@ -188,173 +200,293 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS
SSKEnvironment.shared.notificationManager.updateNotification(context, threadID)
}
private fun MessageReceiver.handleClosedGroupUpdate(message: ClosedGroupUpdate) {
private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroupControlMessage) {
when (message.kind!!) {
is ClosedGroupUpdate.Kind.New -> handleNewGroup(message)
is ClosedGroupUpdate.Kind.Info -> handleGroupUpdate(message)
is ClosedGroupUpdate.Kind.SenderKeyRequest -> handleSenderKeyRequest(message)
is ClosedGroupUpdate.Kind.SenderKey -> handleSenderKey(message)
is ClosedGroupControlMessage.Kind.New -> handleNewClosedGroup(message)
is ClosedGroupControlMessage.Kind.Update -> handleClosedGroupUpdated(message)
is ClosedGroupControlMessage.Kind.EncryptionKeyPair -> handleClosedGroupEncryptionKeyPair(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 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
val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
if (storage.getGroup(groupID) != null) {
// Update the group
storage.updateTitle(groupID, name)
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
} else {
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)
// Add the group to the user's set of public keys to poll for
sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString())
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
storage.addClosedGroupPublicKey(groupPublicKey)
// Store the encryption key pair
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// 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 storage = MessagingConfiguration.shared.storage
val sskDatabase = MessagingConfiguration.shared.sskDatabase
if (message.kind !is ClosedGroupUpdate.Kind.Info) { return }
val kind = message.kind!! as ClosedGroupUpdate.Kind.Info
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 senderPublicKey = message.sender ?: return
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.Update ?: return
val groupPublicKey = message.groupPublicKey ?: return
val userPublicKey = storage.getUserPublicKey()!!
val wasUserRemoved = !members.contains(userPublicKey)
val wasSenderRemoved = !members.contains(message.sender!!)
if (members.toSet().intersect(oldMembers) != oldMembers.toSet()) {
val allOldRatchets = sskDatabase.getAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
for (pair in allOldRatchets) {
val senderPublicKey = pair.first
val ratchet = pair.second
val collection = ClosedGroupRatchetCollectionType.Old
sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet, collection)
}
sskDatabase.removeAllClosedGroupRatchets(groupPublicKey, ClosedGroupRatchetCollectionType.Current)
if (wasUserRemoved) {
sskDatabase.removeClosedGroupPrivateKey(groupPublicKey)
storage.setActive(groupID, false)
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
} else {
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 address = Address.fromSerialized(member)
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
val closedGroupUpdate = ClosedGroupUpdate()
closedGroupUpdate.kind = closedGroupUpdateKind
MessageSender.send(closedGroupUpdate, address)
}
}
// Unwrap the message
val name = kind.name
val members = kind.members.map { it.toByteArray().toHexString() }
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
return
}
val oldMembers = group.members.map { it.serialize() }
// Check common group update logic
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
return
}
// Check that the admin wasn't removed unless the group was destroyed entirely
if (!members.contains(group.admins.first().toString()) && members.isNotEmpty()) {
android.util.Log.d("Loki", "Ignoring invalid closed group update message.")
return
}
// Remove the group from the user's set of public keys to poll for if the current user was removed
val wasCurrentUserRemoved = !members.contains(userPublicKey)
if (wasCurrentUserRemoved) {
disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
}
// Generate and distribute a new encryption key pair if needed
val wasAnyUserRemoved = (members.toSet().intersect(oldMembers) != oldMembers.toSet())
val isCurrentUserAdmin = group.admins.map { it.toString() }.contains(userPublicKey)
if (wasAnyUserRemoved && isCurrentUserAdmin) {
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, members)
}
// Update the group
storage.updateTitle(groupID, name)
storage.updateMembers(groupID, members.map { Address.fromSerialized(it) })
// Notify the user if needed
if (!wasCurrentUserRemoved) {
// 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 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) {
if (message.kind !is ClosedGroupUpdate.Kind.SenderKeyRequest) { return }
val kind = message.kind!! as ClosedGroupUpdate.Kind.SenderKeyRequest
private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) {
// Prepare
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 groupPublicKey = kind.groupPublicKey.toHexString()
val groupID = GroupUtil.getEncodedClosedGroupID(GroupUtil.getEncodedClosedGroupID(Hex.fromStringCondensed(groupPublicKey)).toByteArray()) //double encoded
val group = storage.getGroup(groupID)
if (group == null) {
Log.d("Loki", "Ignoring closed group sender key request for nonexistent group.")
val userKeyPair = storage.getUserX25519KeyPair()
// Unwrap the message
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
return
}
// Check that the requesting user is a member of the group
if (!group.members.map { it.serialize() }.contains(message.sender!!)) {
Log.d("Loki", "Ignoring closed group sender key request from non-member.")
if (!group.admins.map { it.toString() }.contains(senderPublicKey)) {
android.util.Log.d("Loki", "Ignoring closed group encryption key pair from non-admin.")
return
}
// Respond to the request
Log.d("Loki", "Responding to sender key request from: ${message.sender!!}.")
val userRatchet = sskDatabase.getClosedGroupRatchet(groupPublicKey, userPublicKey, ClosedGroupRatchetCollectionType.Current)
?: SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey)
val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey))
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey)
val closedGroupUpdate = ClosedGroupUpdate()
closedGroupUpdate.kind = closedGroupUpdateKind
MessageSender.send(closedGroupUpdate, Address.fromSerialized(groupID))
// Find our wrapper and decrypt it if possible
val wrapper = kind.wrappers.firstOrNull { it.publicKey!!.toByteArray().toHexString() == userPublicKey } ?: return
val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray()
val plaintext = MessageReceiverDecryption.decryptWithSessionProtocol(encryptedKeyPair, userKeyPair).first
// Parse it
val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext)
val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray()))
// Store it
storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey)
Log.d("Loki", "Received a new closed group encryption key pair")
}
private fun MessageReceiver.handleSenderKey(message: ClosedGroupUpdate) {
if (message.kind !is ClosedGroupUpdate.Kind.SenderKey) { return }
val kind = message.kind!! as ClosedGroupUpdate.Kind.SenderKey
val groupPublicKey = kind.groupPublicKey.toHexString()
val senderKey = kind.senderKey
if (senderKey.publicKey.toHexString() != message.sender!!) {
Log.d("Loki", "Ignoring invalid closed group sender key.")
private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupControlMessage) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val senderPublicKey = message.sender ?: return
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.NameChange ?: return
val groupPublicKey = message.groupPublicKey ?: return
// 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
}
Log.d("Loki", "Received a sender key from: ${message.sender!!}.")
val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf())
MessagingConfiguration.shared.sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet, ClosedGroupRatchetCollectionType.Current)
// Check common group update logic
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
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
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 InvalidClosedGroupUpdate : Error("Invalid group update.")

View File

@ -3,15 +3,19 @@
package org.session.libsession.messaging.sending_receiving
import android.util.Log
import com.google.protobuf.ByteString
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
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.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
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.TextSecurePreferences
import org.session.libsignal.utilities.Hex
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.utilities.hexEncodedPrivateKey
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.*
fun MessageSender.createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
val deferred = deferred<String, Exception>()
// Prepare
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val members = members
val userPublicKey = storage.getUserPublicKey()!!
// Generate a key pair for the group
val groupKeyPair = Curve.generateKeyPair()
val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix
members.plus(userPublicKey)
val membersAsData = members.map { Hex.fromStringCondensed(it) }
// Create ratchets for all members
val senderKeys: List<ClosedGroupSenderKey> = members.map { publicKey ->
val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey)
ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey))
ThreadUtils.queue {
// Prepare
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
val membersAsData = members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) }
// Generate the group's public key
val groupPublicKey = Curve.generateKeyPair().hexEncodedPublicKey // Includes the "05" prefix
// Generate the key pair that'll be used for encryption and decryption
val encryptionKeyPair = Curve.generateKeyPair()
// Create the group
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val admins = setOf( userPublicKey )
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 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> {
val deferred = deferred<Unit, Exception>()
val context = MessagingConfiguration.shared.context
@ -216,11 +357,34 @@ fun MessageSender.leave(groupPublicKey: String) {
return update(groupPublicKey, newMembers, name).get()
}
fun MessageSender.requestSenderKey(groupPublicKey: String, senderPublicKey: String) {
Log.d("Loki", "Requesting sender key for group public key: $groupPublicKey, sender public key: $senderPublicKey.")
val address = Address.fromSerialized(senderPublicKey)
val closedGroupUpdateKind = ClosedGroupUpdate.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey))
val closedGroupUpdate = ClosedGroupUpdate()
closedGroupUpdate.kind = closedGroupUpdateKind
MessageSender.send(closedGroupUpdate, address)
fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, targetMembers: Collection<String>) {
// Prepare
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 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
import android.annotation.SuppressLint
import nl.komponents.kovenant.functional.map
import okhttp3.*
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")
private val TAG = Address::class.java.simpleName
private val cachedFormatter = AtomicReference<Pair<String, ExternalAddressFormatter>>()
@JvmStatic
fun fromSerialized(serialized: String): Address {
return Address(serialized)
}

View File

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

View File

@ -2,6 +2,8 @@ package org.session.libsession.utilities
import org.session.libsignal.service.api.messages.SignalServiceGroup
import org.session.libsignal.utilities.Hex
import java.io.IOException
import kotlin.jvm.Throws
object GroupUtil {
const val CLOSED_GROUP_PREFIX = "__textsecure_group__!"
@ -58,11 +60,27 @@ object GroupUtil {
return groupId.startsWith(MMS_GROUP_PREFIX)
}
@JvmStatic
fun isOpenGroup(groupId: String): Boolean {
return groupId.startsWith(OPEN_GROUP_PREFIX)
}
@JvmStatic
fun isClosedGroup(groupId: String): Boolean {
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 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
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.SignalServiceReceiptMessage;
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.ConfigurationMessage;
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.SignalServiceProtos;
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.DataMessage;
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 message The read receipt to deliver.
* @throws IOException
* @throws UntrustedIdentityException
*/
public void sendReceipt(SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
SignalServiceReceiptMessage message)
throws IOException, UntrustedIdentityException
{
throws IOException {
byte[] content = createReceiptContent(message);
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store);
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, message.getTTL(), useFallbackEncryption, false);
}
/**
* 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);
sendMessage(recipient, getTargetUnidentifiedAccess(unidentifiedAccess), message.getWhen(), content, false, message.getTTL(), useFallbackEncryption);
}
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);
}
/**
* 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.
*
* @param recipient The message's destination.
* @param message The message.
* @throws UntrustedIdentityException
* @throws IOException
*/
public SendMessageResult sendMessage(long messageID,
SignalServiceAddress recipient,
Optional<UnidentifiedAccessPair> unidentifiedAccess,
SignalServiceDataMessage message)
throws UntrustedIdentityException, IOException
throws IOException
{
byte[] content = createMessageContent(message, recipient);
long timestamp = message.getTimestamp();
boolean useFallbackEncryption = SessionManagementProtocol.shared.shouldMessageUseFallbackEncryption(message, recipient.getNumber(), store);
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
// boolean wouldSignalSendSyncMessage = (result.getSuccess() != null && result.getSuccess().isNeedsSync()) || unidentifiedAccess.isPresent();
@ -325,8 +282,7 @@ public class SignalServiceMessageSender {
List<SignalServiceAddress> recipients,
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,
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
// whether an attachment is being sent to an open group or not.
byte[] content = createMessageContent(message, recipients.get(0));
@ -350,7 +306,7 @@ public class SignalServiceMessageSender {
for (String device : linkedDevices) {
SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device);
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()) {
content = createMultiDeviceStickerPackOperationContent(message.getStickerPackOperations().get());
} else if (message.getVerified().isPresent()) {
sendMessage(message.getVerified().get(), unidentifiedAccess);
return;
} else {
throw new IOException("Unsupported sync message!");
@ -392,18 +347,10 @@ public class SignalServiceMessageSender {
for (String device : linkedDevices) {
SignalServiceAddress deviceAsAddress = new SignalServiceAddress(device);
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) {
this.pipe.set(Optional.fromNullable(pipe));
this.unidentifiedPipe.set(Optional.fromNullable(unidentifiedPipe));
@ -452,12 +399,6 @@ public class SignalServiceMessageSender {
result.getUrl());
}
private void sendMessage(VerifiedMessage message, Optional<UnidentifiedAccessPair> unidentifiedAccess)
throws IOException, UntrustedIdentityException
{
}
private byte[] createTypingContent(SignalServiceTypingMessage message) {
Content.Builder container = Content.newBuilder();
TypingMessage.Builder builder = TypingMessage.newBuilder();
@ -494,32 +435,6 @@ public class SignalServiceMessageSender {
{
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();
List<AttachmentPointer> pointers = createAttachmentPointers(message.getAttachments(), recipient);
@ -559,6 +474,10 @@ public class SignalServiceMessageSender {
builder.setProfileKey(ByteString.copyFrom(message.getProfileKey().get()));
}
if (message.getSyncTarget().isPresent()) {
builder.setSyncTarget(message.getSyncTarget().get());
}
if (message.getQuote().isPresent()) {
DataMessage.Quote.Builder quoteBuilder = DataMessage.Quote.newBuilder()
.setId(message.getQuote().get().getId())
@ -636,40 +555,6 @@ public class SignalServiceMessageSender {
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)
throws IOException
{
@ -987,6 +872,7 @@ public class SignalServiceMessageSender {
throws IOException
{
List<SendMessageResult> results = new LinkedList<>();
SignalServiceAddress ownAddress = localAddress;
Iterator<SignalServiceAddress> recipientIterator = recipients.iterator();
Iterator<Optional<UnidentifiedAccess>> unidentifiedAccessIterator = unidentifiedAccess.iterator();
@ -995,7 +881,7 @@ public class SignalServiceMessageSender {
try {
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);
} catch (UnregisteredUserException e) {
Log.w(TAG, e);
@ -1009,41 +895,46 @@ public class SignalServiceMessageSender {
return results;
}
private SendMessageResult sendMessage(SignalServiceAddress recipient,
private SendMessageResult sendMessage(SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess,
long timestamp,
byte[] content,
boolean online,
int ttl,
boolean useFallbackEncryption,
boolean isSyncMessage)
long timestamp,
byte[] content,
boolean online,
int ttl,
boolean useFallbackEncryption)
throws IOException
{
// 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,
final SignalServiceAddress recipient,
public SendMessageResult sendMessage(final long messageID,
final SignalServiceAddress recipient,
Optional<UnidentifiedAccess> unidentifiedAccess,
long timestamp,
byte[] content,
boolean online,
int ttl,
boolean isDeviceLinkMessage,
boolean useFallbackEncryption,
boolean isClosedGroup,
boolean isSyncMessage,
boolean notifyPNServer)
long timestamp,
byte[] content,
boolean online,
int ttl,
boolean isDeviceLinkMessage,
boolean useFallbackEncryption,
boolean isClosedGroup,
boolean notifyPNServer,
Optional<String> syncTarget)
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);
try {
if (publicChat != null) {
return sendMessageToPublicChat(messageID, recipient, timestamp, content, publicChat);
} 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) {
return SendMessageResult.networkFailure(recipient);
@ -1152,10 +1043,10 @@ public class SignalServiceMessageSender {
int ttl,
boolean useFallbackEncryption,
boolean isClosedGroup,
final boolean notifyPNServer)
final boolean notifyPNServer,
Optional<String> syncTarget)
throws IOException, UntrustedIdentityException
{
if (recipient.getNumber().equals(userPublicKey)) { return SendMessageResult.success(recipient, false, false); }
final SettableFuture<?>[] future = { new SettableFuture<Unit>() };
OutgoingPushMessageList messages = getSessionProtocolEncryptedMessage(recipient, timestamp, content);
// Loki - Remove this when we have shared sender keys
@ -1221,14 +1112,10 @@ public class SignalServiceMessageSender {
}
return Unit.INSTANCE;
}
}).fail(new Function1<Exception, Unit>() {
@Override
public Unit invoke(Exception exception) {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
f.setException(exception);
return Unit.INSTANCE;
}
}).fail(exception -> {
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
f.setException(exception);
return Unit.INSTANCE;
});
@SuppressWarnings("unchecked") SettableFuture<Unit> f = (SettableFuture<Unit>)future[0];
@ -1304,12 +1191,6 @@ public class SignalServiceMessageSender {
return builder.build();
}
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment)
throws IOException
{
return createAttachmentPointer(attachment, false, null);
}
private AttachmentPointer createAttachmentPointer(SignalServiceAttachmentStream attachment, SignalServiceAddress recipient)
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();
String masterPublicKey = protoDeviceLinkMessage.getPrimaryPublicKey();
String slavePublicKey = protoDeviceLinkMessage.getSecondaryPublicKey();
@ -331,7 +342,6 @@ public class SignalServiceCipher {
kotlin.Pair<byte[], String> plaintextAndSenderPublicKey = SessionProtocolUtilities.INSTANCE.decryptClosedGroupCiphertext(ciphertext, groupPublicKey, apiDB, sessionProtocolImpl);
paddedMessage = plaintextAndSenderPublicKey.getFirst();
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);
sessionVersion = sessionCipher.getSessionVersion();
} else if (envelope.isPreKeySignalMessage()) {
@ -389,6 +399,7 @@ public class SignalServiceCipher {
ClosedGroupUpdate closedGroupUpdate = content.getClosedGroupUpdate();
ClosedGroupUpdateV2 closedGroupUpdateV2 = content.getClosedGroupUpdateV2();
boolean isDeviceUnlinkingRequest = ((content.getFlags() & DataMessage.Flags.DEVICE_UNLINKING_REQUEST_VALUE) != 0);
String syncTarget = content.getSyncTarget();
for (AttachmentPointer pointer : content.getAttachmentsList()) {
attachments.add(createAttachmentPointer(pointer));
@ -417,7 +428,8 @@ public class SignalServiceCipher {
null,
closedGroupUpdate,
closedGroupUpdateV2,
isDeviceUnlinkingRequest);
isDeviceUnlinkingRequest,
syncTarget);
}
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.SignalServiceTypingMessage;
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.internal.push.SignalServiceProtos;
import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink;
import org.session.libsignal.service.loki.protocol.sessionmanagement.PreKeyBundleMessage;
@ -33,10 +35,11 @@ public class SignalServiceContent {
private final Optional<SignalServiceTypingMessage> typingMessage;
// Loki
private final Optional<DeviceLink> deviceLink;
public Optional<PreKeyBundleMessage> preKeyBundleMessage = Optional.absent();
public Optional<String> senderDisplayName = Optional.absent();
public Optional<String> senderProfilePictureURL = Optional.absent();
private final Optional<DeviceLink> deviceLink;
public Optional<SignalServiceProtos.Content> configurationMessageProto = Optional.absent();
public Optional<PreKeyBundleMessage> preKeyBundleMessage = 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) {
this.sender = sender;
@ -128,6 +131,22 @@ public class SignalServiceContent {
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) {
this.sender = sender;
this.senderDevice = senderDevice;

View File

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

View File

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