feat: add contacts expiry serialization/deserialization, more LGC, timestamps to add closed group encryption info (for latest tracking)

This commit is contained in:
0x330a 2023-03-09 12:27:58 +11:00
parent 0b4cff71e3
commit 3318b53a1f
No known key found for this signature in database
GPG Key ID: 267811D6E6A2698C
13 changed files with 156 additions and 33 deletions

View File

@ -458,9 +458,8 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
return ECKeyPair(DjbECPublicKey(keyPair.publicKey.serialize().removingIdPrefixIfNeeded()), DjbECPrivateKey(keyPair.privateKey.serialize()))
}
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) {
val database = databaseHelper.writableDatabase
val timestamp = Date().time.toString()
val index = "$groupPublicKey-$timestamp"
val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removingIdPrefixIfNeeded()
val encryptionKeyPairPrivateKey = encryptionKeyPair.privateKey.serialize().toHexString()

View File

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
import network.loki.messenger.libsession_util.*
import network.loki.messenger.libsession_util.util.BaseCommunityInfo
import network.loki.messenger.libsession_util.util.Conversation
import network.loki.messenger.libsession_util.util.UserPic
import org.session.libsession.avatars.AvatarHelper
@ -27,14 +28,19 @@ import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.*
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.DjbECPrivateKey
import org.session.libsignal.crypto.ecc.DjbECPublicKey
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
@ -49,7 +55,7 @@ import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.GroupManager
import org.thoughtcrime.securesms.groups.ClosedGroupManager
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.mms.PartAuthority
@ -119,7 +125,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
// recipient is open group
recipient.isOpenGroupRecipient -> {
val openGroupJoinUrl = getOpenGroup(threadId)?.joinURL ?: return
Conversation.Community.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
BaseCommunityInfo.parseFullUrl(openGroupJoinUrl)?.let { (base, room, pubKey) ->
config.getOrConstructCommunity(base, room, pubKey)
} ?: return
}
@ -269,6 +275,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
return DatabaseComponent.get(context).sessionJobDatabase().isJobCanceled(job)
}
override fun cancelPendingMessageSendJobs(threadID: Long) {
val jobDb = DatabaseComponent.get(context).sessionJobDatabase()
jobDb.cancelPendingMessageSendJobs(threadID)
}
override fun getAuthToken(room: String, server: String): String? {
val id = "$server.$room"
return DatabaseComponent.get(context).lokiAPIDatabase().getAuthToken(id)
@ -361,6 +372,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
private fun updateUserGroups(userGroups: UserGroupsConfig) {
val threadDb = DatabaseComponent.get(context).threadDatabase()
val localUserPublicKey = getUserPublicKey() ?: return Log.w(
"Loki-DBG",
"No user public key when trying to update user groups from config"
)
val communities = userGroups.allCommunityInfo()
val lgc = userGroups.allLegacyGroupInfo()
val allOpenGroups = getAllOpenGroups()
@ -378,7 +393,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
OpenGroupManager.delete(openGroup.server, openGroup.room, context)
}
// GroupManager.deleteGroup()
toDeleteClosedGroups.forEach { deleteGroup ->
val threadId = getThreadId(deleteGroup.encodedId)
if (threadId == null) {
Log.w("Loki-DBG", "Existing group had no thread to delete")
} else {
ClosedGroupManager.silentlyRemoveGroup(context,threadId,GroupUtil.doubleDecodeGroupId(deleteGroup.encodedId), deleteGroup.encodedId, localUserPublicKey, delete = true)
}
}
for (groupInfo in communities) {
val groupBaseCommunity = groupInfo.community
@ -407,8 +429,24 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
val admins = group.members.filter { it.value /*admin = true*/ }.keys.map { Address.fromSerialized(it) }
val groupId = GroupUtil.doubleEncodeGroupID(group.sessionId)
val title = group.name
val formationTimestamp = 0L
val formationTimestamp = SnodeAPI.nowWithOffset // TODO: formation timestamp for legacy ? current time?
createGroup(groupId, title, members, null, null, admins, formationTimestamp)
setProfileSharing(Address.fromSerialized(groupId), true)
// Add the group to the user's set of public keys to poll for
addClosedGroupPublicKey(group.sessionId)
// Store the encryption key pair
val keyPair = ECKeyPair(DjbECPublicKey(group.encPubKey), DjbECPrivateKey(group.encSecKey))
addClosedGroupEncryptionKeyPair(keyPair, group.sessionId, SnodeAPI.nowWithOffset)
// Set expiration timer
val expireTimer = group.expiration
setExpirationTimer(groupId, expireTimer)
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, group.sessionId, localUserPublicKey)
// Notify the user
val threadID = getOrCreateThreadIdFor(Address.fromSerialized(groupId))
insertOutgoingInfoMessage(context, groupId, SignalServiceGroup.Type.CREATION, title, members.map { it.serialize() }, admins.map { it.serialize() }, threadID, formationTimestamp)
// Start polling
ClosedGroupPollerV2.shared.startPolling(group.sessionId)
}
}
}
@ -696,8 +734,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
DatabaseComponent.get(context).lokiAPIDatabase().removeClosedGroupPublicKey(groupPublicKey)
}
override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
override fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long) {
DatabaseComponent.get(context).lokiAPIDatabase().addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey, timestamp)
}
override fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String) {
@ -716,7 +754,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
override fun setExpirationTimer(groupID: String, duration: Int) {
val recipient = Recipient.from(context, fromSerialized(groupID), false)
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration)
}
override fun setServerCapabilities(server: String, capabilities: List<String>) {
@ -917,6 +955,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper, private val configF
return threadDB.isPinned(threadID)
}
override fun deleteConversation(threadID: Long) {
val threadDB = DatabaseComponent.get(context).threadDatabase()
threadDB.deleteConversation(threadID)
}
override fun getAttachmentDataUri(attachmentId: AttachmentId): Uri {
return PartAuthority.getAttachmentDataUri(attachmentId)
}

View File

@ -514,7 +514,7 @@ public class ThreadDatabase extends Database {
public void setLastSeen(long threadId, long timestamp) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ContentValues contentValues = new ContentValues(1);
long lastSeenTime = timestamp == -1 ? SnodeAPI.INSTANCE.getNowWithOffset() : timestamp;
long lastSeenTime = timestamp == -1 ? SnodeAPI.getNowWithOffset() : timestamp;
contentValues.put(LAST_SEEN, lastSeenTime);
db.beginTransaction();
db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {String.valueOf(threadId)});

View File

@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.groups
import android.content.Context
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.utilities.Address
import org.thoughtcrime.securesms.ApplicationContext
object ClosedGroupManager {
fun silentlyRemoveGroup(context: Context, threadId: Long, groupPublicKey: String, groupID: String, userPublicKey: String, delete: Boolean = true) {
val storage = MessagingModuleConfiguration.shared.storage
storage.removeClosedGroupPublicKey(groupPublicKey)
// Remove the key pairs
storage.removeAllClosedGroupEncryptionKeyPairs(groupPublicKey)
// Mark the group as inactive
storage.setActive(groupID, false)
storage.removeMember(groupID, Address.fromSerialized(userPublicKey))
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Unsubscribe, groupPublicKey, userPublicKey)
// Stop polling
ClosedGroupPollerV2.shared.stopPolling(groupPublicKey)
storage.cancelPendingMessageSendJobs(threadId)
ApplicationContext.getInstance(context).messageNotifier.updateNotification(context)
if (delete) {
storage.deleteConversation(threadId)
}
}
}

View File

@ -13,8 +13,7 @@ inline session::config::Contacts *ptrToContacts(JNIEnv *env, jobject obj) {
inline jobject serialize_contact(JNIEnv *env, session::config::contact_info info) {
jclass contactClass = env->FindClass("network/loki/messenger/libsession_util/util/Contact");
jmethodID constructor = env->GetMethodID(contactClass, "<init>",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZLnetwork/loki/messenger/libsession_util/util/UserPic;I)V");
jmethodID constructor = env->GetMethodID(contactClass, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZLnetwork/loki/messenger/libsession_util/util/UserPic;ILnetwork/loki/messenger/libsession_util/util/ExpiryMode;)V");
jstring id = env->NewStringUTF(info.session_id.data());
jstring name = env->NewStringUTF(info.name.data());
jstring nickname = env->NewStringUTF(info.nickname.data());
@ -23,22 +22,27 @@ inline jobject serialize_contact(JNIEnv *env, session::config::contact_info info
approvedMe = info.approved_me;
blocked = info.blocked;
jobject profilePic = util::serialize_user_pic(env, info.profile_picture);
jobject returnObj = env->NewObject(contactClass, constructor, id, name, nickname, approved, approvedMe, blocked, profilePic, info.priority);
jobject returnObj = env->NewObject(contactClass, constructor, id, name, nickname, approved,
approvedMe, blocked, profilePic, info.priority,
util::serialize_expiry(env, info.exp_mode, info.exp_timer));
return returnObj;
}
inline session::config::contact_info deserialize_contact(JNIEnv *env, jobject info, session::config::Contacts *conf) {
inline session::config::contact_info
deserialize_contact(JNIEnv *env, jobject info, session::config::Contacts *conf) {
jclass contactClass = env->FindClass("network/loki/messenger/libsession_util/util/Contact");
jfieldID getId, getName, getNick, getApproved, getApprovedMe, getBlocked, getUserPic, getPriority;
jfieldID getId, getName, getNick, getApproved, getApprovedMe, getBlocked, getUserPic, getPriority, getExpiry;
getId = env->GetFieldID(contactClass, "id", "Ljava/lang/String;");
getName = env->GetFieldID(contactClass, "name", "Ljava/lang/String;");
getNick = env->GetFieldID(contactClass, "nickname", "Ljava/lang/String;");
getApproved = env->GetFieldID(contactClass, "approved", "Z");
getApprovedMe = env->GetFieldID(contactClass, "approvedMe", "Z");
getBlocked = env->GetFieldID(contactClass, "blocked", "Z");
getUserPic = env->GetFieldID(contactClass, "profilePicture", "Lnetwork/loki/messenger/libsession_util/util/UserPic;");
getUserPic = env->GetFieldID(contactClass, "profilePicture",
"Lnetwork/loki/messenger/libsession_util/util/UserPic;");
getPriority = env->GetFieldID(contactClass, "priority", "I");
getExpiry = env->GetFieldID(contactClass, "expiryMode", "Lnetwork/loki/messenger/libsession_util/util/ExpiryMode;");
jstring name, nickname, session_id;
session_id = static_cast<jstring>(env->GetObjectField(info, getId));
name = static_cast<jstring>(env->GetObjectField(info, getName));
@ -49,6 +53,9 @@ inline session::config::contact_info deserialize_contact(JNIEnv *env, jobject in
approvedMe = env->GetBooleanField(info, getApprovedMe);
blocked = env->GetBooleanField(info, getBlocked);
jobject user_pic = env->GetObjectField(info, getUserPic);
jobject expiry_mode = env->GetObjectField(info, getExpiry);
auto expiry_pair = util::deserialize_expiry(env, expiry_mode);
std::string url;
session::ustring key;
@ -91,6 +98,8 @@ inline session::config::contact_info deserialize_contact(JNIEnv *env, jobject in
}
contact_info.priority = priority;
contact_info.exp_mode = expiry_pair.first;
contact_info.exp_timer = std::chrono::seconds(expiry_pair.second);
return contact_info;
}

View File

@ -51,6 +51,7 @@ inline session::config::legacy_group_info deserialize_legacy_group_info(JNIEnv *
auto enc_pub_key_field = env->GetFieldID(clazz, "encPubKey", "[B");
auto enc_sec_key_field = env->GetFieldID(clazz, "encSecKey", "[B");
auto priority_field = env->GetFieldID(clazz, "priority", "I");
auto disappearing_timer_field = env->GetFieldID(clazz, "disappearingTimer", "J");
jstring id = static_cast<jstring>(env->GetObjectField(info, id_field));
jstring name = static_cast<jstring>(env->GetObjectField(info, name_field));
jobject members_map = env->GetObjectField(info, members_field);
@ -72,8 +73,7 @@ inline session::config::legacy_group_info deserialize_legacy_group_info(JNIEnv *
info_deserialized.hidden = hidden;
info_deserialized.enc_pubkey = enc_pub_key_bytes;
info_deserialized.enc_seckey = enc_sec_key_bytes;
// TODO: this
// info_deserialized.disappearing_timer
info_deserialized.disappearing_timer = std::chrono::seconds(env->GetLongField(info, disappearing_timer_field));
env->ReleaseStringUTFChars(id, id_bytes);
env->ReleaseStringUTFChars(name, name_bytes);
return info_deserialized;
@ -119,8 +119,8 @@ inline jobject serialize_legacy_group_info(JNIEnv *env, session::config::legacy_
bool hidden = info.hidden;
jclass legacy_group_class = env->FindClass("network/loki/messenger/libsession_util/util/GroupInfo$LegacyGroupInfo");
jmethodID constructor = env->GetMethodID(legacy_group_class, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Z[B[BI)V");
jobject serialized = env->NewObject(legacy_group_class, constructor, session_id, name, members, hidden, enc_pubkey, enc_seckey, priority);
jmethodID constructor = env->GetMethodID(legacy_group_class, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Z[B[BIJ)V");
jobject serialized = env->NewObject(legacy_group_class, constructor, session_id, name, members, hidden, enc_pubkey, enc_seckey, priority, (jlong) info.disappearing_timer.count());
return serialized;
}

View File

@ -67,6 +67,40 @@ namespace util {
return community;
}
jobject serialize_expiry(JNIEnv *env, const session::config::expiration_mode& mode, const std::chrono::seconds& time_seconds) {
jclass none = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$NONE");
jfieldID none_instance = env->GetStaticFieldID(none, "INSTANCE", "Lnetwork/loki/messenger/libsession_util/util/ExpiryMode$NONE;");
jclass after_send = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterSend");
jmethodID send_init = env->GetMethodID(after_send, "<init>", "(J)V");
jclass after_read = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterRead");
jmethodID read_init = env->GetMethodID(after_read, "<init>", "(J)V");
if (mode == session::config::expiration_mode::none) {
return env->GetStaticObjectField(none, none_instance);
} else if (mode == session::config::expiration_mode::after_send) {
return env->NewObject(after_send, send_init, time_seconds.count());
} else if (mode == session::config::expiration_mode::after_read) {
return env->NewObject(after_read, read_init, time_seconds.count());
}
return nullptr;
}
std::pair<session::config::expiration_mode, long> deserialize_expiry(JNIEnv *env, jobject expiry_mode) {
jclass parent = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode");
jclass after_read = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterRead");
jclass after_send = env->FindClass("network/loki/messenger/libsession_util/util/ExpiryMode$AfterSend");
jfieldID duration_seconds = env->GetFieldID(parent, "expirySeconds", "J");
jclass object_class = env->GetObjectClass(expiry_mode);
if (object_class == after_read) {
return std::pair(session::config::expiration_mode::after_read, env->GetLongField(expiry_mode, duration_seconds));
} else if (object_class == after_send) {
return std::pair(session::config::expiration_mode::after_send, env->GetLongField(expiry_mode, duration_seconds));
}
return std::pair(session::config::expiration_mode::none, 0);
}
}
extern "C"

View File

@ -7,6 +7,7 @@
#include "session/types.hpp"
#include "session/config/profile_pic.hpp"
#include "session/config/user_groups.hpp"
#include "session/config/expiring.hpp"
namespace util {
jbyteArray bytes_from_ustring(JNIEnv* env, session::ustring_view from_str);
@ -15,6 +16,8 @@ namespace util {
std::pair<jstring, jbyteArray> deserialize_user_pic(JNIEnv *env, jobject user_pic);
jobject serialize_base_community(JNIEnv *env, const session::config::community& base_community);
session::config::community deserialize_base_community(JNIEnv *env, jobject base_community);
jobject serialize_expiry(JNIEnv *env, const session::config::expiration_mode& mode, const std::chrono::seconds& time_seconds);
std::pair<session::config::expiration_mode, long> deserialize_expiry(JNIEnv *env, jobject expiry_mode);
}
#endif

View File

@ -2,11 +2,12 @@ package network.loki.messenger.libsession_util.util
data class Contact(
val id: String,
var name: String = "",
var nickname: String = "",
var approved: Boolean = false,
var approvedMe: Boolean = false,
var blocked: Boolean = false,
var profilePicture: UserPic = UserPic.DEFAULT,
var priority: Int = 0
val name: String = "",
val nickname: String = "",
val approved: Boolean = false,
val approvedMe: Boolean = false,
val blocked: Boolean = false,
val profilePicture: UserPic = UserPic.DEFAULT,
val priority: Int = 0,
val expiryMode: ExpiryMode
)

View File

@ -1,7 +1,7 @@
package network.loki.messenger.libsession_util.util
sealed class ExpiryMode(val expiryMinutes: Long) {
sealed class ExpiryMode(val expirySeconds: Long) {
object NONE: ExpiryMode(0)
class AfterSend(minutes: Long): ExpiryMode(minutes)
class AfterRead(minutes: Long): ExpiryMode(minutes)
class AfterSend(seconds: Long): ExpiryMode(seconds)
class AfterRead(seconds: Long): ExpiryMode(seconds)
}

View File

@ -11,7 +11,8 @@ sealed class GroupInfo {
val hidden: Boolean,
val encPubKey: ByteArray,
val encSecKey: ByteArray,
val priority: Int = 0
val priority: Int = 0,
val disappearingTimer: Long
): GroupInfo() {
companion object {
@Suppress("FunctionName")

View File

@ -56,6 +56,7 @@ interface StorageProtocol {
fun getConfigSyncJob(destination: Destination): Job?
fun resumeMessageSendJobIfNeeded(messageSendJobID: String)
fun isJobCanceled(job: Job): Boolean
fun cancelPendingMessageSendJobs(threadID: Long)
// Authorization
fun getAuthToken(room: String, server: String): String?
@ -130,7 +131,7 @@ interface StorageProtocol {
fun getAllActiveClosedGroupPublicKeys(): Set<String>
fun addClosedGroupPublicKey(groupPublicKey: String)
fun removeClosedGroupPublicKey(groupPublicKey: String)
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String, timestamp: Long)
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type,
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
@ -163,6 +164,7 @@ interface StorageProtocol {
fun getMessageCount(threadID: Long): Long
fun setPinned(threadID: Long, isPinned: Boolean)
fun isPinned(threadID: Long): Boolean
fun deleteConversation(threadID: Long)
// Contacts
fun getContactWithSessionID(sessionID: String): Contact?

View File

@ -748,7 +748,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
}
}
private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean {
private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean {
val oldMembers = group.members.map { it.serialize() }
// Check that the message isn't from before the group was created
if (group.formationTimestamp > sentTimestamp) {