Merge branch 'testing_multi_merge' of https://github.com/hjubb/session-android into multi_device_refactor

This commit is contained in:
Ryan ZHAO 2021-02-16 09:26:55 +11:00
commit a7764ed5c9
37 changed files with 620 additions and 545 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

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

@ -195,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);
@ -222,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);
@ -231,6 +231,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
});
notifyConversationListListeners();
return threadId;
}
public boolean delete(@NonNull String groupId) {

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

@ -730,7 +730,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
@ -401,7 +400,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)

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;
@ -97,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;
@ -266,7 +264,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType {
SessionMetaProtocol.handleProfileUpdateIfNeeded(context, content);
if (content.configurationMessageProto.isPresent()) {
MultiDeviceProtocol.handleConfigurationMessage(context, content.configurationMessageProto.get(), content.getSender());
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();
@ -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

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

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

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

@ -5,6 +5,7 @@ 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
@ -33,7 +34,7 @@ object MultiDeviceProtocol {
try {
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false,
true, false, true, 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.")
@ -51,14 +52,14 @@ object MultiDeviceProtocol {
try {
messageSender.sendMessage(0, address, udAccess.get().targetUnidentifiedAccess,
Date().time, serializedMessage, false, configurationMessage.ttl.toInt(), false,
true, false, true, 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) {
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
@ -79,7 +80,7 @@ object MultiDeviceProtocol {
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)
ClosedGroupsProtocolV2.handleNewClosedGroup(context, closedGroupUpdate.build(), userPublicKey, timestamp)
}
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
for (openGroup in configurationMessage.openGroups) {

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

@ -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,8 +326,8 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
private static Drawable getPlaceholderDrawable(Context context, Recipient recipient) {
String publicKey = recipient.getAddress().serialize();
String hepk = (recipient.isLocalNumber() && publicKey != null)
? TextSecurePreferences.getLocalNumber(context)
String hepk = (recipient.isLocalNumber() && publicKey == null)
? TextSecurePreferences.getMasterHexEncodedPublicKey(context)
: publicKey;
String displayName = recipient.getName();
return AvatarPlaceholderGenerator.generate(context, 128, hepk, displayName);

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

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

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

@ -60,10 +60,12 @@ 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)
}

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);
}
}
@ -391,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));
@ -487,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);
@ -552,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())
@ -629,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
{
@ -980,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();
@ -988,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);
@ -1002,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);
@ -1145,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
@ -1214,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];
@ -1297,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

@ -342,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()) {
@ -400,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));
@ -428,7 +428,8 @@ public class SignalServiceCipher {
null,
closedGroupUpdate,
closedGroupUpdateV2,
isDeviceUnlinkingRequest);
isDeviceUnlinkingRequest,
syncTarget);
}
private SignalServiceSyncMessage createSynchronizeMessage(Metadata metadata, SyncMessage content)

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.")