diff --git a/res/layout/activity_create_closed_group.xml b/res/layout/activity_create_closed_group.xml index 15a0579e38..296128a3c7 100644 --- a/res/layout/activity_create_closed_group.xml +++ b/res/layout/activity_create_closed_group.xml @@ -12,27 +12,15 @@ android:orientation="vertical"> - - + android:hint="@string/activity_create_closed_group_edit_text_hint" /> 继续 复制 - 无效的网址 + 无效的链接 复制到剪贴板 无法链接设备。 下一步 - 分享 + 共享 无效的Session ID 取消 您的Session ID 您的Session从这里开始... - 注册Session ID + 创建Session ID 继续使用您的Session ID - 链接到现有帐号 - 您的设备已成功断开链接 + 关联现有帐号 + 您的设备已成功取消关联 什么是Session? Session是一个去中心化的加密消息应用。 - 所以Session不会收集我的个人信息或对话原始数据?怎么做到的?。 + 所以Session不会收集我的个人信息或者对话元数据?怎么做到的? 通过结合高效的匿名路由和端到端的加密技术。 - 好朋友就要与朋友使用能够保证信息安全的聊天工具,不用谢啦 + 好朋友之间就要使用能够保证信息安全的聊天工具,不用谢啦! - 向您的新Session ID打个招呼吧 - Session ID是其他用户需要与您聊天时使用的独一无二的地址。与您的真实身份无关,Session ID的设计是完全是匿名和私有的。 + 向您的Session ID打个招呼吧 + 您的Session ID是其他用户在与您聊天时使用的独一无二的地址。Session ID与您的真实身份无关,它在设计上完全是匿名且私密的。 复制到剪贴板 恢复您的帐号 在您重新登陆并需要恢复账户时,请输入您注册帐号时的恢复口令。 输入您的恢复口令 - 链接设备 + 关联设备 输入Session ID 扫描二维码 - 在您的设备上导航到“设置”>“设备”>“链接设备”,然后扫描出现的二维码以开始链接过程。 + 在您的设备上导航到“设置”>“设备”>“链接设备”,然后扫描出现的二维码以开始关联。 - 链接您的设备 - 在您的另一个设备上导航到“设置”>“设备” >“链接设备”,然后在此处输入Session ID以开始链接过程。 + 关联您的设备 + 在您的另一个设备上导航到“设置”>“设备” >“链接设备”,然后在此处输入Session ID以开始关联。 输入Session ID - 选择您的显示名称 - 使用Session时,这就是您的名字。它可以是您的真实姓名,别名或您喜欢的其他任何名称。 - 输入显示名称 - 请选择一个显示名称 - 请选择一个仅包含 az,AZ,0-9 和_字符的显示名称 - 请选择一个较短的显示名称 + 选择您想显示的名称 + 这就是您在使用Session时的名字。它可以是您的真实姓名,别名或您喜欢的其他任何名称。 + 输入您想显示的名称 + 请设定一个名称 + 请设定一个仅包含 a-z,A-Z,0-9 和 _ 的名称 + 请设定一个较短的名称 推荐的选项 请选择一个选项 @@ -1331,15 +1331,15 @@ 您的恢复口令 这里是您的恢复口令 - 您的恢复口令是Session ID的主密钥 - 如果您无法访问您的现有设备,则可以使用它在其他设备上恢复Session ID。将您的恢复口令存储在安全的地方,不要将其提供给任何人。 + 您的恢复口令是Session ID的主密钥 - 如果您无法访问您的现有设备,则可以使用它在其他设备上恢复您的Session ID。请将您的恢复口令存储在安全的地方,不要将其提供给任何人。 长按显示内容 - 保存恢复短语以保护您的帐号安全 - 点击并按住遮盖住的单词以显示您的恢复短语,然后安全地存储它以保护Session ID。 - 确保将恢复短语存储在安全的地方 + 保存恢复口令以保护您的帐号安全 + 点击并按住遮盖住的单词以显示您的恢复口令,请将它安全地存储以保护您的Session ID。 + 请确保将恢复口令存储在安全的地方 路径 - Session会通过Session的分散网络中的多个服务节点跳转消息以隐藏IP。以下是国家您目前的消息连接跳转服务节点所在地: + Session会通过其去中心化网络中的多个服务节点跳转消息以隐藏IP。以下国家是您目前的消息连接跳转服务节点所在地: 入口节点 服务节点 @@ -1349,38 +1349,38 @@ 新建私人聊天 输入Session ID 扫描二维码 - 扫描另一用户的二维码以开始使用Session。您可以在帐号设置中点击二维码图标找到二维码。 + 扫描其他用户的二维码来发起对话。您可以在帐号设置中点击二维码图标找到二维码。 输入对方的Session ID - 用户可以通过进入帐号设置并点击共享Session ID来分享自己的Session ID,或通过共享其二维码来分享其Session ID。 + 用户可以通过进入帐号设置并点击“共享Session ID”来分享自己的Session ID,或通过共享其二维码来分享其Session ID。 Session需要摄像头访问权限才能扫描二维码 授予摄像头访问权限 创建私密群组 输入群组名称 - 私密群组最多支持 10 位成员,并提供与一对一对话相同的隐私保护。 + 私密群组最多支持10位成员,并提供与一对一对话相同的隐私保护。 您还没有任何联系人 开始对话 请输入群组名称 请输入较短的群组名称 - 请选择至少 2 位小组成员 - 私密群组成员不得超过 10 个 + 请选择至少2位群组成员 + 私密群组成员不得超过10个 您群组中的一位成员的Session ID无效 加入公开群组 无法加入群组 - 公开群组网址 + 公开群组链接 扫描二维码 扫描您想加入的公开群组的二维码 - 输入一个公开群组网址 + 输入公开群组链接 设置 - 输入显示的名称 - 请选择一个显示名称 - 请选择一个仅包含 az,AZ,0-9 和 _ 字符的显示名称 - 请选择一个较短的显示名称 + 输入您想显示的名称 + 请设定一个名称 + 请设定一个仅包含 a-z,A-Z,0-9 和 _ 的名称 + 请设定一个较短的名称 隐私 通知 聊天 @@ -1389,44 +1389,44 @@ 清除数据 通知 - 通知风格类型 + 通知风格 通知内容 隐私 - 聊天 + 会话 设备 达到设备限制 - 当前不允许链接多个设备。 - 无法断开链接设备。 - 您的设备已成功断开链接 - 无法链接设备。 - 您尚未链接任何设备 - 链接设备(测试版) + 当前不允许关联多个设备。 + 无法断开关联设备。 + 您的设备已成功取消关联 + 无法关联设备。 + 您尚未关联任何设备 + 关联设备(测试版) 通知选项 等待授权 - 设备链接授权 + 设备关联已授权 请检查以下单词是否与您其他设备上显示的单词匹配。 - 您的设备已成功链接 + 您的设备已成功关联 等待设备 - 收到链接请求 - 授权设备链接 - 在其他设备上下载Session,然后点击登陆页面屏幕底部的“链接到现有帐号”。如果您的其他设备上已有一个帐号,则必须先删除已有帐号。 + 收到关联请求 + 授权设备关联 + 在其他设备上下载Session,然后点击登陆页面屏幕底部的“关联到现有帐号”。如果您的其他设备上已有一个帐号,则必须先删除已有帐号。 请检查以下单词是否与您其他设备上显示的单词匹配。 - 创建设备关联时,请耐心等待。这可能需要一分钟的时间。 + 创建设备关联时,请耐心等待。这可能需要一分钟左右的时间。 授权 - 更换名字 - 断开设备链接 + 更换名称 + 断开设备关联 - 输入名字 + 输入名称 您的恢复口令 - 这是您的恢复口令。有了它,您可以将Session ID还原或迁移到新设备上。 + 这是您的恢复口令。您可以通过该口令将Session ID还原或迁移到新设备上。 清除所有数据 这将永久删除您的消息、对话和联系人。 @@ -1434,13 +1434,13 @@ 二维码 查看我的二维码 扫描二维码 - 扫描对方的二维码,与他们开始对话 + 扫描对方的二维码以发起对话 - 这是您的二维码。其他用户可以对其进行扫描以开始对话。 + 这是您的二维码。其他用户可以对其进行扫描以发起与您的对话。 分享二维码 您要恢复与%s的对话吗? - 解散 + 取消 恢复 联系人 diff --git a/src/org/thoughtcrime/securesms/ApplicationContext.java b/src/org/thoughtcrime/securesms/ApplicationContext.java index ae30fce7b2..5090774f31 100644 --- a/src/org/thoughtcrime/securesms/ApplicationContext.java +++ b/src/org/thoughtcrime/securesms/ApplicationContext.java @@ -61,12 +61,15 @@ import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; import org.thoughtcrime.securesms.loki.activities.HomeActivity; import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker; +import org.thoughtcrime.securesms.loki.api.ClosedGroupPoller; import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager; import org.thoughtcrime.securesms.loki.api.PublicChatManager; import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; -import org.thoughtcrime.securesms.loki.protocol.PushSessionRequestMessageSendJob; +import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase; +import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol; +import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; import org.thoughtcrime.securesms.loki.utilities.Broadcaster; import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; @@ -106,6 +109,8 @@ import org.whispersystems.signalservice.loki.api.opengroups.PublicChatAPI; import org.whispersystems.signalservice.loki.api.shelved.p2p.LokiP2PAPI; import org.whispersystems.signalservice.loki.api.shelved.p2p.LokiP2PAPIDelegate; import org.whispersystems.signalservice.loki.database.LokiAPIDatabaseProtocol; +import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation; +import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementationDelegate; import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager; import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol; import org.whispersystems.signalservice.loki.protocol.meta.TTLUtilities; @@ -138,7 +143,8 @@ import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant; * * @author Moxie Marlinspike */ -public class ApplicationContext extends MultiDexApplication implements DependencyInjector, DefaultLifecycleObserver, LokiP2PAPIDelegate, SessionManagementProtocolDelegate { +public class ApplicationContext extends MultiDexApplication implements DependencyInjector, DefaultLifecycleObserver, LokiP2PAPIDelegate, + SessionManagementProtocolDelegate, SharedSenderKeysImplementationDelegate { private static final String TAG = ApplicationContext.class.getSimpleName(); private final static int OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10 MB @@ -154,6 +160,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc // Loki public MessageNotifier messageNotifier = null; public Poller poller = null; + public ClosedGroupPoller closedGroupPoller = null; public PublicChatManager publicChatManager = null; private PublicChatAPI publicChatAPI = null; public Broadcaster broadcaster = null; @@ -183,8 +190,10 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc LokiAPIDatabase apiDB = DatabaseFactory.getLokiAPIDatabase(this); LokiThreadDatabase threadDB = DatabaseFactory.getLokiThreadDatabase(this); LokiUserDatabase userDB = DatabaseFactory.getLokiUserDatabase(this); + SharedSenderKeysDatabase sskDatabase = DatabaseFactory.getSSKDatabase(this); String userPublicKey = TextSecurePreferences.getLocalNumber(this); SessionResetImplementation sessionResetImpl = new SessionResetImplementation(this); + SharedSenderKeysImplementation.Companion.configureIfNeeded(sskDatabase, this); if (userPublicKey != null) { SwarmAPI.Companion.configureIfNeeded(apiDB); SnodeAPI.Companion.configureIfNeeded(userPublicKey, apiDB, broadcaster); @@ -193,7 +202,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc SyncMessagesProtocol.Companion.configureIfNeeded(apiDB, userPublicKey); } MultiDeviceProtocol.Companion.configureIfNeeded(apiDB); - SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, threadDB, this); + SessionManagementProtocol.Companion.configureIfNeeded(sessionResetImpl, sskDatabase, this); setUpP2PAPIIfNeeded(); PushNotificationAcknowledgement.Companion.configureIfNeeded(BuildConfig.DEBUG); if (setUpStorageAPIIfNeeded()) { @@ -507,16 +516,20 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc } return Unit.INSTANCE; }); + SharedSenderKeysDatabase sskDatabase = DatabaseFactory.getSSKDatabase(this); + ClosedGroupPoller.Companion.configureIfNeeded(this, sskDatabase); + closedGroupPoller = ClosedGroupPoller.Companion.getShared(); } public void startPollingIfNeeded() { setUpPollingIfNeeded(); if (poller != null) { poller.startIfNeeded(); } + if (closedGroupPoller != null) { closedGroupPoller.startIfNeeded(); } } public void stopPolling() { - if (poller == null) { return; } - poller.stopIfNeeded(); + if (poller != null) { poller.stopIfNeeded(); } + if (closedGroupPoller != null) { closedGroupPoller.stopIfNeeded(); } } private void resubmitProfilePictureIfNeeded() { @@ -621,8 +634,13 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc // Send the session request long timestamp = new Date().getTime(); apiDB.setSessionRequestSentTimestamp(publicKey, timestamp); - PushSessionRequestMessageSendJob job = new PushSessionRequestMessageSendJob(publicKey, timestamp); + SessionRequestMessageSendJob job = new SessionRequestMessageSendJob(publicKey, timestamp); jobManager.add(job); } + + @Override + public void requestSenderKey(@NotNull String groupPublicKey, @NotNull String senderPublicKey) { + ClosedGroupsProtocol.requestSenderKey(this, groupPublicKey, senderPublicKey); + } // endregion } diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 40d8383401..139d05281e 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -211,6 +211,7 @@ import org.thoughtcrime.securesms.util.Dialogs; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.ExpirationUtil; +import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.ServiceUtil; @@ -229,6 +230,7 @@ import org.whispersystems.signalservice.loki.protocol.mentions.Mention; import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager; import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol; import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol; +import org.whispersystems.signalservice.loki.utilities.HexEncodingKt; import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation; import java.io.IOException; @@ -1167,10 +1169,26 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity builder.setCancelable(true); builder.setMessage(getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group)); builder.setPositiveButton(R.string.yes, (dialog, which) -> { - Recipient groupRecipient = getRecipient(); - if (ClosedGroupsProtocol.leaveGroup(this, groupRecipient)) { - initializeEnabledCheck(); - } else { + Recipient groupRecipient = getRecipient(); + String groupPublicKey; + boolean isSSKBasedClosedGroup; + try { + groupPublicKey = HexEncodingKt.toHexString(GroupUtil.getDecodedId(groupRecipient.getAddress().toString())); + isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey); + } catch (IOException e) { + groupPublicKey = null; + isSSKBasedClosedGroup = false; + } + try { + if (isSSKBasedClosedGroup) { + ClosedGroupsProtocol.leave(this, groupPublicKey); + initializeEnabledCheck(); + } else if (ClosedGroupsProtocol.leaveLegacyGroup(this, groupRecipient)) { + initializeEnabledCheck(); + } else { + Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show(); + } + } catch (Exception e) { Toast.makeText(this, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show(); } }); @@ -2229,13 +2247,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void markThreadAsRead() { + Recipient recipient = this.recipient; new AsyncTask() { @Override protected Void doInBackground(Long... params) { Context context = ConversationActivity.this; List messageIds = DatabaseFactory.getThreadDatabase(context).setRead(params[0], false); - MarkReadReceiver.process(context, messageIds); + if (!org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol.shouldSendReadReceipt(recipient.getAddress())) { + for (MarkedMessageInfo messageInfo : messageIds) { + MarkReadReceiver.scheduleDeletion(context, messageInfo.getExpirationInfo()); + } + } else { + MarkReadReceiver.process(context, messageIds); + } return null; } @@ -2389,10 +2414,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity final long id = fragment.stageOutgoingMessage(outgoingMessage); - if (!recipient.isGroupRecipient()) { - ApplicationContext.getInstance(this).sendSessionRequestIfNeeded(recipient.getAddress().serialize()); - } - new AsyncTask() { @Override protected Long doInBackground(Void... param) { @@ -2400,7 +2421,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); } - return MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id)); + long result = MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id)); + + if (!recipient.isGroupRecipient()) { + ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(recipient.getAddress().serialize()); + } + + return result; } @Override @@ -2436,10 +2463,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity silentlySetComposeText(""); final long id = fragment.stageOutgoingMessage(message); - if (!recipient.isGroupRecipient()) { - ApplicationContext.getInstance(this).sendSessionRequestIfNeeded(recipient.getAddress().serialize()); - } - new AsyncTask() { @Override protected Long doInBackground(OutgoingTextMessage... messages) { @@ -2447,7 +2470,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity DatabaseFactory.getRecipientDatabase(context).setProfileSharing(recipient, true); } - return MessageSender.send(context, messages[0], threadId, forceSms, () -> fragment.releaseOutgoingMessage(id)); + long result = MessageSender.send(context, messages[0], threadId, forceSms, () -> fragment.releaseOutgoingMessage(id)); + + if (!recipient.isGroupRecipient()) { + ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(recipient.getAddress().serialize()); + } + + return result; } @Override diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 4465a5c1c0..d945967507 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.loki.database.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.loki.database.LokiPreKeyRecordDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; +import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase; import org.thoughtcrime.securesms.util.TextSecurePreferences; public class DatabaseFactory { @@ -73,6 +74,7 @@ public class DatabaseFactory { private final LokiMessageDatabase lokiMessageDatabase; private final LokiThreadDatabase lokiThreadDatabase; private final LokiUserDatabase lokiUserDatabase; + private final SharedSenderKeysDatabase sskDatabase; public static DatabaseFactory getInstance(Context context) { synchronized (lock) { @@ -187,6 +189,10 @@ public class DatabaseFactory { public static LokiUserDatabase getLokiUserDatabase(Context context) { return getInstance(context).lokiUserDatabase; } + + public static SharedSenderKeysDatabase getSSKDatabase(Context context) { + return getInstance(context).sskDatabase; + } // endregion public static void upgradeRestored(Context context, SQLiteDatabase database){ @@ -200,32 +206,33 @@ public class DatabaseFactory { DatabaseSecret databaseSecret = new DatabaseSecretProvider(context).getOrCreateDatabaseSecret(); AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); - this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret); - this.sms = new SmsDatabase(context, databaseHelper); - this.mms = new MmsDatabase(context, databaseHelper); - this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret); - this.media = new MediaDatabase(context, databaseHelper); - this.thread = new ThreadDatabase(context, databaseHelper); - this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper); - this.identityDatabase = new IdentityDatabase(context, databaseHelper); - this.draftDatabase = new DraftDatabase(context, databaseHelper); - this.pushDatabase = new PushDatabase(context, databaseHelper); - this.groupDatabase = new GroupDatabase(context, databaseHelper); - this.recipientDatabase = new RecipientDatabase(context, databaseHelper); - this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper); - this.contactsDatabase = new ContactsDatabase(context); - this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper); - this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper); - this.sessionDatabase = new SessionDatabase(context, databaseHelper); - this.searchDatabase = new SearchDatabase(context, databaseHelper); - this.jobDatabase = new JobDatabase(context, databaseHelper); - this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret); - this.lokiAPIDatabase = new LokiAPIDatabase(context, databaseHelper); + this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret); + this.sms = new SmsDatabase(context, databaseHelper); + this.mms = new MmsDatabase(context, databaseHelper); + this.attachments = new AttachmentDatabase(context, databaseHelper, attachmentSecret); + this.media = new MediaDatabase(context, databaseHelper); + this.thread = new ThreadDatabase(context, databaseHelper); + this.mmsSmsDatabase = new MmsSmsDatabase(context, databaseHelper); + this.identityDatabase = new IdentityDatabase(context, databaseHelper); + this.draftDatabase = new DraftDatabase(context, databaseHelper); + this.pushDatabase = new PushDatabase(context, databaseHelper); + this.groupDatabase = new GroupDatabase(context, databaseHelper); + this.recipientDatabase = new RecipientDatabase(context, databaseHelper); + this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper); + this.contactsDatabase = new ContactsDatabase(context); + this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper); + this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper); + this.sessionDatabase = new SessionDatabase(context, databaseHelper); + this.searchDatabase = new SearchDatabase(context, databaseHelper); + this.jobDatabase = new JobDatabase(context, databaseHelper); + this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret); + this.lokiAPIDatabase = new LokiAPIDatabase(context, databaseHelper); this.lokiContactPreKeyDatabase = new LokiPreKeyRecordDatabase(context, databaseHelper); - this.lokiPreKeyBundleDatabase = new LokiPreKeyBundleDatabase(context, databaseHelper); - this.lokiMessageDatabase = new LokiMessageDatabase(context, databaseHelper); - this.lokiThreadDatabase = new LokiThreadDatabase(context, databaseHelper); - this.lokiUserDatabase = new LokiUserDatabase(context, databaseHelper); + this.lokiPreKeyBundleDatabase = new LokiPreKeyBundleDatabase(context, databaseHelper); + this.lokiMessageDatabase = new LokiMessageDatabase(context, databaseHelper); + this.lokiThreadDatabase = new LokiThreadDatabase(context, databaseHelper); + this.lokiUserDatabase = new LokiUserDatabase(context, databaseHelper); + this.sskDatabase = new SharedSenderKeysDatabase(context, databaseHelper); } public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret, diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 2dc877906d..58afc5520d 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.loki.database.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.loki.database.LokiPreKeyRecordDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; +import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.GroupUtil; @@ -85,8 +86,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV9 = 30; private static final int lokiV10 = 31; private static final int lokiV11 = 32; + private static final int lokiV12 = 33; - private static final int DATABASE_VERSION = lokiV11; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes + private static final int DATABASE_VERSION = lokiV12; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -134,20 +136,20 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } db.execSQL(StickerDatabase.CREATE_TABLE); - db.execSQL(LokiAPIDatabase.getCreateSnodePoolCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateSwarmCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateSwarmTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTable2Command()); + db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesTable2Command()); + db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand()); db.execSQL(LokiAPIDatabase.getCreateDeviceLinkCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateUserCountCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand()); db.execSQL(LokiAPIDatabase.getCreateSessionRequestTimestampCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyDBCommand()); + db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyTableCommand()); db.execSQL(LokiPreKeyBundleDatabase.getCreateTableCommand()); db.execSQL(LokiPreKeyRecordDatabase.getCreateTableCommand()); db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand()); @@ -157,6 +159,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiThreadDatabase.getCreatePublicChatTableCommand()); db.execSQL(LokiUserDatabase.getCreateDisplayNameTableCommand()); db.execSQL(LokiUserDatabase.getCreateServerDisplayNameTableCommand()); + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupRatchetTableCommand()); + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeyTableCommand()); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -519,9 +523,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } if (oldVersion < lokiV1) { - db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateOpenGroupAuthTokenTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastMessageServerIDTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateLastDeletionServerIDTableCommand()); } if (oldVersion < lokiV2) { @@ -541,7 +545,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } if (oldVersion < lokiV5) { - db.execSQL(LokiAPIDatabase.getCreateUserCountCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateUserCountTableCommand()); } if (oldVersion < lokiV6) { @@ -590,17 +594,24 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { } if (oldVersion < lokiV9) { - db.execSQL(LokiAPIDatabase.getCreateSnodePoolCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateSnodePoolTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateOnionRequestPathTableCommand()); } if (oldVersion < lokiV10) { - db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampCacheCommand()); - db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampCacheCommand()); + db.execSQL(LokiAPIDatabase.getCreateSessionRequestSentTimestampTableCommand()); + db.execSQL(LokiAPIDatabase.getCreateSessionRequestProcessedTimestampTableCommand()); } if (oldVersion < lokiV11) { - db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyDBCommand()); + db.execSQL(LokiAPIDatabase.getCreateOpenGroupPublicKeyTableCommand()); + } + + if (oldVersion < lokiV12) { + db.execSQL(LokiAPIDatabase.getCreateLastMessageHashValueTable2Command()); + db.execSQL(LokiAPIDatabase.getCreateReceivedMessageHashValuesTable2Command()); + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupRatchetTableCommand()); + db.execSQL(SharedSenderKeysDatabase.getCreateClosedGroupPrivateKeyTableCommand()); } db.setTransactionSuccessful(); diff --git a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java index 79a875c817..520761b536 100644 --- a/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java +++ b/src/org/thoughtcrime/securesms/dependencies/SignalCommunicationModule.java @@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceOpenGroupUpdateJob; +import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceOpenGroupUpdateJob; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.push.MessageSenderEventListener; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; @@ -153,6 +153,7 @@ public class SignalCommunicationModule { Optional.of(new MessageSenderEventListener(context)), TextSecurePreferences.getLocalNumber(context), DatabaseFactory.getLokiAPIDatabase(context), + DatabaseFactory.getSSKDatabase(context), DatabaseFactory.getLokiThreadDatabase(context), DatabaseFactory.getLokiMessageDatabase(context), DatabaseFactory.getLokiPreKeyBundleDatabase(context), diff --git a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java index 849cc67a32..f7c9bb9de6 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java +++ b/src/org/thoughtcrime/securesms/groups/GroupMessageProcessor.java @@ -97,11 +97,6 @@ public class GroupMessageProcessor { } } - // Loki - Ignore message if needed - if (ClosedGroupsProtocol.shouldIgnoreGroupCreatedMessage(context, group)) { - return null; - } - // Loki - Parse admins if (group.getAdmins().isPresent()) { for (String admin : group.getAdmins().get()) { diff --git a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java index c4aa802414..04bb55eca4 100644 --- a/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java +++ b/src/org/thoughtcrime/securesms/jobmanager/migration/WorkManagerFactoryMappings.java @@ -44,7 +44,8 @@ import org.thoughtcrime.securesms.jobs.SmsSentJob; import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.jobs.TypingSendJob; import org.thoughtcrime.securesms.jobs.UpdateApkJob; -import org.thoughtcrime.securesms.loki.protocol.PushNullMessageSendJob; +import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob; +import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob; import java.util.HashMap; import java.util.Map; @@ -75,7 +76,8 @@ public class WorkManagerFactoryMappings { put(PushMediaSendJob.class.getName(), PushMediaSendJob.KEY); put(PushNotificationReceiveJob.class.getName(), PushNotificationReceiveJob.KEY); put(PushTextSendJob.class.getName(), PushTextSendJob.KEY); - put(PushNullMessageSendJob.class.getName(), PushNullMessageSendJob.KEY); + put(NullMessageSendJob.class.getName(), NullMessageSendJob.KEY); + put(ClosedGroupUpdateMessageSendJob.class.getName(), ClosedGroupUpdateMessageSendJob.KEY); put(RefreshAttributesJob.class.getName(), RefreshAttributesJob.KEY); put(RefreshPreKeysJob.class.getName(), RefreshPreKeysJob.KEY); put(RefreshUnidentifiedDeliveryAbilityJob.class.getName(), RefreshUnidentifiedDeliveryAbilityJob.KEY); diff --git a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index be5aef3962..41b31fd332 100644 --- a/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/src/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -13,9 +13,10 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint; import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver; -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceOpenGroupUpdateJob; -import org.thoughtcrime.securesms.loki.protocol.PushNullMessageSendJob; -import org.thoughtcrime.securesms.loki.protocol.PushSessionRequestMessageSendJob; +import org.thoughtcrime.securesms.loki.protocol.ClosedGroupUpdateMessageSendJob; +import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceOpenGroupUpdateJob; +import org.thoughtcrime.securesms.loki.protocol.NullMessageSendJob; +import org.thoughtcrime.securesms.loki.protocol.SessionRequestMessageSendJob; import java.util.Arrays; import java.util.HashMap; @@ -51,7 +52,8 @@ public final class JobManagerFactories { put(PushMediaSendJob.KEY, new PushMediaSendJob.Factory()); put(PushNotificationReceiveJob.KEY, new PushNotificationReceiveJob.Factory()); put(PushTextSendJob.KEY, new PushTextSendJob.Factory()); - put(PushNullMessageSendJob.KEY, new PushNullMessageSendJob.Factory()); + put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory()); + put(ClosedGroupUpdateMessageSendJob.KEY, new ClosedGroupUpdateMessageSendJob.Factory()); put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory()); put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory()); put(RefreshUnidentifiedDeliveryAbilityJob.KEY, new RefreshUnidentifiedDeliveryAbilityJob.Factory()); @@ -72,7 +74,7 @@ public final class JobManagerFactories { put(TrimThreadJob.KEY, new TrimThreadJob.Factory()); put(TypingSendJob.KEY, new TypingSendJob.Factory()); put(UpdateApkJob.KEY, new UpdateApkJob.Factory()); - put(PushSessionRequestMessageSendJob.KEY, new PushSessionRequestMessageSendJob.Factory()); + put(SessionRequestMessageSendJob.KEY, new SessionRequestMessageSendJob.Factory()); put(MultiDeviceOpenGroupUpdateJob.KEY, new MultiDeviceOpenGroupUpdateJob.Factory()); }}; } diff --git a/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java b/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java index 1a012b7270..184c515c12 100644 --- a/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MultiDeviceContactUpdateJob.java @@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol; +import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.IdentityKey; diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java index 6183467232..cc6445c532 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java @@ -66,11 +66,11 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.activities.HomeActivity; import org.thoughtcrime.securesms.loki.database.LokiMessageDatabase; import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol; -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol; +import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation; -import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol; +import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol; import org.thoughtcrime.securesms.loki.utilities.MentionManagerUtilities; import org.thoughtcrime.securesms.loki.utilities.PromiseUtilities; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; @@ -127,6 +127,7 @@ import org.whispersystems.signalservice.loki.crypto.LokiServiceCipher; import org.whispersystems.signalservice.loki.protocol.mentions.MentionsManager; import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation; +import java.io.IOException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; @@ -255,7 +256,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { SignalProtocolStore axolotlStore = new SignalProtocolStoreImpl(context); SessionResetProtocol sessionResetProtocol = new SessionResetImplementation(context); SignalServiceAddress localAddress = new SignalServiceAddress(TextSecurePreferences.getLocalNumber(context)); - LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator()); + LokiServiceCipher cipher = new LokiServiceCipher(localAddress, axolotlStore, DatabaseFactory.getSSKDatabase(context), sessionResetProtocol, UnidentifiedAccessUtil.getCertificateValidator()); SignalServiceContent content = cipher.decrypt(envelope); @@ -278,6 +279,10 @@ public class PushDecryptJob extends BaseJob implements InjectableType { MultiDeviceProtocol.handleUnlinkingRequestIfNeeded(context, content); } else { + if (message.getClosedGroupUpdate().isPresent()) { + ClosedGroupsProtocol.handleSharedSenderKeysUpdate(context, message.getClosedGroupUpdate().get(), content.getSender()); + } + if (message.isEndSession()) { handleEndSessionMessage(content, smsMessageId); } else if (message.isGroupUpdate()) { @@ -298,7 +303,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { SessionMetaProtocol.handleProfileKeyUpdate(context, content); } - if (content.isNeedsReceipt()) { + if (content.isNeedsReceipt() && SessionMetaProtocol.shouldSendDeliveryReceipt(Address.fromSerialized(content.getSender()))) { handleNeedsDeliveryReceipt(content, message); } } @@ -369,6 +374,8 @@ public class PushDecryptJob extends BaseJob implements InjectableType { Log.w(TAG, e); } catch (SelfSendException e) { Log.i(TAG, "Dropping UD message from self."); + } catch (IOException e) { + Log.i(TAG, "IOException during message decryption."); } } @@ -1454,7 +1461,7 @@ public class PushDecryptJob extends BaseJob implements InjectableType { boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get()); boolean isLeaveMessage = message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() == SignalServiceGroup.Type.QUIT; - boolean shouldIgnoreContentMessage = ClosedGroupsProtocol.shouldIgnoreContentMessage(context, conversation, groupId.orNull(), content); + boolean shouldIgnoreContentMessage = ClosedGroupsProtocol.shouldIgnoreContentMessage(context, conversation.getAddress(), groupId.orNull(), content.getSender()); return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage) || (isContentMessage && shouldIgnoreContentMessage); } else { return sender.isBlocked(); diff --git a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 0a98ebddc0..f240ec24a2 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -154,7 +154,7 @@ public class PushGroupSendJob extends PushSendJob implements InjectableType { if (filterAddress != null) targets = Collections.singletonList(Address.fromSerialized(filterAddress)); else if (!existingNetworkFailures.isEmpty()) targets = Stream.of(existingNetworkFailures).map(NetworkFailure::getAddress).toList(); - else targets = ClosedGroupsProtocol.getDestinations(message.getRecipient().getAddress().toGroupString(), context).get(); + else targets = ClosedGroupsProtocol.getMessageDestinations(context, message.getRecipient().getAddress().toGroupString()); List results = deliver(message, targets); List networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList(); diff --git a/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java b/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java index 63fc0f65d5..34570b169d 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushReceivedJob.java @@ -36,7 +36,8 @@ public abstract class PushReceivedJob extends BaseJob { if (envelope.isReceipt()) { handleReceipt(envelope); - } else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() || envelope.isUnidentifiedSender() || envelope.isFallbackMessage()) { + } else if (envelope.isPreKeySignalMessage() || envelope.isSignalMessage() + || envelope.isUnidentifiedSender() || envelope.isFallbackMessage() || envelope.isClosedGroupCiphertext()) { handleMessage(envelope, isPushNotification); } else { Log.w(TAG, "Received envelope of unknown type: " + envelope.getType()); diff --git a/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java b/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java index b008edb95e..fa589cd5ac 100644 --- a/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java +++ b/src/org/thoughtcrime/securesms/jobs/SendDeliveryReceiptJob.java @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; @@ -83,6 +84,8 @@ public class SendDeliveryReceiptJob extends BaseJob implements InjectableType { Collections.singletonList(messageId), timestamp); + if (!SessionMetaProtocol.shouldSendDeliveryReceipt(Address.fromSerialized(address))) { return; } + messageSender.sendReceipt(remoteAddress, UnidentifiedAccessUtil.getAccessFor(context, Recipient.from(context, Address.fromSerialized(address), false)), receiptMessage); diff --git a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt index b56b75d4fd..f4efe23150 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/CreateClosedGroupActivity.kt @@ -18,8 +18,10 @@ import network.loki.messenger.R import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.conversation.ConversationActivity import org.thoughtcrime.securesms.database.Address +import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.ThreadDatabase import org.thoughtcrime.securesms.groups.GroupManager +import org.thoughtcrime.securesms.loki.protocol.ClosedGroupsProtocol import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.TextSecurePreferences @@ -93,6 +95,35 @@ class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderM } private fun createClosedGroup() { + if (ClosedGroupsProtocol.isSharedSenderKeysEnabled) { + createSSKBasedClosedGroup() + } else { + createLegacyClosedGroup() + } + } + + private fun createSSKBasedClosedGroup() { + val name = nameEditText.text.trim() + if (name.isEmpty()) { + return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show() + } + if (name.length >= 64) { + return Toast.makeText(this, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show() + } + val selectedMembers = this.selectContactsAdapter.selectedMembers + if (selectedMembers.count() < 2) { + return Toast.makeText(this, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show() + } + if (selectedMembers.count() > 49) { // Minus one because we're going to include self later + return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show() + } + val userPublicKey = TextSecurePreferences.getLocalNumber(this) + val groupID = ClosedGroupsProtocol.createClosedGroup(this, name.toString(), selectedMembers + setOf( userPublicKey )) + val threadID = DatabaseFactory.getThreadDatabase(this).getThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false)) + openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false)) + } + + private fun createLegacyClosedGroup() { val name = nameEditText.text.trim() if (name.isEmpty()) { return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show() diff --git a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index b65938afee..57cbc479d0 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegat import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.util.GroupUtil import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI @@ -49,6 +50,7 @@ import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol import org.whispersystems.signalservice.loki.protocol.sessionmanagement.SessionManagementProtocol import org.whispersystems.signalservice.loki.protocol.shelved.syncmessages.SyncMessagesProtocol +import org.whispersystems.signalservice.loki.utilities.toHexString class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate { private lateinit var glide: GlideRequests @@ -154,6 +156,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe val apiDB = DatabaseFactory.getLokiAPIDatabase(this) val threadDB = DatabaseFactory.getLokiThreadDatabase(this) val userDB = DatabaseFactory.getLokiUserDatabase(this) + val sskDatabase = DatabaseFactory.getSSKDatabase(this) val userPublicKey = TextSecurePreferences.getLocalNumber(this) val sessionResetImpl = SessionResetImplementation(this) if (userPublicKey != null) { @@ -162,7 +165,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe SyncMessagesProtocol.configureIfNeeded(apiDB, userPublicKey) application.publicChatManager.startPollersIfNeeded() } - SessionManagementProtocol.configureIfNeeded(sessionResetImpl, threadDB, application) + SessionManagementProtocol.configureIfNeeded(sessionResetImpl, sskDatabase, application) MultiDeviceProtocol.configureIfNeeded(apiDB) IP2Country.configureIfNeeded(this) // Preload device links to make message sending quicker @@ -332,7 +335,11 @@ class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListe val isClosedGroup = recipient.address.isClosedGroup // Send a leave group message if this is an active closed group if (isClosedGroup && DatabaseFactory.getGroupDatabase(this).isActive(recipient.address.toGroupString())) { - if (!ClosedGroupsProtocol.leaveGroup(this, recipient)) { + val groupPublicKey = GroupUtil.getDecodedId(recipient.address.toString()).toHexString() + val isSSKBasedClosedGroup = DatabaseFactory.getSSKDatabase(this).isSSKBasedClosedGroup(groupPublicKey) + if (isSSKBasedClosedGroup) { + ClosedGroupsProtocol.leave(this, groupPublicKey) + } else if (!ClosedGroupsProtocol.leaveLegacyGroup(this, recipient)) { Toast.makeText(this, R.string.activity_home_leaving_group_failed_message, Toast.LENGTH_LONG).show() return@setPositiveButton } diff --git a/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index fcf3e9e4e7..11f3a79b06 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.loki.fragments.ScanQRCodeWrapperFragmentDelegate -import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol +import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol import org.thoughtcrime.securesms.loki.utilities.OpenGroupUtilities class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { diff --git a/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt index 8cd45d95f0..d19a0002ab 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/LandingActivity.kt @@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.logging.Log import org.thoughtcrime.securesms.loki.dialogs.LinkDeviceSlaveModeDialog import org.thoughtcrime.securesms.loki.dialogs.LinkDeviceSlaveModeDialogDelegate import org.thoughtcrime.securesms.loki.protocol.SessionResetImplementation -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol +import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.utilities.push import org.thoughtcrime.securesms.loki.utilities.setUpActionBarSessionLogo import org.thoughtcrime.securesms.loki.utilities.show @@ -108,12 +108,13 @@ class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelega val apiDB = DatabaseFactory.getLokiAPIDatabase(this) val threadDB = DatabaseFactory.getLokiThreadDatabase(this) val userDB = DatabaseFactory.getLokiUserDatabase(this) + val sskDatabase = DatabaseFactory.getSSKDatabase(this) val userPublicKey = TextSecurePreferences.getLocalNumber(this) val sessionResetImpl = SessionResetImplementation(this) MentionsManager.configureIfNeeded(userPublicKey, threadDB, userDB) SessionMetaProtocol.configureIfNeeded(apiDB, userPublicKey) org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol.configureIfNeeded(apiDB) - SessionManagementProtocol.configureIfNeeded(sessionResetImpl, threadDB, application) + SessionManagementProtocol.configureIfNeeded(sessionResetImpl, sskDatabase, application) SyncMessagesProtocol.configureIfNeeded(apiDB, userPublicKey) application.setUpP2PAPIIfNeeded() application.setUpStorageAPIIfNeeded() diff --git a/src/org/thoughtcrime/securesms/loki/activities/LinkedDevicesActivity.kt b/src/org/thoughtcrime/securesms/loki/activities/LinkedDevicesActivity.kt index 041e8a8efa..2c4e9dabde 100644 --- a/src/org/thoughtcrime/securesms/loki/activities/LinkedDevicesActivity.kt +++ b/src/org/thoughtcrime/securesms/loki/activities/LinkedDevicesActivity.kt @@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.devicelist.Device import org.thoughtcrime.securesms.logging.Log import org.thoughtcrime.securesms.loki.dialogs.* -import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol +import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol import org.thoughtcrime.securesms.loki.utilities.recipient import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage diff --git a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt index 46ffe29abb..9cd363823e 100644 --- a/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt +++ b/src/org/thoughtcrime/securesms/loki/api/BackgroundPollWorker.kt @@ -36,7 +36,7 @@ class BackgroundPollWorker : PersistentAlarmManagerListener() { val applicationContext = context.applicationContext as ApplicationContext val broadcaster = applicationContext.broadcaster SnodeAPI.configureIfNeeded(userPublicKey, lokiAPIDatabase, broadcaster) - SnodeAPI.shared.getMessages().map { messages -> + SnodeAPI.shared.getMessages(userPublicKey).map { messages -> messages.forEach { PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false) } diff --git a/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt new file mode 100644 index 0000000000..7109640bba --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/api/ClosedGroupPoller.kt @@ -0,0 +1,81 @@ +package org.thoughtcrime.securesms.loki.api + +import android.content.Context +import android.os.Handler +import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map +import org.thoughtcrime.securesms.jobs.PushContentReceiveJob +import org.thoughtcrime.securesms.logging.Log +import org.thoughtcrime.securesms.loki.database.SharedSenderKeysDatabase +import org.thoughtcrime.securesms.loki.utilities.successBackground +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope +import org.whispersystems.signalservice.loki.api.SnodeAPI +import org.whispersystems.signalservice.loki.api.SwarmAPI +import org.whispersystems.signalservice.loki.utilities.getRandomElementOrNull + +class ClosedGroupPoller private constructor(private val context: Context, private val database: SharedSenderKeysDatabase) { + private var isPolling = false + private val handler = Handler() + + private val task = object : Runnable { + + override fun run() { + poll() + handler.postDelayed(this, ClosedGroupPoller.pollInterval) + } + } + + // region Settings + companion object { + private val pollInterval: Long = 2 * 1000 + + public lateinit var shared: ClosedGroupPoller + + public fun configureIfNeeded(context: Context, sskDatabase: SharedSenderKeysDatabase) { + if (::shared.isInitialized) { return; } + shared = ClosedGroupPoller(context, sskDatabase) + } + } + // endregion + + // region Error + public class InsufficientSnodesException() : Exception("No snodes left to poll.") + public class PollingCanceledException() : Exception("Polling canceled.") + // endregion + + // region Public API + public fun startIfNeeded() { + if (isPolling) { return } + isPolling = true + task.run() + } + + public fun stopIfNeeded() { + isPolling = false + handler.removeCallbacks(task) + } + // endregion + + // region Private API + private fun poll() { + if (!isPolling) { return } + val publicKeys = database.getAllClosedGroupPublicKeys() + publicKeys.forEach { publicKey -> + SwarmAPI.shared.getSwarm(publicKey).bind { swarm -> + val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure + if (!isPolling) { throw PollingCanceledException() } + SnodeAPI.shared.getRawMessages(snode, publicKey).map {SnodeAPI.shared.parseRawMessagesResponse(it, snode, publicKey) } + }.successBackground { messages -> + if (messages.isNotEmpty()) { + Log.d("Loki", "Received ${messages.count()} new message(s) in closed group with public key: $publicKey.") + } + messages.forEach { + PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it), false) + } + }.fail { + Log.d("Loki", "Polling failed for closed group with public key: $publicKey due to error: $it.") + } + } + } + // endregion +} diff --git a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt index fefcfcc211..58728ed199 100644 --- a/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/database/LokiAPIDatabase.kt @@ -6,7 +6,6 @@ import android.util.Log import org.thoughtcrime.securesms.database.Database import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.loki.utilities.* -import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.loki.api.Snode import org.whispersystems.signalservice.loki.database.LokiAPIDatabaseProtocol @@ -14,78 +13,73 @@ import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.Device class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol { - private val userPublicKey get() = TextSecurePreferences.getLocalNumber(context) - companion object { // Shared private val publicKey = "public_key" private val timestamp = "timestamp" - // Snode pool cache - private val snodePoolCache = "loki_snode_pool_cache" + private val snode = "snode" + // Snode pool + private val snodePoolTable = "loki_snode_pool_cache" private val dummyKey = "dummy_key" private val snodePool = "snode_pool_key" - @JvmStatic val createSnodePoolCacheCommand = "CREATE TABLE $snodePoolCache ($dummyKey TEXT PRIMARY KEY, $snodePool TEXT);" - // Onion request path cache - private val onionRequestPathCache = "loki_path_cache" + @JvmStatic val createSnodePoolTableCommand = "CREATE TABLE $snodePoolTable ($dummyKey TEXT PRIMARY KEY, $snodePool TEXT);" + // Onion request paths + private val onionRequestPathTable = "loki_path_cache" private val indexPath = "index_path" - private val snode = "snode" - @JvmStatic val createOnionRequestPathCacheCommand = "CREATE TABLE $onionRequestPathCache ($indexPath TEXT PRIMARY KEY, $snode TEXT);" - // Swarm cache - private val swarmCache = "loki_api_swarm_cache" + @JvmStatic val createOnionRequestPathTableCommand = "CREATE TABLE $onionRequestPathTable ($indexPath TEXT PRIMARY KEY, $snode TEXT);" + // Swarms + private val swarmTable = "loki_api_swarm_cache" private val swarmPublicKey = "hex_encoded_public_key" private val swarm = "swarm" - @JvmStatic val createSwarmCacheCommand = "CREATE TABLE $swarmCache ($swarmPublicKey TEXT PRIMARY KEY, $swarm TEXT);" - // Last message hash value cache - private val lastMessageHashValueCache = "loki_api_last_message_hash_value_cache" - private val target = "target" + @JvmStatic val createSwarmTableCommand = "CREATE TABLE $swarmTable ($swarmPublicKey TEXT PRIMARY KEY, $swarm TEXT);" + // Last message hash values + private val lastMessageHashValueTable2 = "last_message_hash_value_table" private val lastMessageHashValue = "last_message_hash_value" - @JvmStatic val createLastMessageHashValueCacheCommand = "CREATE TABLE $lastMessageHashValueCache ($target TEXT PRIMARY KEY, $lastMessageHashValue TEXT);" - // Received message hash values cache - private val receivedMessageHashValuesCache = "loki_api_received_message_hash_values_cache" - private val userID = "user_id" + @JvmStatic val createLastMessageHashValueTable2Command + = "CREATE TABLE $lastMessageHashValueTable2 ($snode TEXT, $publicKey TEXT, $lastMessageHashValue TEXT, PRIMARY KEY ($snode, $publicKey));" + // Received message hash values + private val receivedMessageHashValuesTable2 = "received_message_hash_values_table" private val receivedMessageHashValues = "received_message_hash_values" - @JvmStatic val createReceivedMessageHashValuesCacheCommand = "CREATE TABLE $receivedMessageHashValuesCache ($userID TEXT PRIMARY KEY, $receivedMessageHashValues TEXT);" - // Open group auth token cache - private val openGroupAuthTokenCache = "loki_api_group_chat_auth_token_database" + @JvmStatic val createReceivedMessageHashValuesTable2Command + = "CREATE TABLE $receivedMessageHashValuesTable2 ($snode STRING, $publicKey STRING, $receivedMessageHashValues TEXT, PRIMARY KEY ($snode, $publicKey));" + // Open group auth tokens + private val openGroupAuthTokenTable = "loki_api_group_chat_auth_token_database" private val server = "server" private val token = "token" - @JvmStatic val createOpenGroupAuthTokenCacheCommand = "CREATE TABLE $openGroupAuthTokenCache ($server TEXT PRIMARY KEY, $token TEXT);" - // Last message server ID cache - private val lastMessageServerIDCache = "loki_api_last_message_server_id_cache" - private val lastMessageServerIDCacheIndex = "loki_api_last_message_server_id_cache_index" + @JvmStatic val createOpenGroupAuthTokenTableCommand = "CREATE TABLE $openGroupAuthTokenTable ($server TEXT PRIMARY KEY, $token TEXT);" + // Last message server IDs + private val lastMessageServerIDTable = "loki_api_last_message_server_id_cache" + private val lastMessageServerIDTableIndex = "loki_api_last_message_server_id_cache_index" private val lastMessageServerID = "last_message_server_id" - @JvmStatic val createLastMessageServerIDCacheCommand = "CREATE TABLE $lastMessageServerIDCache ($lastMessageServerIDCacheIndex STRING PRIMARY KEY, $lastMessageServerID INTEGER DEFAULT 0);" - // Last deletion server ID cache - private val lastDeletionServerIDCache = "loki_api_last_deletion_server_id_cache" - private val lastDeletionServerIDCacheIndex = "loki_api_last_deletion_server_id_cache_index" + @JvmStatic val createLastMessageServerIDTableCommand = "CREATE TABLE $lastMessageServerIDTable ($lastMessageServerIDTableIndex STRING PRIMARY KEY, $lastMessageServerID INTEGER DEFAULT 0);" + // Last deletion server IDs + private val lastDeletionServerIDTable = "loki_api_last_deletion_server_id_cache" + private val lastDeletionServerIDTableIndex = "loki_api_last_deletion_server_id_cache_index" private val lastDeletionServerID = "last_deletion_server_id" - @JvmStatic val createLastDeletionServerIDCacheCommand = "CREATE TABLE $lastDeletionServerIDCache ($lastDeletionServerIDCacheIndex STRING PRIMARY KEY, $lastDeletionServerID INTEGER DEFAULT 0);" - // Device link cache + @JvmStatic val createLastDeletionServerIDTableCommand = "CREATE TABLE $lastDeletionServerIDTable ($lastDeletionServerIDTableIndex STRING PRIMARY KEY, $lastDeletionServerID INTEGER DEFAULT 0);" + // User counts + private val userCountTable = "loki_user_count_cache" + private val publicChatID = "public_chat_id" + private val userCount = "user_count" + @JvmStatic val createUserCountTableCommand = "CREATE TABLE $userCountTable ($publicChatID STRING PRIMARY KEY, $userCount INTEGER DEFAULT 0);" + // Session request sent timestamps + private val sessionRequestSentTimestampTable = "session_request_sent_timestamp_cache" + @JvmStatic val createSessionRequestSentTimestampTableCommand = "CREATE TABLE $sessionRequestSentTimestampTable ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);" + // Session request processed timestamp cache + private val sessionRequestProcessedTimestampTable = "session_request_processed_timestamp_cache" + @JvmStatic val createSessionRequestProcessedTimestampTableCommand = "CREATE TABLE $sessionRequestProcessedTimestampTable ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);" + // Open group public keys + private val openGroupPublicKeyTable = "open_group_public_keys" + @JvmStatic val createOpenGroupPublicKeyTableCommand = "CREATE TABLE $openGroupPublicKeyTable ($server STRING PRIMARY KEY, $publicKey INTEGER DEFAULT 0);" + + // region Deprecated private val deviceLinkCache = "loki_pairing_authorisation_cache" private val masterPublicKey = "primary_device" private val slavePublicKey = "secondary_device" private val requestSignature = "request_signature" private val authorizationSignature = "grant_signature" - @JvmStatic val createDeviceLinkCacheCommand = "CREATE TABLE $deviceLinkCache ($masterPublicKey TEXT, $slavePublicKey TEXT, " + - "$requestSignature TEXT NULLABLE DEFAULT NULL, $authorizationSignature TEXT NULLABLE DEFAULT NULL, PRIMARY KEY ($masterPublicKey, $slavePublicKey));" - // User count cache - private val userCountCache = "loki_user_count_cache" - private val publicChatID = "public_chat_id" - private val userCount = "user_count" - @JvmStatic val createUserCountCacheCommand = "CREATE TABLE $userCountCache ($publicChatID STRING PRIMARY KEY, $userCount INTEGER DEFAULT 0);" - // Session request sent timestamp cache - private val sessionRequestSentTimestampCache = "session_request_sent_timestamp_cache" - @JvmStatic val createSessionRequestSentTimestampCacheCommand = "CREATE TABLE $sessionRequestSentTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);" - // Session request processed timestamp cache - private val sessionRequestProcessedTimestampCache = "session_request_processed_timestamp_cache" - @JvmStatic val createSessionRequestProcessedTimestampCacheCommand = "CREATE TABLE $sessionRequestProcessedTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp INTEGER DEFAULT 0);" - // Open group public keys - private val openGroupPublicKeyDB = "open_group_public_keys" - @JvmStatic val createOpenGroupPublicKeyDBCommand = "CREATE TABLE $openGroupPublicKeyDB ($server STRING PRIMARY KEY, $publicKey INTEGER DEFAULT 0);" - - - - // region Deprecated + @JvmStatic val createDeviceLinkCacheCommand = "CREATE TABLE $deviceLinkCache ($masterPublicKey STRING, $slavePublicKey STRING, " + + "$requestSignature STRING NULLABLE DEFAULT NULL, $authorizationSignature STRING NULLABLE DEFAULT NULL, PRIMARY KEY ($masterPublicKey, $slavePublicKey));" private val sessionRequestTimestampCache = "session_request_timestamp_cache" @JvmStatic val createSessionRequestTimestampCacheCommand = "CREATE TABLE $sessionRequestTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp STRING);" // endregion @@ -93,7 +87,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun getSnodePool(): Set { val database = databaseHelper.readableDatabase - return database.get(snodePoolCache, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor -> + return database.get(snodePoolTable, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor -> val snodePoolAsString = cursor.getString(cursor.getColumnIndexOrThrow(snodePool)) snodePoolAsString.split(", ").mapNotNull { snodeAsString -> val components = snodeAsString.split("-") @@ -116,14 +110,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } string } - val row = wrap(mapOf(Companion.dummyKey to "dummy_key", snodePool to snodePoolAsString)) - database.insertOrUpdate(snodePoolCache, row, "${Companion.dummyKey} = ?", wrap("dummy_key")) + val row = wrap(mapOf( Companion.dummyKey to "dummy_key", snodePool to snodePoolAsString )) + database.insertOrUpdate(snodePoolTable, row, "${Companion.dummyKey} = ?", wrap("dummy_key")) } override fun getOnionRequestPaths(): List> { val database = databaseHelper.readableDatabase fun get(indexPath: String): Snode? { - return database.get(onionRequestPathCache, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor -> + return database.get(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor -> val snodeAsString = cursor.getString(cursor.getColumnIndexOrThrow(snode)) val components = snodeAsString.split("-") val address = components[0] @@ -146,7 +140,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( fun clearOnionRequestPaths() { val database = databaseHelper.writableDatabase fun delete(indexPath: String) { - database.delete(onionRequestPathCache, "${Companion.indexPath} = ?", wrap(indexPath)) + database.delete(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) } delete("0-0"); delete("0-1") delete("0-2"); delete("1-0") @@ -154,21 +148,21 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } override fun setOnionRequestPaths(newValue: List>) { - // FIXME: This is a bit of a dirty approach that assumes 2 paths of length 3 each. We should do better than this. + // TODO: Make this work with arbitrary paths if (newValue.count() != 2) { return } val path0 = newValue[0] val path1 = newValue[1] if (path0.count() != 3 || path1.count() != 3) { return } Log.d("Loki", "Persisting onion request paths to database.") val database = databaseHelper.writableDatabase - fun set(indexPath: String ,snode: Snode) { + fun set(indexPath: String, snode: Snode) { var snodeAsString = "${snode.address}-${snode.port}" val keySet = snode.publicKeySet if (keySet != null) { snodeAsString += "-${keySet.ed25519Key}-${keySet.x25519Key}" } - val row = wrap(mapOf(Companion.indexPath to indexPath, Companion.snode to snodeAsString)) - database.insertOrUpdate(onionRequestPathCache, row, "${Companion.indexPath} = ?", wrap(indexPath)) + val row = wrap(mapOf( Companion.indexPath to indexPath, Companion.snode to snodeAsString )) + database.insertOrUpdate(onionRequestPathTable, row, "${Companion.indexPath} = ?", wrap(indexPath)) } set("0-0", path0[0]); set("0-1", path0[1]) set("0-2", path0[2]); set("1-0", path1[0]) @@ -177,7 +171,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun getSwarm(publicKey: String): Set? { val database = databaseHelper.readableDatabase - return database.get(swarmCache, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor -> + return database.get(swarmTable, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor -> val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm)) swarmAsString.split(", ").mapNotNull { targetAsString -> val components = targetAsString.split("-") @@ -200,41 +194,45 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( } string } - val row = wrap(mapOf(Companion.swarmPublicKey to publicKey, swarm to swarmAsString)) - database.insertOrUpdate(swarmCache, row, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) + val row = wrap(mapOf( Companion.swarmPublicKey to publicKey, swarm to swarmAsString )) + database.insertOrUpdate(swarmTable, row, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) } - override fun getLastMessageHashValue(snode: Snode): String? { + override fun getLastMessageHashValue(snode: Snode, publicKey: String): String? { val database = databaseHelper.readableDatabase - return database.get(lastMessageHashValueCache, "${Companion.target} = ?", wrap(snode.address)) { cursor -> + val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ?" + return database.get(lastMessageHashValueTable2, query, arrayOf( snode.toString(), publicKey )) { cursor -> cursor.getString(cursor.getColumnIndexOrThrow(lastMessageHashValue)) } } - override fun setLastMessageHashValue(snode: Snode, newValue: String) { + override fun setLastMessageHashValue(snode: Snode, publicKey: String, newValue: String) { val database = databaseHelper.writableDatabase - val row = wrap(mapOf(Companion.target to snode.address, lastMessageHashValue to newValue)) - database.insertOrUpdate(lastMessageHashValueCache, row, "${Companion.target} = ?", wrap(snode.address)) + val row = wrap(mapOf( Companion.snode to snode.toString(), Companion.publicKey to publicKey, lastMessageHashValue to newValue )) + val query = "${Companion.snode} = ? AND ${Companion.publicKey} = ?" + database.insertOrUpdate(lastMessageHashValueTable2, row, query, arrayOf( snode.toString(), publicKey )) } - override fun getReceivedMessageHashValues(): Set? { + override fun getReceivedMessageHashValues(publicKey: String): Set? { val database = databaseHelper.readableDatabase - return database.get(receivedMessageHashValuesCache, "$userID = ?", wrap(userPublicKey)) { cursor -> + val query = "$Companion.publicKey = ?" + return database.get(receivedMessageHashValuesTable2, query, arrayOf( publicKey )) { cursor -> val receivedMessageHashValuesAsString = cursor.getString(cursor.getColumnIndexOrThrow(receivedMessageHashValues)) - receivedMessageHashValuesAsString.split(", ").toSet() + receivedMessageHashValuesAsString.split("-").toSet() } } - override fun setReceivedMessageHashValues(newValue: Set) { + override fun setReceivedMessageHashValues(publicKey: String, newValue: Set) { val database = databaseHelper.writableDatabase - val receivedMessageHashValuesAsString = newValue.joinToString(", ") - val row = wrap(mapOf(userID to userPublicKey, receivedMessageHashValues to receivedMessageHashValuesAsString)) - database.insertOrUpdate(receivedMessageHashValuesCache, row, "$userID = ?", wrap(userPublicKey)) + val receivedMessageHashValuesAsString = newValue.joinToString("-") + val row = wrap(mapOf( Companion.publicKey to publicKey, receivedMessageHashValues to receivedMessageHashValuesAsString )) + val query = "$Companion.publicKey = ?" + database.insertOrUpdate(receivedMessageHashValuesTable2, row, query, arrayOf( publicKey )) } override fun getAuthToken(server: String): String? { val database = databaseHelper.readableDatabase - return database.get(openGroupAuthTokenCache, "${Companion.server} = ?", wrap(server)) { cursor -> + return database.get(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) { cursor -> cursor.getString(cursor.getColumnIndexOrThrow(token)) } } @@ -242,17 +240,17 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setAuthToken(server: String, newValue: String?) { val database = databaseHelper.writableDatabase if (newValue != null) { - val row = wrap(mapOf(Companion.server to server, token to newValue)) - database.insertOrUpdate(openGroupAuthTokenCache, row, "${Companion.server} = ?", wrap(server)) + val row = wrap(mapOf( Companion.server to server, token to newValue )) + database.insertOrUpdate(openGroupAuthTokenTable, row, "${Companion.server} = ?", wrap(server)) } else { - database.delete(openGroupAuthTokenCache, "${Companion.server} = ?", wrap(server)) + database.delete(openGroupAuthTokenTable, "${Companion.server} = ?", wrap(server)) } } override fun getLastMessageServerID(group: Long, server: String): Long? { val database = databaseHelper.readableDatabase val index = "$server.$group" - return database.get(lastMessageServerIDCache, "$lastMessageServerIDCacheIndex = ?", wrap(index)) { cursor -> + return database.get(lastMessageServerIDTable, "$lastMessageServerIDTableIndex = ?", wrap(index)) { cursor -> cursor.getInt(lastMessageServerID) }?.toLong() } @@ -260,20 +258,20 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setLastMessageServerID(group: Long, server: String, newValue: Long) { val database = databaseHelper.writableDatabase val index = "$server.$group" - val row = wrap(mapOf(lastMessageServerIDCacheIndex to index, lastMessageServerID to newValue.toString())) - database.insertOrUpdate(lastMessageServerIDCache, row, "$lastMessageServerIDCacheIndex = ?", wrap(index)) + val row = wrap(mapOf( lastMessageServerIDTableIndex to index, lastMessageServerID to newValue.toString() )) + database.insertOrUpdate(lastMessageServerIDTable, row, "$lastMessageServerIDTableIndex = ?", wrap(index)) } fun removeLastMessageServerID(group: Long, server: String) { val database = databaseHelper.writableDatabase val index = "$server.$group" - database.delete(lastMessageServerIDCache,"$lastMessageServerIDCacheIndex = ?", wrap(index)) + database.delete(lastMessageServerIDTable,"$lastMessageServerIDTableIndex = ?", wrap(index)) } override fun getLastDeletionServerID(group: Long, server: String): Long? { val database = databaseHelper.readableDatabase val index = "$server.$group" - return database.get(lastDeletionServerIDCache, "$lastDeletionServerIDCacheIndex = ?", wrap(index)) { cursor -> + return database.get(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index)) { cursor -> cursor.getInt(lastDeletionServerID) }?.toLong() } @@ -281,16 +279,71 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( override fun setLastDeletionServerID(group: Long, server: String, newValue: Long) { val database = databaseHelper.writableDatabase val index = "$server.$group" - val row = wrap(mapOf(lastDeletionServerIDCacheIndex to index, lastDeletionServerID to newValue.toString())) - database.insertOrUpdate(lastDeletionServerIDCache, row, "$lastDeletionServerIDCacheIndex = ?", wrap(index)) + val row = wrap(mapOf( lastDeletionServerIDTableIndex to index, lastDeletionServerID to newValue.toString() )) + database.insertOrUpdate(lastDeletionServerIDTable, row, "$lastDeletionServerIDTableIndex = ?", wrap(index)) } fun removeLastDeletionServerID(group: Long, server: String) { val database = databaseHelper.writableDatabase val index = "$server.$group" - database.delete(lastDeletionServerIDCache,"$lastDeletionServerIDCacheIndex = ?", wrap(index)) + database.delete(lastDeletionServerIDTable,"$lastDeletionServerIDTableIndex = ?", wrap(index)) } + fun getUserCount(group: Long, server: String): Int? { + val database = databaseHelper.readableDatabase + val index = "$server.$group" + return database.get(userCountTable, "$publicChatID = ?", wrap(index)) { cursor -> + cursor.getInt(userCount) + }?.toInt() + } + + override fun setUserCount(group: Long, server: String, newValue: Int) { + val database = databaseHelper.writableDatabase + val index = "$server.$group" + val row = wrap(mapOf( publicChatID to index, Companion.userCount to newValue.toString() )) + database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index)) + } + + override fun getSessionRequestSentTimestamp(publicKey: String): Long? { + val database = databaseHelper.readableDatabase + return database.get(sessionRequestSentTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor -> + cursor.getInt(LokiAPIDatabase.timestamp) + }?.toLong() + } + + override fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) { + val database = databaseHelper.writableDatabase + val row = wrap(mapOf( LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString() )) + database.insertOrUpdate(sessionRequestSentTimestampTable, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) + } + + override fun getSessionRequestProcessedTimestamp(publicKey: String): Long? { + val database = databaseHelper.readableDatabase + return database.get(sessionRequestProcessedTimestampTable, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor -> + cursor.getInt(LokiAPIDatabase.timestamp) + }?.toLong() + } + + override fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long) { + val database = databaseHelper.writableDatabase + val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString())) + database.insertOrUpdate(sessionRequestProcessedTimestampTable, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) + } + + override fun getOpenGroupPublicKey(server: String): String? { + val database = databaseHelper.readableDatabase + return database.get(openGroupPublicKeyTable, "${LokiAPIDatabase.server} = ?", wrap(server)) { cursor -> + cursor.getString(LokiAPIDatabase.publicKey) + } + } + + override fun setOpenGroupPublicKey(server: String, newValue: String) { + val database = databaseHelper.writableDatabase + val row = wrap(mapOf( LokiAPIDatabase.server to server, LokiAPIDatabase.publicKey to newValue )) + database.insertOrUpdate(openGroupPublicKeyTable, row, "${LokiAPIDatabase.server} = ?", wrap(server)) + } + + // region Deprecated override fun getDeviceLinks(publicKey: String): Set { return setOf() /* @@ -330,60 +383,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.delete(deviceLinkCache, "$masterPublicKey = ? OR $slavePublicKey = ?", arrayOf( deviceLink.masterPublicKey, deviceLink.slavePublicKey )) */ } - - fun getUserCount(group: Long, server: String): Int? { - val database = databaseHelper.readableDatabase - val index = "$server.$group" - return database.get(userCountCache, "$publicChatID = ?", wrap(index)) { cursor -> - cursor.getInt(userCount) - }?.toInt() - } - - override fun setUserCount(group: Long, server: String, newValue: Int) { - val database = databaseHelper.writableDatabase - val index = "$server.$group" - val row = wrap(mapOf(publicChatID to index, Companion.userCount to newValue.toString())) - database.insertOrUpdate(userCountCache, row, "$publicChatID = ?", wrap(index)) - } - - override fun getSessionRequestSentTimestamp(publicKey: String): Long? { - val database = databaseHelper.readableDatabase - return database.get(sessionRequestSentTimestampCache, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor -> - cursor.getInt(LokiAPIDatabase.timestamp) - }?.toLong() - } - - override fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) { - val database = databaseHelper.writableDatabase - val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString())) - database.insertOrUpdate(sessionRequestSentTimestampCache, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) - } - - override fun getSessionRequestProcessedTimestamp(publicKey: String): Long? { - val database = databaseHelper.readableDatabase - return database.get(sessionRequestProcessedTimestampCache, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) { cursor -> - cursor.getInt(LokiAPIDatabase.timestamp) - }?.toLong() - } - - override fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long) { - val database = databaseHelper.writableDatabase - val row = wrap(mapOf(LokiAPIDatabase.publicKey to publicKey, LokiAPIDatabase.timestamp to newValue.toString())) - database.insertOrUpdate(sessionRequestProcessedTimestampCache, row, "${LokiAPIDatabase.publicKey} = ?", wrap(publicKey)) - } - - override fun getOpenGroupPublicKey(server: String): String? { - val database = databaseHelper.readableDatabase - return database.get(openGroupPublicKeyDB, "${LokiAPIDatabase.server} = ?", wrap(server)) { cursor -> - cursor.getString(LokiAPIDatabase.publicKey) - } - } - - override fun setOpenGroupPublicKey(server: String, newValue: String) { - val database = databaseHelper.writableDatabase - val row = wrap(mapOf(LokiAPIDatabase.server to server, LokiAPIDatabase.publicKey to newValue)) - database.insertOrUpdate(openGroupPublicKeyDB, row, "${LokiAPIDatabase.server} = ?", wrap(server)) - } + // endregion } // region Convenience diff --git a/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt new file mode 100644 index 0000000000..23afeeaca0 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/database/SharedSenderKeysDatabase.kt @@ -0,0 +1,115 @@ +package org.thoughtcrime.securesms.loki.database + +import android.content.ContentValues +import android.content.Context +import org.thoughtcrime.securesms.database.Database +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.loki.utilities.* +import org.thoughtcrime.securesms.util.Hex +import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet +import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey +import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol +import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation + +class SharedSenderKeysDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), SharedSenderKeysDatabaseProtocol { + + companion object { + // Shared + private val closedGroupPublicKey = "closed_group_public_key" + // Ratchets + private val closedGroupRatchetTable = "closed_group_ratchet_table" + private val senderPublicKey = "sender_public_key" + private val chainKey = "chain_key" + private val keyIndex = "key_index" + private val messageKeys = "message_keys" + @JvmStatic val createClosedGroupRatchetTableCommand + = "CREATE TABLE $closedGroupRatchetTable ($closedGroupPublicKey STRING, $senderPublicKey STRING, $chainKey STRING, " + + "$keyIndex INTEGER DEFAULT 0, $messageKeys TEXT, PRIMARY KEY ($closedGroupPublicKey, $senderPublicKey));" + // Private keys + private val closedGroupPrivateKeyTable = "closed_group_private_key_table" + private val closedGroupPrivateKey = "closed_group_private_key" + @JvmStatic val createClosedGroupPrivateKeyTableCommand + = "CREATE TABLE $closedGroupPrivateKeyTable ($closedGroupPublicKey STRING PRIMARY KEY, $closedGroupPrivateKey STRING);" + } + + // region Ratchets & Sender Keys + override fun getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String): ClosedGroupRatchet? { + val database = databaseHelper.readableDatabase + val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" + return database.get(closedGroupRatchetTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> + val chainKey = cursor.getString(Companion.chainKey) + val keyIndex = cursor.getInt(Companion.keyIndex) + val messageKeys = cursor.getString(Companion.messageKeys).split("-") + ClosedGroupRatchet(chainKey, keyIndex, messageKeys) + } + } + + override fun setClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet) { + val database = databaseHelper.writableDatabase + val values = ContentValues() + values.put(Companion.closedGroupPublicKey, groupPublicKey) + values.put(Companion.senderPublicKey, senderPublicKey) + values.put(Companion.chainKey, ratchet.chainKey) + values.put(Companion.keyIndex, ratchet.keyIndex) + values.put(Companion.messageKeys, ratchet.messageKeys.joinToString("-")) + val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" + database.insertOrUpdate(closedGroupRatchetTable, values, query, arrayOf( groupPublicKey, senderPublicKey )) + } + + override fun removeAllClosedGroupRatchets(groupPublicKey: String) { + val database = databaseHelper.writableDatabase + database.delete(closedGroupRatchetTable, null, null) + } + + override fun getAllClosedGroupSenderKeys(groupPublicKey: String): Set { + val database = databaseHelper.readableDatabase + val query = "${Companion.closedGroupPublicKey} = ? AND ${Companion.senderPublicKey} = ?" + return database.getAll(closedGroupRatchetTable, query, arrayOf( groupPublicKey, senderPublicKey )) { cursor -> + val chainKey = cursor.getString(Companion.chainKey) + val keyIndex = cursor.getInt(Companion.keyIndex) + val senderPublicKey = cursor.getString(Companion.senderPublicKey) + ClosedGroupSenderKey(Hex.fromStringCondensed(chainKey), keyIndex, Hex.fromStringCondensed(senderPublicKey)) + }.toSet() + } + // endregion + + // region Public & Private Keys + override fun getClosedGroupPrivateKey(groupPublicKey: String): String? { + val database = databaseHelper.readableDatabase + val query = "${Companion.closedGroupPublicKey} = ?" + return database.get(closedGroupPrivateKeyTable, query, arrayOf( groupPublicKey )) { cursor -> + cursor.getString(Companion.closedGroupPrivateKey) + } + } + + override fun setClosedGroupPrivateKey(groupPublicKey: String, groupPrivateKey: String) { + val database = databaseHelper.writableDatabase + val values = ContentValues() + values.put(Companion.closedGroupPublicKey, groupPublicKey) + values.put(Companion.closedGroupPrivateKey, groupPrivateKey) + val query = "${Companion.closedGroupPublicKey} = ?" + database.insertOrUpdate(closedGroupPrivateKeyTable, values, query, arrayOf( groupPublicKey )) + } + + override fun removeClosedGroupPrivateKey(groupPublicKey: String) { + val database = databaseHelper.writableDatabase + val query = "${Companion.closedGroupPublicKey} = ?" + database.delete(closedGroupPrivateKeyTable, query, arrayOf( groupPublicKey )) + } + + override fun getAllClosedGroupPublicKeys(): Set { + val database = databaseHelper.readableDatabase + return database.getAll(closedGroupPrivateKeyTable, null, null) { cursor -> + cursor.getString(Companion.closedGroupPublicKey) + }.filter { + PublicKeyValidation.isValid(it) + }.toSet() + } + // endregion + + override fun isSSKBasedClosedGroup(groupPublicKey: String): Boolean { + if (!PublicKeyValidation.isValid(groupPublicKey)) { return false } + return getAllClosedGroupPublicKeys().contains(groupPublicKey) + } + // endregion +} diff --git a/src/org/thoughtcrime/securesms/loki/dialogs/LinkDeviceMasterModeDialog.kt b/src/org/thoughtcrime/securesms/loki/dialogs/LinkDeviceMasterModeDialog.kt index fbb476e527..7dd30cf323 100644 --- a/src/org/thoughtcrime/securesms/loki/dialogs/LinkDeviceMasterModeDialog.kt +++ b/src/org/thoughtcrime/securesms/loki/dialogs/LinkDeviceMasterModeDialog.kt @@ -15,7 +15,7 @@ import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi import org.thoughtcrime.securesms.database.DatabaseFactory -import org.thoughtcrime.securesms.loki.protocol.MultiDeviceProtocol +import org.thoughtcrime.securesms.loki.protocol.shelved.MultiDeviceProtocol import org.thoughtcrime.securesms.loki.utilities.MnemonicUtilities import org.thoughtcrime.securesms.loki.utilities.QRCodeUtilities import org.thoughtcrime.securesms.loki.utilities.toPx diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt new file mode 100644 index 0000000000..2c0f4ef702 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupUpdateMessageSendJob.kt @@ -0,0 +1,181 @@ +package org.thoughtcrime.securesms.loki.protocol + +import com.google.protobuf.ByteString +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil +import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl +import org.thoughtcrime.securesms.jobmanager.Data +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.jobs.BaseJob +import org.thoughtcrime.securesms.logging.Log +import org.thoughtcrime.securesms.loki.utilities.recipient +import org.thoughtcrime.securesms.util.Hex +import org.whispersystems.libsignal.SignalProtocolAddress +import org.whispersystems.signalservice.api.push.SignalServiceAddress +import org.whispersystems.signalservice.internal.push.SignalServiceProtos +import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey +import org.whispersystems.signalservice.loki.protocol.meta.TTLUtilities +import org.whispersystems.signalservice.loki.utilities.toHexString +import java.util.* +import java.util.concurrent.TimeUnit + +class ClosedGroupUpdateMessageSendJob private constructor(parameters: Parameters, private val destination: String, private val kind: Kind) : BaseJob(parameters) { + + sealed class Kind { + class New(val groupPublicKey: ByteArray, val name: String, val groupPrivateKey: ByteArray, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class Info(val groupPublicKey: ByteArray, val name: String, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class SenderKeyRequest(val groupPublicKey: ByteArray) : Kind() + class SenderKey(val groupPublicKey: ByteArray, val senderKey: ClosedGroupSenderKey) : Kind() + } + + companion object { + const val KEY = "ClosedGroupUpdateMessageSendJob" + } + + constructor(destination: String, kind: Kind) : this(Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setQueue(KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(1) + .build(), + destination, + kind) + + override fun getFactoryKey(): String { return KEY } + + override fun serialize(): Data { + val builder = Data.Builder() + builder.putString("destination", destination) + when (kind) { + is Kind.New -> { + builder.putString("kind", "New") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + builder.putString("name", kind.name) + builder.putByteArray("groupPrivateKey", kind.groupPrivateKey) + val senderKeys = kind.senderKeys.joinToString(" - ") { it.toJSON() } + builder.putString("senderKeys", senderKeys) + val members = kind.members.joinToString(" - ") { it.toHexString() } + builder.putString("members", members) + val admins = kind.admins.joinToString(" - ") { it.toHexString() } + builder.putString("admins", admins) + } + is Kind.Info -> { + builder.putString("kind", "Info") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + builder.putString("name", kind.name) + val senderKeys = kind.senderKeys.joinToString(" - ") { it.toJSON() } + builder.putString("senderKeys", senderKeys) + val members = kind.members.joinToString(" - ") { it.toHexString() } + builder.putString("members", members) + val admins = kind.admins.joinToString(" - ") { it.toHexString() } + builder.putString("admins", admins) + } + is Kind.SenderKeyRequest -> { + builder.putString("kind", "SenderKeyRequest") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + } + is Kind.SenderKey -> { + builder.putString("kind", "SenderKey") + builder.putByteArray("groupPublicKey", kind.groupPublicKey) + builder.putString("senderKey", kind.senderKey.toJSON()) + } + } + return builder.build() + } + + public override fun onRun() { + val contentMessage = SignalServiceProtos.Content.newBuilder() + val dataMessage = SignalServiceProtos.DataMessage.newBuilder() + val closedGroupUpdate = SignalServiceProtos.ClosedGroupUpdate.newBuilder() + when (kind) { + is Kind.New -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.NEW + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.name = kind.name + closedGroupUpdate.groupPrivateKey = ByteString.copyFrom(kind.groupPrivateKey) + closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() }) + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) }) + } + is Kind.Info -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.INFO + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.name = kind.name + closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() }) + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) }) + } + is Kind.SenderKeyRequest -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + } + is Kind.SenderKey -> { + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.addAllSenderKeys(listOf( kind.senderKey.toProto() )) + } + } + dataMessage.closedGroupUpdate = closedGroupUpdate.build() + contentMessage.dataMessage = dataMessage.build() + val serializedContentMessage = contentMessage.build().toByteArray() + val messageSender = ApplicationContext.getInstance(context).communicationModule.provideSignalMessageSender() + val address = SignalServiceAddress(destination) + val recipient = recipient(context, destination) + val udAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) + val ttl = TTLUtilities.getTTL(TTLUtilities.MessageType.ClosedGroupUpdate) + val useFallbackEncryption = SignalProtocolStoreImpl(context).containsSession(SignalProtocolAddress(destination, 1)) + 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, + useFallbackEncryption, false, false) + } catch (e: Exception) { + Log.d("Loki", "Failed to send closed group update message to: $destination due to error: $e.") + throw e + } + } + + public override fun onShouldRetry(e: Exception): Boolean { + // Disable since we have our own retrying + return false + } + + override fun onCanceled() { } + + class Factory : Job.Factory { + + override fun create(parameters: Parameters, data: Data): ClosedGroupUpdateMessageSendJob { + val destination = data.getString("destination") + val rawKind = data.getString("kind") + val groupPublicKey = data.getByteArray("groupPublicKey") + val kind: Kind + when (rawKind) { + "New" -> { + val name = data.getString("name") + val groupPrivateKey = data.getByteArray("groupPrivateKey") + val senderKeys = data.getString("senderKeys").split(" - ").map { ClosedGroupSenderKey.fromJSON(it)!! } + val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } + val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) } + kind = Kind.New(groupPublicKey, name, groupPrivateKey, senderKeys, members, admins) + } + "Info" -> { + val name = data.getString("name") + val senderKeys = data.getStringOrDefault("senderKeys", "").split(" - ").mapNotNull { ClosedGroupSenderKey.fromJSON(it) } // Can be empty + val members = data.getString("members").split(" - ").map { Hex.fromStringCondensed(it) } + val admins = data.getString("admins").split(" - ").map { Hex.fromStringCondensed(it) } + kind = Kind.Info(groupPublicKey, name, senderKeys, members, admins) + } + "SenderKeyRequest" -> { + kind = Kind.SenderKeyRequest(groupPublicKey) + } + "SenderKey" -> { + val senderKey = ClosedGroupSenderKey.fromJSON(data.getString("senderKey"))!! + kind = Kind.SenderKey(groupPublicKey, senderKey) + } + else -> throw Exception("Invalid closed group update message kind: $rawKind.") + } + return ClosedGroupUpdateMessageSendJob(parameters, destination, kind) + } + } +} diff --git a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt index 8bcff5516d..d43492b48b 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocol.kt @@ -1,62 +1,402 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context -import nl.komponents.kovenant.Promise -import nl.komponents.kovenant.functional.map +import android.util.Log +import com.google.protobuf.ByteString import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.loki.utilities.recipient -import org.thoughtcrime.securesms.loki.utilities.timeout +import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.sms.IncomingGroupMessage +import org.thoughtcrime.securesms.sms.IncomingTextMessage import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.util.GroupUtil +import org.thoughtcrime.securesms.util.Hex import org.thoughtcrime.securesms.util.TextSecurePreferences -import org.whispersystems.libsignal.SignalProtocolAddress -import org.whispersystems.signalservice.api.messages.SignalServiceContent +import org.whispersystems.libsignal.ecc.Curve +import org.whispersystems.libsignal.util.guava.Optional import org.whispersystems.signalservice.api.messages.SignalServiceGroup -import org.whispersystems.signalservice.api.push.SignalServiceAddress -import org.whispersystems.signalservice.loki.api.SnodeAPI -import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI -import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol +import org.whispersystems.signalservice.api.messages.SignalServiceGroup.GroupType +import org.whispersystems.signalservice.internal.push.SignalServiceProtos +import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext +import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupRatchet +import org.whispersystems.signalservice.loki.protocol.closedgroups.ClosedGroupSenderKey +import org.whispersystems.signalservice.loki.protocol.closedgroups.SharedSenderKeysImplementation +import org.whispersystems.signalservice.loki.utilities.hexEncodedPrivateKey +import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey +import org.whispersystems.signalservice.loki.utilities.toHexString import java.util.* object ClosedGroupsProtocol { + val isSharedSenderKeysEnabled = false + + public fun createClosedGroup(context: Context, name: String, members: Collection): String { + // Prepare + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + // Generate a key pair for the group + val groupKeyPair = Curve.generateKeyPair() + val groupPublicKey = groupKeyPair.hexEncodedPublicKey // Includes the "05" prefix + val membersAsData = members.map { Hex.fromStringCondensed(it) } + // Create ratchets for all members + val senderKeys: List = members.map { publicKey -> + val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) + ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) + } + // Create the group + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val admins = setOf( userPublicKey ) + DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), + null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) + DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, members) + // Send a closed group update message to all members using established channels + val adminsAsData = admins.map { Hex.fromStringCondensed(it) } + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, groupKeyPair.privateKey.serialize(), + senderKeys, membersAsData, adminsAsData) + for (member in members) { + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + // TODO: Wait for the messages to finish sending + // Add the group to the user's set of public keys to poll for + DatabaseFactory.getSSKDatabase(context).setClosedGroupPrivateKey(groupPublicKey, groupKeyPair.hexEncodedPrivateKey) + // Notify the user + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) + // Return + return groupID + } + + public fun addMembers(context: Context, newMembers: Collection, groupPublicKey: String) { + // Prepare + val sskDatabase = DatabaseFactory.getSSKDatabase(context) + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Can't add users to nonexistent closed group.") + return + } + val name = group.title + val admins = group.admins.map { it.serialize() } + val adminsAsData = admins.map { Hex.fromStringCondensed(it) } + val groupPrivateKey = DatabaseFactory.getSSKDatabase(context).getClosedGroupPrivateKey(groupPublicKey) + if (groupPrivateKey == null) { + Log.d("Loki", "Couldn't get private key for closed group.") + return + } + // Add the members to the member list + val members = group.members.map { it.serialize() }.toMutableSet() + members.addAll(newMembers) + val membersAsData = members.map { Hex.fromStringCondensed(it) } + // Generate ratchets for the new members + val senderKeys: List = newMembers.map { publicKey -> + val ratchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, publicKey) + ClosedGroupSenderKey(Hex.fromStringCondensed(ratchet.chainKey), ratchet.keyIndex, Hex.fromStringCondensed(publicKey)) + } + // Send a closed group update message to the existing members with the new members' ratchets (this message is aimed at the group) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), name, + senderKeys, membersAsData, adminsAsData) + val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, newMembers) + // Send closed group update messages to the new members using established channels + val allSenderKeys = sskDatabase.getAllClosedGroupSenderKeys(groupPublicKey) + senderKeys + for (member in members) { + @Suppress("NAME_SHADOWING") + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.New(Hex.fromStringCondensed(groupPublicKey), name, + Hex.fromStringCondensed(groupPrivateKey), allSenderKeys, membersAsData, adminsAsData) + @Suppress("NAME_SHADOWING") + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + // Update the group + groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + // Notify the user + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.UPDATE, name, members, admins, threadID) + } - /** - * Blocks the calling thread. - */ @JvmStatic - fun shouldIgnoreContentMessage(context: Context, conversation: Recipient, groupID: String?, content: SignalServiceContent): Boolean { - if (!conversation.address.isClosedGroup || groupID == null) { return false } - // A closed group's members should never include slave devices - val senderPublicKey = content.sender + public fun leave(context: Context, groupPublicKey: String) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + removeMembers(context, setOf( userPublicKey ), groupPublicKey) + } + + public fun removeMembers(context: Context, membersToRemove: Collection, groupPublicKey: String) { + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val sskDatabase = DatabaseFactory.getSSKDatabase(context) + val isUserLeaving = membersToRemove.contains(userPublicKey) + if (isUserLeaving && membersToRemove.count() != 1) { + Log.d("Loki", "Can't remove self and others simultaneously.") + return + } + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Can't add users to nonexistent closed group.") + return + } + val name = group.title + val admins = group.admins.map { it.serialize() } + val adminsAsData = admins.map { Hex.fromStringCondensed(it) } + // Remove the members from the member list + val members = group.members.map { it.serialize() }.toSet().minus(membersToRemove) + val membersAsData = members.map { Hex.fromStringCondensed(it) } + // Send the update to the group (don't include new ratchets as everyone should regenerate new ratchets individually) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.Info(Hex.fromStringCondensed(groupPublicKey), + name, setOf(), membersAsData, adminsAsData) + val job = ClosedGroupUpdateMessageSendJob(groupPublicKey, closedGroupUpdateKind) + job.setContext(context) + job.onRun() // Run the job immediately + // Delete all ratchets (it's important that this happens after sending out the update) + sskDatabase.removeAllClosedGroupRatchets(groupPublicKey) + // Remove the group from the user's set of public keys to poll for if the user is leaving. Otherwise generate a new ratchet and + // send it out to all members (minus the removed ones) using established channels. + if (isUserLeaving) { + sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) + groupDB.setActive(groupID, false) + } else { + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, members) + // Send out the user's new ratchet to all members (minus the removed ones) using established channels + val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) + val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) + for (member in members) { + @Suppress("NAME_SHADOWING") + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) + @Suppress("NAME_SHADOWING") + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + } + // Update the group + groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + // Notify the user + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertOutgoingInfoMessage(context, groupID, GroupContext.Type.QUIT, name, members, admins, threadID) + } + + @JvmStatic + public fun requestSenderKey(context: Context, groupPublicKey: String, senderPublicKey: String) { + // Establish session if needed + ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) + // Send the request + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKeyRequest(Hex.fromStringCondensed(groupPublicKey)) + val job = ClosedGroupUpdateMessageSendJob(senderPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + + @JvmStatic + public fun handleSharedSenderKeysUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { + if (!isValid(closedGroupUpdate)) { return; } + when (closedGroupUpdate.type) { + SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> handleNewClosedGroup(context, closedGroupUpdate) + SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> handleClosedGroupUpdate(context, closedGroupUpdate, senderPublicKey) + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST -> handleSenderKeyRequest(context, closedGroupUpdate, senderPublicKey) + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY -> handleSenderKey(context, closedGroupUpdate, senderPublicKey) + else -> { + // Do nothing + } + } + } + + private fun isValid(closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate): Boolean { + if (closedGroupUpdate.groupPublicKey.isEmpty) { return false } + when (closedGroupUpdate.type) { + SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> { + return !closedGroupUpdate.name.isNullOrEmpty() && !(closedGroupUpdate.groupPrivateKey ?: ByteString.copyFrom(ByteArray(0))).isEmpty + && closedGroupUpdate.senderKeysCount > 0 && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0 + } + SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> { + return !closedGroupUpdate.name.isNullOrEmpty() && closedGroupUpdate.membersCount > 0 && closedGroupUpdate.adminsCount > 0 // senderKeys may be empty + } + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY -> return true + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST -> return closedGroupUpdate.senderKeysCount > 0 + else -> return false + } + } + + public fun handleNewClosedGroup(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate) { + // Prepare + val sskDatabase = DatabaseFactory.getSSKDatabase(context) + // Unwrap the message + val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() + val name = closedGroupUpdate.name + val groupPrivateKey = closedGroupUpdate.groupPrivateKey.toByteArray() + val senderKeys = closedGroupUpdate.senderKeysList.map { + ClosedGroupSenderKey(it.chainKey.toByteArray(), it.keyIndex, it.publicKey.toByteArray()) + } + val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } + val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() } + // Persist the ratchets + senderKeys.forEach { senderKey -> + if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach } + val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) + sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet) + } + // Create the group + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + DatabaseFactory.getGroupDatabase(context).create(groupID, name, LinkedList
(members.map { Address.fromSerialized(it) }), + null, null, LinkedList
(admins.map { Address.fromSerialized(it) })) + DatabaseFactory.getRecipientDatabase(context).setProfileSharing(Recipient.from(context, Address.fromSerialized(groupID), false), true) + // Add the group to the user's set of public keys to poll for + sskDatabase.setClosedGroupPrivateKey(groupPublicKey, groupPrivateKey.toHexString()) + // Notify the user + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertIncomingInfoMessage(context, groupID, GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, threadID) + // Establish sessions if needed + establishSessionsWithMembersIfNeeded(context, members) + } + + public fun handleClosedGroupUpdate(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { + // Prepare + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val sskDatabase = DatabaseFactory.getSSKDatabase(context) + // Unwrap the message + val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() + val name = closedGroupUpdate.name + val senderKeys = closedGroupUpdate.senderKeysList.map { + ClosedGroupSenderKey(it.chainKey.toByteArray(), it.keyIndex, it.publicKey.toByteArray()) + } + val members = closedGroupUpdate.membersList.map { it.toByteArray().toHexString() } + val admins = closedGroupUpdate.adminsList.map { it.toByteArray().toHexString() } + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + return + } + val oldMembers = group.members.map { it.serialize() } + // Check that the sender is a member of the group (before the update) + if (!oldMembers.contains(senderPublicKey)) { + Log.d("Loki", "Ignoring closed group info message from non-member.") + return + } + // Store the ratchets for any new members (it's important that this happens before the code below) + senderKeys.forEach { senderKey -> + if (!members.contains(senderKey.publicKey.toHexString())) { return@forEach } + val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) + sskDatabase.setClosedGroupRatchet(groupPublicKey, senderKey.publicKey.toHexString(), ratchet) + } + // Delete all ratchets and either: + // • Send out the user's new ratchet using established channels if other members of the group left or were removed + // • Remove the group from the user's set of public keys to poll for if the current user was among the members that were removed + val wasCurrentUserRemoved = !members.contains(userPublicKey) + val wasAnyUserRemoved = members.toSet().intersect(oldMembers) != oldMembers.toSet() + if (wasAnyUserRemoved) { + sskDatabase.removeAllClosedGroupRatchets(groupPublicKey) + if (wasCurrentUserRemoved) { + sskDatabase.removeClosedGroupPrivateKey(groupPublicKey) + groupDB.setActive(groupID, false) + } else { + establishSessionsWithMembersIfNeeded(context, members) + val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) + val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) + for (member in members) { + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) + val job = ClosedGroupUpdateMessageSendJob(member, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + } + } + // Update the group + groupDB.updateTitle(groupID, name) + groupDB.updateMembers(groupID, members.map { Address.fromSerialized(it) }) + // Notify the user + val type0 = if (wasAnyUserRemoved) GroupContext.Type.QUIT else GroupContext.Type.UPDATE + val type1 = if (wasAnyUserRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE + val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(Recipient.from(context, Address.fromSerialized(groupID), false)) + insertIncomingInfoMessage(context, groupID, type0, type1, name, members, admins, threadID) + } + + public fun handleSenderKeyRequest(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { + // Prepare + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Ignoring closed group sender key request for nonexistent group.") + return + } + // Check that the requesting user is a member of the group + if (!group.members.map { it.serialize() }.contains(senderPublicKey)) { + Log.d("Loki", "Ignoring closed group sender key request from non-member.") + return + } + // Respond to the request + ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(senderPublicKey) + val userRatchet = SharedSenderKeysImplementation.shared.generateRatchet(groupPublicKey, userPublicKey) + val userSenderKey = ClosedGroupSenderKey(Hex.fromStringCondensed(userRatchet.chainKey), userRatchet.keyIndex, Hex.fromStringCondensed(userPublicKey)) + val closedGroupUpdateKind = ClosedGroupUpdateMessageSendJob.Kind.SenderKey(Hex.fromStringCondensed(groupPublicKey), userSenderKey) + val job = ClosedGroupUpdateMessageSendJob(senderPublicKey, closedGroupUpdateKind) + ApplicationContext.getInstance(context).jobManager.add(job) + } + + public fun handleSenderKey(context: Context, closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate, senderPublicKey: String) { + // Prepare + val sskDatabase = DatabaseFactory.getSSKDatabase(context) + val groupPublicKey = closedGroupUpdate.groupPublicKey.toByteArray().toHexString() + val groupDB = DatabaseFactory.getGroupDatabase(context) + val groupID = GroupUtil.getEncodedId(Hex.fromStringCondensed(groupPublicKey), false) + val group = groupDB.getGroup(groupID).orNull() + if (group == null) { + Log.d("Loki", "Ignoring closed group sender key for nonexistent group.") + return + } + val senderKeyProto = closedGroupUpdate.senderKeysList.firstOrNull() + if (senderKeyProto == null) { + Log.d("Loki", "Ignoring invalid closed group sender key.") + return + } + val senderKey = ClosedGroupSenderKey(senderKeyProto.chainKey.toByteArray(), senderKeyProto.keyIndex, senderKeyProto.publicKey.toByteArray()) + // Check that the sending user is a member of the group + if (!group.members.map { it.serialize() }.contains(senderPublicKey)) { + Log.d("Loki", "Ignoring closed group sender key from non-member.") + return + } + if (senderKeyProto.publicKey.toByteArray().toHexString() != senderPublicKey) { + Log.d("Loki", "Ignoring invalid closed group sender key.") + return + } + // Store the sender key + val ratchet = ClosedGroupRatchet(senderKey.chainKey.toHexString(), senderKey.keyIndex, listOf()) + sskDatabase.setClosedGroupRatchet(groupPublicKey, senderPublicKey, ratchet) + } + + @JvmStatic + fun shouldIgnoreContentMessage(context: Context, address: Address, groupID: String?, senderPublicKey: String): Boolean { + if (!address.isClosedGroup || groupID == null) { return false } + /* FileServerAPI.shared.getDeviceLinks(senderPublicKey).timeout(6000).get() val senderMasterPublicKey = MultiDeviceProtocol.shared.getMasterDevice(senderPublicKey) val publicKeyToCheckFor = senderMasterPublicKey ?: senderPublicKey + */ val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, true) - return !members.contains(recipient(context, publicKeyToCheckFor)) + return !members.contains(recipient(context, senderPublicKey)) } @JvmStatic - fun shouldIgnoreGroupCreatedMessage(context: Context, group: SignalServiceGroup): Boolean { - val members = group.members - val masterPublicKeyOrNull = TextSecurePreferences.getMasterHexEncodedPublicKey(context) - val masterPublicKey = masterPublicKeyOrNull ?: TextSecurePreferences.getLocalNumber(context) - return !members.isPresent || !members.get().contains(masterPublicKey) - } - - @JvmStatic - fun getDestinations(groupID: String, context: Context): Promise, Exception> { - if (GroupUtil.isRSSFeed(groupID)) { return Promise.of(listOf()) } + fun getMessageDestinations(context: Context, groupID: String): List
{ + if (GroupUtil.isRSSFeed(groupID)) { return listOf() } if (GroupUtil.isOpenGroup(groupID)) { - val result = mutableListOf
() - result.add(Address.fromSerialized(groupID)) - return Promise.of(result) + return listOf( Address.fromSerialized(groupID) ) } else { - // A closed group's members should never include slave devices - val members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false) + val groupPublicKey = GroupUtil.getDecodedId(groupID).toHexString() + if (DatabaseFactory.getSSKDatabase(context).isSSKBasedClosedGroup(groupPublicKey)) { + return listOf( Address.fromSerialized(groupPublicKey) ) + } else { + return DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupID, false).map { it.address } + } + /* return FileServerAPI.shared.getDeviceLinks(members.map { it.address.serialize() }.toSet()).map { val result = members.flatMap { member -> MultiDeviceProtocol.shared.getAllLinkedDevices(member.address.serialize()).map { Address.fromSerialized(it) } @@ -71,29 +411,33 @@ object ClosedGroupsProtocol { } result.toList() } + */ } } @JvmStatic - fun leaveGroup(context: Context, recipient: Recipient): Boolean { + fun leaveLegacyGroup(context: Context, recipient: Recipient): Boolean { if (!recipient.address.isClosedGroup) { return true } val threadID = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient) - val message = GroupUtil.createGroupLeaveMessage(context, recipient) - if (threadID < 0 || !message.isPresent) { return false } - MessageSender.send(context, message.get(), threadID, false, null) - // Remove the master device from the group (a closed group's members should never include slave devices) + val message = GroupUtil.createGroupLeaveMessage(context, recipient).orNull() + if (threadID < 0 || message == null) { return false } + MessageSender.send(context, message, threadID, false, null) + /* val masterPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context) val publicKeyToRemove = masterPublicKey ?: TextSecurePreferences.getLocalNumber(context) + */ + val userPublicKey = TextSecurePreferences.getLocalNumber(context) val groupDatabase = DatabaseFactory.getGroupDatabase(context) val groupID = recipient.address.toGroupString() groupDatabase.setActive(groupID, false) - groupDatabase.remove(groupID, Address.fromSerialized(publicKeyToRemove)) + groupDatabase.remove(groupID, Address.fromSerialized(userPublicKey)) return true } @JvmStatic - fun establishSessionsWithMembersIfNeeded(context: Context, members: List) { - // A closed group's members should never include slave devices + fun establishSessionsWithMembersIfNeeded(context: Context, members: Collection) { + @Suppress("NAME_SHADOWING") val members = members.toMutableSet() + /* val allDevices = members.flatMap { member -> MultiDeviceProtocol.shared.getAllLinkedDevices(member) }.toMutableSet() @@ -101,12 +445,43 @@ object ClosedGroupsProtocol { if (userMasterPublicKey != null && allDevices.contains(userMasterPublicKey)) { allDevices.remove(userMasterPublicKey) } + */ val userPublicKey = TextSecurePreferences.getLocalNumber(context) - if (userPublicKey != null && allDevices.contains(userPublicKey)) { - allDevices.remove(userPublicKey) + if (userPublicKey != null && members.contains(userPublicKey)) { + members.remove(userPublicKey) } - for (device in allDevices) { - ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(device) + for (member in members) { + ApplicationContext.getInstance(context).sendSessionRequestIfNeeded(member) } } + + private fun insertIncomingInfoMessage(context: Context, groupID: String, type0: GroupContext.Type, type1: SignalServiceGroup.Type, name: String, + members: Collection, admins: Collection, threadID: Long) { + val groupContextBuilder = GroupContext.newBuilder() + .setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupID))) + .setType(type0) + .setName(name) + .addAllMembers(members) + .addAllAdmins(admins) + val group = SignalServiceGroup(type1, GroupUtil.getDecodedId(groupID), GroupType.SIGNAL, name, members.toList(), null, admins.toList()) + val m = IncomingTextMessage(Address.fromSerialized(groupID), 1, System.currentTimeMillis(), "", Optional.of(group), 0, true) + val infoMessage = IncomingGroupMessage(m, groupContextBuilder.build(), "") + val smsDB = DatabaseFactory.getSmsDatabase(context) + smsDB.insertMessageInbox(infoMessage) + } + + private fun insertOutgoingInfoMessage(context: Context, groupID: String, type: GroupContext.Type, name: String, + members: Collection, admins: Collection, threadID: Long) { + val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) + val groupContextBuilder = GroupContext.newBuilder() + .setId(ByteString.copyFrom(GroupUtil.getDecodedId(groupID))) + .setType(type) + .setName(name) + .addAllMembers(members) + .addAllAdmins(admins) + val infoMessage = OutgoingGroupMediaMessage(recipient, groupContextBuilder.build(), null, System.currentTimeMillis(), 0, null, listOf(), listOf()) + val mmsDB = DatabaseFactory.getMmsDatabase(context) + val infoMessageID = mmsDB.insertMessageOutbox(infoMessage, threadID, false, null) + mmsDB.markAsSent(infoMessageID, true) + } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/protocol/PushNullMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt similarity index 89% rename from src/org/thoughtcrime/securesms/loki/protocol/PushNullMessageSendJob.kt rename to src/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt index 90b8ef0e00..821a281eb2 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/PushNullMessageSendJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/NullMessageSendJob.kt @@ -18,7 +18,7 @@ import java.security.SecureRandom import java.util.* import java.util.concurrent.TimeUnit -class PushNullMessageSendJob private constructor(parameters: Parameters, private val publicKey: String) : BaseJob(parameters) { +class NullMessageSendJob private constructor(parameters: Parameters, private val publicKey: String) : BaseJob(parameters) { companion object { const val KEY = "PushNullMessageSendJob" @@ -70,12 +70,12 @@ class PushNullMessageSendJob private constructor(parameters: Parameters, private override fun onCanceled() { } - class Factory : Job.Factory { + class Factory : Job.Factory { - override fun create(parameters: Parameters, data: Data): PushNullMessageSendJob { + override fun create(parameters: Parameters, data: Data): NullMessageSendJob { try { val publicKey = data.getString("publicKey") - return PushNullMessageSendJob(parameters, publicKey) + return NullMessageSendJob(parameters, publicKey) } catch (e: IOException) { throw AssertionError(e) } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt index 4bd08e275f..fe9e07344c 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionManagementProtocol.kt @@ -76,7 +76,7 @@ object SessionManagementProtocol { val preKeyBundle = preKeyBundleMessage.getPreKeyBundle(registrationID) lokiPreKeyBundleDatabase.setPreKeyBundle(publicKey, preKeyBundle) DatabaseFactory.getLokiAPIDatabase(context).setSessionRequestProcessedTimestamp(publicKey, Date().time) - val job = PushNullMessageSendJob(publicKey) + val job = NullMessageSendJob(publicKey) ApplicationContext.getInstance(context).jobManager.add(job) } @@ -89,7 +89,7 @@ object SessionManagementProtocol { sessionStore.archiveAllSessions(content.sender) lokiThreadDB.setSessionResetStatus(content.sender, SessionResetStatus.REQUEST_RECEIVED) Log.d("Loki", "Sending an ephemeral message back to: ${content.sender}.") - val job = PushNullMessageSendJob(content.sender) + val job = NullMessageSendJob(content.sender) ApplicationContext.getInstance(context).jobManager.add(job) SecurityEvent.broadcastSecurityUpdateEvent(context) } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt index c0dc77f4b1..1aacde4196 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionMetaProtocol.kt @@ -78,6 +78,11 @@ object SessionMetaProtocol { return !recipient.address.isRSSFeed } + @JvmStatic + fun shouldSendDeliveryReceipt(address: Address): Boolean { + return !address.isGroup + } + /** * Should be invoked for the recipient's master device. */ diff --git a/src/org/thoughtcrime/securesms/loki/protocol/PushSessionRequestMessageSendJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionRequestMessageSendJob.kt similarity index 88% rename from src/org/thoughtcrime/securesms/loki/protocol/PushSessionRequestMessageSendJob.kt rename to src/org/thoughtcrime/securesms/loki/protocol/SessionRequestMessageSendJob.kt index f530468235..155ca1e29b 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/PushSessionRequestMessageSendJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionRequestMessageSendJob.kt @@ -19,20 +19,20 @@ import java.security.SecureRandom import java.util.* import java.util.concurrent.TimeUnit -class PushSessionRequestMessageSendJob private constructor(parameters: Parameters, private val publicKey: String, private val timestamp: Long) : BaseJob(parameters) { +class SessionRequestMessageSendJob private constructor(parameters: Parameters, private val publicKey: String, private val timestamp: Long) : BaseJob(parameters) { companion object { const val KEY = "PushSessionRequestMessageSendJob" } constructor(publicKey: String, timestamp: Long) : this(Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setQueue(KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(1) - .build(), - publicKey, - timestamp) + .addConstraint(NetworkConstraint.KEY) + .setQueue(KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(1) + .build(), + publicKey, + timestamp) override fun serialize(): Data { return Data.Builder().putString("publicKey", publicKey).putLong("timestamp", timestamp).build() @@ -92,13 +92,13 @@ class PushSessionRequestMessageSendJob private constructor(parameters: Parameter } } - class Factory : Job.Factory { + class Factory : Job.Factory { - override fun create(parameters: Parameters, data: Data): PushSessionRequestMessageSendJob { + override fun create(parameters: Parameters, data: Data): SessionRequestMessageSendJob { try { val publicKey = data.getString("publicKey") val timestamp = data.getLong("timestamp") - return PushSessionRequestMessageSendJob(parameters, publicKey, timestamp) + return SessionRequestMessageSendJob(parameters, publicKey, timestamp) } catch (e: IOException) { throw AssertionError(e) } diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt b/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt index 4c34d64b64..1c8a283125 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/SessionResetImplementation.kt @@ -19,7 +19,7 @@ class SessionResetImplementation(private val context: Context) : SessionResetPro override fun onNewSessionAdopted(publicKey: String, oldSessionResetStatus: SessionResetStatus) { if (oldSessionResetStatus == SessionResetStatus.IN_PROGRESS) { - val job = PushNullMessageSendJob(publicKey) + val job = NullMessageSendJob(publicKey) ApplicationContext.getInstance(context).jobManager.add(job) } // TODO: Show session reset succeed message diff --git a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt b/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceOpenGroupUpdateJob.kt similarity index 98% rename from src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt rename to src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceOpenGroupUpdateJob.kt index 70de694044..6679594639 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceOpenGroupUpdateJob.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceOpenGroupUpdateJob.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.protocol +package org.thoughtcrime.securesms.loki.protocol.shelved import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil import org.thoughtcrime.securesms.database.DatabaseFactory diff --git a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceProtocol.kt similarity index 98% rename from src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt rename to src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceProtocol.kt index 4d37ab1e58..130dd2b9b3 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/MultiDeviceProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/shelved/MultiDeviceProtocol.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.protocol +package org.thoughtcrime.securesms.loki.protocol.shelved import android.content.Context import android.util.Log @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.jobs.PushMediaSendJob import org.thoughtcrime.securesms.jobs.PushSendJob import org.thoughtcrime.securesms.jobs.PushTextSendJob +import org.thoughtcrime.securesms.loki.protocol.SessionManagementProtocol import org.thoughtcrime.securesms.loki.utilities.Broadcaster import org.thoughtcrime.securesms.loki.utilities.recipient import org.thoughtcrime.securesms.recipients.Recipient @@ -19,7 +20,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI -import org.whispersystems.signalservice.loki.protocol.meta.SessionMetaProtocol import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.DeviceLink import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.DeviceLinkingSession import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol diff --git a/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt b/src/org/thoughtcrime/securesms/loki/protocol/shelved/SyncMessagesProtocol.kt similarity index 99% rename from src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt rename to src/org/thoughtcrime/securesms/loki/protocol/shelved/SyncMessagesProtocol.kt index 5ec37a41dd..487dfe7251 100644 --- a/src/org/thoughtcrime/securesms/loki/protocol/SyncMessagesProtocol.kt +++ b/src/org/thoughtcrime/securesms/loki/protocol/shelved/SyncMessagesProtocol.kt @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.loki.protocol +package org.thoughtcrime.securesms.loki.protocol.shelved import android.content.Context import android.util.Log @@ -28,7 +28,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInp import org.whispersystems.signalservice.loki.api.opengroups.PublicChat import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation -import java.util.* object SyncMessagesProtocol { diff --git a/src/org/thoughtcrime/securesms/loki/utilities/DatabaseUtilities.kt b/src/org/thoughtcrime/securesms/loki/utilities/DatabaseUtilities.kt index 8d3112939b..33d8997f02 100644 --- a/src/org/thoughtcrime/securesms/loki/utilities/DatabaseUtilities.kt +++ b/src/org/thoughtcrime/securesms/loki/utilities/DatabaseUtilities.kt @@ -5,7 +5,7 @@ import net.sqlcipher.Cursor import net.sqlcipher.database.SQLiteDatabase import org.whispersystems.signalservice.internal.util.Base64 -fun SQLiteDatabase.get(table: String, query: String, arguments: Array, get: (Cursor) -> T): T? { +fun SQLiteDatabase.get(table: String, query: String?, arguments: Array?, get: (Cursor) -> T): T? { var cursor: Cursor? = null try { cursor = query(table, null, query, arguments, null, null, null) @@ -18,7 +18,7 @@ fun SQLiteDatabase.get(table: String, query: String, arguments: Array SQLiteDatabase.getAll(table: String, query: String, arguments: Array, get: (Cursor) -> T): List { +fun SQLiteDatabase.getAll(table: String, query: String?, arguments: Array?, get: (Cursor) -> T): List { val result = mutableListOf() var cursor: Cursor? = null try { diff --git a/src/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/src/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index 660e1a4ecd..0183fdf108 100644 --- a/src/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/src/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -213,7 +213,7 @@ public class DefaultMessageNotifier implements MessageNotifier { } @Override - public void updateNotification(@NonNull Context context, long threadId, boolean signal) + public void updateNotification(@NonNull Context context, long threadId, boolean signal) { boolean isVisible = visibleThread == threadId; @@ -221,7 +221,7 @@ public class DefaultMessageNotifier implements MessageNotifier { Recipient recipients = DatabaseFactory.getThreadDatabase(context) .getRecipientForThreadId(threadId); - if (isVisible) { + if (isVisible && recipients != null && SessionMetaProtocol.shouldSendReadReceipt(recipients.getAddress())) { List messageIds = threads.setRead(threadId, false); MarkReadReceiver.process(context, messageIds); } diff --git a/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java b/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java index 942de714c3..8f556026e1 100644 --- a/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java +++ b/src/org/thoughtcrime/securesms/notifications/MarkReadReceiver.java @@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; import org.thoughtcrime.securesms.jobs.SendReadReceiptJob; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.loki.protocol.SessionMetaProtocol; -import org.thoughtcrime.securesms.loki.protocol.SyncMessagesProtocol; +import org.thoughtcrime.securesms.loki.protocol.shelved.SyncMessagesProtocol; import org.thoughtcrime.securesms.service.ExpiringMessageManager; import org.whispersystems.signalservice.loki.protocol.shelved.multidevice.MultiDeviceProtocol; @@ -105,7 +105,7 @@ public class MarkReadReceiver extends BroadcastReceiver { } } - private static void scheduleDeletion(Context context, ExpirationInfo expirationInfo) { + public static void scheduleDeletion(Context context, ExpirationInfo expirationInfo) { if (expirationInfo.getExpiresIn() > 0 && expirationInfo.getExpireStarted() <= 0) { ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();