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