Merge branch 'dev' into editgroup

This commit is contained in:
Anton Chekulaev 2020-08-14 17:47:48 +10:00
commit f7923cd8a4
38 changed files with 1243 additions and 384 deletions

View File

@ -12,27 +12,15 @@
android:orientation="vertical">
<EditText
style="@style/SmallSessionEditText"
style="@style/SessionEditText"
android:id="@+id/nameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/large_spacing"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginRight="@dimen/large_spacing"
android:hint="@string/activity_create_closed_group_edit_text_hint" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/large_spacing"
android:layout_marginTop="@dimen/medium_spacing"
android:layout_marginRight="@dimen/large_spacing"
android:layout_marginBottom="@dimen/medium_spacing"
android:textSize="@dimen/small_font_size"
android:textColor="@color/text"
android:alpha="0.6"
android:textAlignment="center"
android:text="@string/activity_create_closed_group_explanation" />
android:hint="@string/activity_create_closed_group_edit_text_hint" />
<View
android:layout_width="match_parent"

View File

@ -1274,50 +1274,50 @@
<!-- Session -->
<string name="continue_2">继续</string>
<string name="copy">复制</string>
<string name="invalid_url">无效的网址</string>
<string name="invalid_url">无效的链接</string>
<string name="copied_to_clipboard">复制到剪贴板</string>
<string name="device_linking_failed">无法链接设备。</string>
<string name="next">下一步</string>
<string name="share"></string>
<string name="share"></string>
<string name="invalid_session_id">无效的Session ID</string>
<string name="cancel">取消</string>
<string name="your_session_id">您的Session ID</string>
<string name="activity_landing_title_2">您的Session从这里开始...</string>
<string name="activity_landing_register_button_title">注册Session ID</string>
<string name="activity_landing_register_button_title">创建Session ID</string>
<string name="activity_landing_restore_button_title">继续使用您的Session ID</string>
<string name="activity_landing_link_button_title">链接到现有帐号</string>
<string name="activity_landing_device_unlinked_dialog_title">您的设备已成功断开链接</string>
<string name="activity_landing_link_button_title">关联现有帐号</string>
<string name="activity_landing_device_unlinked_dialog_title">您的设备已成功取消关联</string>
<string name="view_fake_chat_bubble_1">什么是Session</string>
<string name="view_fake_chat_bubble_2">Session是一个去中心化的加密消息应用。</string>
<string name="view_fake_chat_bubble_3">所以Session不会收集我的个人信息或对话原始数据?怎么做到的?。</string>
<string name="view_fake_chat_bubble_3">所以Session不会收集我的个人信息或者对话元数据?怎么做到的?</string>
<string name="view_fake_chat_bubble_4">通过结合高效的匿名路由和端到端的加密技术。</string>
<string name="view_fake_chat_bubble_5">好朋友就要与朋友使用能够保证信息安全的聊天工具,不用谢啦</string>
<string name="view_fake_chat_bubble_5">好朋友之间就要使用能够保证信息安全的聊天工具,不用谢啦</string>
<string name="activity_register_title">向您的Session ID打个招呼吧</string>
<string name="activity_register_explanation">Session ID是其他用户需要与您聊天时使用的独一无二的地址。与您的真实身份无关Session ID的设计是完全是匿名和私有的。</string>
<string name="activity_register_title">向您的Session ID打个招呼吧</string>
<string name="activity_register_explanation">您的Session ID是其他用户在与您聊天时使用的独一无二的地址。Session ID与您的真实身份无关它在设计上完全是匿名且私密的。</string>
<string name="activity_register_public_key_copied_message">复制到剪贴板</string>
<string name="activity_restore_title">恢复您的帐号</string>
<string name="activity_restore_explanation">在您重新登陆并需要恢复账户时,请输入您注册帐号时的恢复口令。</string>
<string name="activity_restore_seed_edit_text_hint">输入您的恢复口令</string>
<string name="activity_link_device_title">链接设备</string>
<string name="activity_link_device_title">关联设备</string>
<string name="activity_link_device_enter_session_id_tab_title">输入Session ID</string>
<string name="activity_link_device_scan_qr_code_tab_title">扫描二维码</string>
<string name="activity_link_device_scan_qr_code_explanation">在您的设备上导航到“设置”>“设备”>“链接设备”,然后扫描出现的二维码以开始链接过程</string>
<string name="activity_link_device_scan_qr_code_explanation">在您的设备上导航到“设置”>“设备”>“链接设备”,然后扫描出现的二维码以开始关联</string>
<string name="fragment_enter_session_id_title">链接您的设备</string>
<string name="fragment_enter_session_id_explanation">在您的另一个设备上导航到“设置”>“设备” >“链接设备”然后在此处输入Session ID以开始链接过程</string>
<string name="fragment_enter_session_id_title">关联您的设备</string>
<string name="fragment_enter_session_id_explanation">在您的另一个设备上导航到“设置”>“设备” >“链接设备”然后在此处输入Session ID以开始关联</string>
<string name="fragment_enter_session_id_edit_text_hint">输入Session ID</string>
<string name="activity_display_name_title_2">选择您的显示名称</string>
<string name="activity_display_name_explanation">使用Session时,这就是您的名字。它可以是您的真实姓名,别名或您喜欢的其他任何名称。</string>
<string name="activity_display_name_edit_text_hint">输入显示名称</string>
<string name="activity_display_name_display_name_missing_error">选择一个显示名称</string>
<string name="activity_display_name_display_name_invalid_error">选择一个仅包含 azAZ0-9 和_字符的显示名称</string>
<string name="activity_display_name_display_name_too_long_error">选择一个较短的显示名称</string>
<string name="activity_display_name_title_2">选择您想显示的名称</string>
<string name="activity_display_name_explanation">这就是您在使用Session时的名字。它可以是您的真实姓名别名或您喜欢的其他任何名称。</string>
<string name="activity_display_name_edit_text_hint">输入您想显示名称</string>
<string name="activity_display_name_display_name_missing_error">设定一个名称</string>
<string name="activity_display_name_display_name_invalid_error">设定一个仅包含 a-zA-Z0-9 和 _ 的名称</string>
<string name="activity_display_name_display_name_too_long_error">设定一个较短的名称</string>
<string name="activity_pn_mode_recommended_option_tag">推荐的选项</string>
<string name="activity_pn_mode_no_option_picked_dialog_title">请选择一个选项</string>
@ -1331,15 +1331,15 @@
<string name="activity_seed_title">您的恢复口令</string>
<string name="activity_seed_title_2">这里是您的恢复口令</string>
<string name="activity_seed_explanation">您的恢复口令是Session ID的主密钥 - 如果您无法访问您的现有设备则可以使用它在其他设备上恢复Session ID。将您的恢复口令存储在安全的地方不要将其提供给任何人。</string>
<string name="activity_seed_explanation">您的恢复口令是Session ID的主密钥 - 如果您无法访问您的现有设备,则可以使用它在其他设备上恢复您的Session ID。将您的恢复口令存储在安全的地方,不要将其提供给任何人。</string>
<string name="activity_seed_reveal_button_title">长按显示内容</string>
<string name="view_seed_reminder_subtitle_1">保存恢复短语以保护您的帐号安全</string>
<string name="view_seed_reminder_subtitle_2">点击并按住遮盖住的单词以显示您的恢复短语,然后安全地存储它以保护Session ID。</string>
<string name="view_seed_reminder_subtitle_3">确保将恢复短语存储在安全的地方</string>
<string name="view_seed_reminder_subtitle_1">保存恢复口令以保护您的帐号安全</string>
<string name="view_seed_reminder_subtitle_2">点击并按住遮盖住的单词以显示您的恢复口令,请将它安全地存储以保护您的Session ID。</string>
<string name="view_seed_reminder_subtitle_3">请确保将恢复口令存储在安全的地方</string>
<string name="activity_path_title">路径</string>
<string name="activity_path_explanation">Session会通过Session的分散网络中的多个服务节点跳转消息以隐藏IP。以下国家您目前的消息连接跳转服务节点所在地:</string>
<string name="activity_path_explanation">Session会通过其去中心化网络中的多个服务节点跳转消息以隐藏IP。以下国家您目前的消息连接跳转服务节点所在地:</string>
<string name="activity_path_device_row_title"></string>
<string name="activity_path_guard_node_row_title">入口节点</string>
<string name="activity_path_service_node_row_title">服务节点</string>
@ -1349,38 +1349,38 @@
<string name="activity_create_private_chat_title">新建私人聊天</string>
<string name="activity_create_private_chat_enter_session_id_tab_title">输入Session ID</string>
<string name="activity_create_private_chat_scan_qr_code_tab_title">扫描二维码</string>
<string name="activity_create_private_chat_scan_qr_code_explanation">扫描另一用户的二维码以开始使用Session。您可以在帐号设置中点击二维码图标找到二维码。</string>
<string name="activity_create_private_chat_scan_qr_code_explanation">扫描其他用户的二维码来发起对话。您可以在帐号设置中点击二维码图标找到二维码。</string>
<string name="fragment_enter_public_key_edit_text_hint">输入对方的Session ID</string>
<string name="fragment_enter_public_key_explanation">用户可以通过进入帐号设置并点击共享Session ID来分享自己的Session ID或通过共享其二维码来分享其Session ID。</string>
<string name="fragment_enter_public_key_explanation">用户可以通过进入帐号设置并点击共享Session ID来分享自己的Session ID或通过共享其二维码来分享其Session ID。</string>
<string name="fragment_scan_qr_code_camera_access_explanation">Session需要摄像头访问权限才能扫描二维码</string>
<string name="fragment_scan_qr_code_grant_camera_access_button_title">授予摄像头访问权限</string>
<string name="activity_create_closed_group_title">创建私密群组</string>
<string name="activity_create_closed_group_edit_text_hint">输入群组名称</string>
<string name="activity_create_closed_group_explanation">私密群组最多支持 10 位成员,并提供与一对一对话相同的隐私保护。</string>
<string name="activity_create_closed_group_explanation">私密群组最多支持10位成员并提供与一对一对话相同的隐私保护。</string>
<string name="activity_create_closed_group_empty_state_message">您还没有任何联系人</string>
<string name="activity_create_closed_group_empty_state_button_title">开始对话</string>
<string name="activity_create_closed_group_group_name_missing_error">请输入群组名称</string>
<string name="activity_create_closed_group_group_name_too_long_error">请输入较短的群组名称</string>
<string name="activity_create_closed_group_not_enough_group_members_error">请选择至少 2 位小组成员</string>
<string name="activity_create_closed_group_too_many_group_members_error">私密群组成员不得超过 10 </string>
<string name="activity_create_closed_group_not_enough_group_members_error">请选择至少2位群组成员</string>
<string name="activity_create_closed_group_too_many_group_members_error">私密群组成员不得超过10个</string>
<string name="activity_create_closed_group_invalid_session_id_error">您群组中的一位成员的Session ID无效</string>
<string name="activity_join_public_chat_title">加入公开群组</string>
<string name="activity_join_public_chat_error">无法加入群组</string>
<string name="activity_join_public_chat_enter_group_url_tab_title">公开群组网址</string>
<string name="activity_join_public_chat_enter_group_url_tab_title">公开群组链接</string>
<string name="activity_join_public_chat_scan_qr_code_tab_title">扫描二维码</string>
<string name="activity_join_public_chat_scan_qr_code_explanation">扫描您想加入的公开群组的二维码</string>
<string name="fragment_enter_chat_url_edit_text_hint">输入一个公开群组网址</string>
<string name="fragment_enter_chat_url_edit_text_hint">输入公开群组链接</string>
<string name="activity_settings_title">设置</string>
<string name="activity_settings_display_name_edit_text_hint">输入显示的名称</string>
<string name="activity_settings_display_name_missing_error">选择一个显示名称</string>
<string name="activity_settings_invalid_display_name_error">选择一个仅包含 azAZ0-9 和 _ 字符的显示名称</string>
<string name="activity_settings_display_name_too_long_error">选择一个较短的显示名称</string>
<string name="activity_settings_display_name_edit_text_hint">输入您想显示的名称</string>
<string name="activity_settings_display_name_missing_error">设定一个名称</string>
<string name="activity_settings_invalid_display_name_error">设定一个仅包含 a-zA-Z0-9 和 _ 的名称</string>
<string name="activity_settings_display_name_too_long_error">设定一个较短的名称</string>
<string name="activity_settings_privacy_button_title">隐私</string>
<string name="activity_settings_notifications_button_title">通知</string>
<string name="activity_settings_chats_button_title">聊天</string>
@ -1389,44 +1389,44 @@
<string name="activity_settings_clear_all_data_button_title">清除数据</string>
<string name="activity_notification_settings_title">通知</string>
<string name="activity_notification_settings_style_section_title">通知风格类型</string>
<string name="activity_notification_settings_style_section_title">通知风格</string>
<string name="activity_notification_settings_content_section_title">通知内容</string>
<string name="activity_privacy_settings_title">隐私</string>
<string name="activity_chat_settings_title">聊天</string>
<string name="activity_chat_settings_title">会话</string>
<string name="activity_linked_devices_title">设备</string>
<string name="activity_linked_devices_multi_device_limit_reached_dialog_title">达到设备限制</string>
<string name="activity_linked_devices_multi_device_limit_reached_dialog_explanation">当前不允许链接多个设备。</string>
<string name="activity_linked_devices_unlinking_failed_message">无法断开链接设备。</string>
<string name="activity_linked_devices_unlinking_successful_message">您的设备已成功断开链接</string>
<string name="activity_linked_devices_linking_failed_message">无法链接设备。</string>
<string name="activity_linked_devices_empty_state_message">您尚未链接任何设备</string>
<string name="activity_linked_devices_empty_state_button_title">链接设备(测试版)</string>
<string name="activity_linked_devices_multi_device_limit_reached_dialog_explanation">当前不允许关联多个设备。</string>
<string name="activity_linked_devices_unlinking_failed_message">无法断开关联设备。</string>
<string name="activity_linked_devices_unlinking_successful_message">您的设备已成功取消关联</string>
<string name="activity_linked_devices_linking_failed_message">无法关联设备。</string>
<string name="activity_linked_devices_empty_state_message">您尚未关联任何设备</string>
<string name="activity_linked_devices_empty_state_button_title">关联设备(测试版)</string>
<string name="preferences_notifications_strategy_category_title">通知选项</string>
<string name="dialog_link_device_slave_mode_title_1">等待授权</string>
<string name="dialog_link_device_slave_mode_title_2">设备链接授权</string>
<string name="dialog_link_device_slave_mode_title_2">设备关联已授权</string>
<string name="dialog_link_device_slave_mode_explanation_1">请检查以下单词是否与您其他设备上显示的单词匹配。</string>
<string name="dialog_link_device_slave_mode_explanation_2">您的设备已成功链接</string>
<string name="dialog_link_device_slave_mode_explanation_2">您的设备已成功关联</string>
<string name="dialog_link_device_master_mode_title_1">等待设备</string>
<string name="dialog_link_device_master_mode_title_2">收到链接请求</string>
<string name="dialog_link_device_master_mode_title_3">授权设备链接</string>
<string name="dialog_link_device_master_mode_explanation_1">在其他设备上下载Session然后点击登陆页面屏幕底部的“链接到现有帐号”。如果您的其他设备上已有一个帐号,则必须先删除已有帐号。</string>
<string name="dialog_link_device_master_mode_title_2">收到关联请求</string>
<string name="dialog_link_device_master_mode_title_3">授权设备关联</string>
<string name="dialog_link_device_master_mode_explanation_1">在其他设备上下载Session然后点击登陆页面屏幕底部的“关联到现有帐号”。如果您的其他设备上已有一个帐号,则必须先删除已有帐号。</string>
<string name="dialog_link_device_master_mode_explanation_2">请检查以下单词是否与您其他设备上显示的单词匹配。</string>
<string name="dialog_link_device_master_mode_explanation_3">创建设备关联时,请耐心等待。这可能需要一分钟的时间。</string>
<string name="dialog_link_device_master_mode_explanation_3">创建设备关联时,请耐心等待。这可能需要一分钟左右的时间。</string>
<string name="dialog_link_device_master_mode_authorize_button_title">授权</string>
<string name="fragment_device_list_bottom_sheet_change_name_button_title">更换名</string>
<string name="fragment_device_list_bottom_sheet_unlink_device_button_title">断开设备链接</string>
<string name="fragment_device_list_bottom_sheet_change_name_button_title">更换名</string>
<string name="fragment_device_list_bottom_sheet_unlink_device_button_title">断开设备关联</string>
<string name="dialog_edit_device_name_edit_text_hint">输入名</string>
<string name="dialog_edit_device_name_edit_text_hint">输入名</string>
<string name="dialog_seed_title">您的恢复口令</string>
<string name="dialog_seed_explanation">这是您的恢复口令。有了它,您可以将Session ID还原或迁移到新设备上。</string>
<string name="dialog_seed_explanation">这是您的恢复口令。您可以通过该口令将Session ID还原或迁移到新设备上。</string>
<string name="dialog_clear_all_data_title">清除所有数据</string>
<string name="dialog_clear_all_data_explanation">这将永久删除您的消息、对话和联系人。</string>
@ -1434,13 +1434,13 @@
<string name="activity_qr_code_title">二维码</string>
<string name="activity_qr_code_view_my_qr_code_tab_title">查看我的二维码</string>
<string name="activity_qr_code_view_scan_qr_code_tab_title">扫描二维码</string>
<string name="activity_qr_code_view_scan_qr_code_explanation">扫描对方的二维码,与他们开始对话</string>
<string name="activity_qr_code_view_scan_qr_code_explanation">扫描对方的二维码以发起对话</string>
<string name="fragment_view_my_qr_code_explanation">这是您的二维码。其他用户可以对其进行扫描以开始对话。</string>
<string name="fragment_view_my_qr_code_explanation">这是您的二维码。其他用户可以对其进行扫描以发起与您的对话。</string>
<string name="fragment_view_my_qr_code_share_title">分享二维码</string>
<string name="session_reset_banner_message">您要恢复与%s的对话吗</string>
<string name="session_reset_banner_dismiss_button_title">解散</string>
<string name="session_reset_banner_dismiss_button_title">取消</string>
<string name="session_reset_banner_restore_button_title">恢复</string>
<string name="fragment_contact_selection_contacts_title">联系人</string>

View File

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

View File

@ -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<Long, Void, Void>() {
@Override
protected Void doInBackground(Long... params) {
Context context = ConversationActivity.this;
List<MarkedMessageInfo> 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<Void, Void, Long>() {
@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<OutgoingTextMessage, Void, Long>() {
@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

View File

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

View File

@ -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();

View File

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

View File

@ -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()) {

View File

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

View File

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

View File

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

View File

@ -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();

View File

@ -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<SendMessageResult> results = deliver(message, targets);
List<NetworkFailure> networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Address.fromSerialized(result.getAddress().getNumber()))).toList();

View File

@ -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());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Snode> {
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<List<Snode>> {
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<List<Snode>>) {
// 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<Snode>? {
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<String>? {
override fun getReceivedMessageHashValues(publicKey: String): Set<String>? {
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<String>) {
override fun setReceivedMessageHashValues(publicKey: String, newValue: Set<String>) {
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<DeviceLink> {
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

View File

@ -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<ClosedGroupSenderKey> {
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<String> {
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
}

View File

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

View File

@ -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<ClosedGroupSenderKey>, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : Kind()
class Info(val groupPublicKey: ByteArray, val name: String, val senderKeys: Collection<ClosedGroupSenderKey>, val members: Collection<ByteArray>, val admins: Collection<ByteArray>) : 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<ClosedGroupUpdateMessageSendJob> {
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)
}
}
}

View File

@ -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>): 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<ClosedGroupSenderKey> = 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<Address>(members.map { Address.fromSerialized(it) }),
null, null, LinkedList<Address>(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<String>, 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<ClosedGroupSenderKey> = 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<String>, 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<Address>(members.map { Address.fromSerialized(it) }),
null, null, LinkedList<Address>(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<List<Address>, Exception> {
if (GroupUtil.isRSSFeed(groupID)) { return Promise.of(listOf()) }
fun getMessageDestinations(context: Context, groupID: String): List<Address> {
if (GroupUtil.isRSSFeed(groupID)) { return listOf() }
if (GroupUtil.isOpenGroup(groupID)) {
val result = mutableListOf<Address>()
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<String>) {
// A closed group's members should never include slave devices
fun establishSessionsWithMembersIfNeeded(context: Context, members: Collection<String>) {
@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<String>, admins: Collection<String>, 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<String>, admins: Collection<String>, 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)
}
}

View File

@ -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<PushNullMessageSendJob> {
class Factory : Job.Factory<NullMessageSendJob> {
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)
}

View File

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

View File

@ -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.
*/

View File

@ -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<PushSessionRequestMessageSendJob> {
class Factory : Job.Factory<SessionRequestMessageSendJob> {
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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import net.sqlcipher.Cursor
import net.sqlcipher.database.SQLiteDatabase
import org.whispersystems.signalservice.internal.util.Base64
fun <T> SQLiteDatabase.get(table: String, query: String, arguments: Array<String>, get: (Cursor) -> T): T? {
fun <T> SQLiteDatabase.get(table: String, query: String?, arguments: Array<String>?, get: (Cursor) -> T): T? {
var cursor: Cursor? = null
try {
cursor = query(table, null, query, arguments, null, null, null)
@ -18,7 +18,7 @@ fun <T> SQLiteDatabase.get(table: String, query: String, arguments: Array<String
return null
}
fun <T> SQLiteDatabase.getAll(table: String, query: String, arguments: Array<String>, get: (Cursor) -> T): List<T> {
fun <T> SQLiteDatabase.getAll(table: String, query: String?, arguments: Array<String>?, get: (Cursor) -> T): List<T> {
val result = mutableListOf<T>()
var cursor: Cursor? = null
try {

View File

@ -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<MarkedMessageInfo> messageIds = threads.setRead(threadId, false);
MarkReadReceiver.process(context, messageIds);
}

View File

@ -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();