diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 12ee0ca57f..cbbba696a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -73,7 +73,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } override fun setUserProfilePictureURL(newValue: String) { - val ourRecipient = Address.fromSerialized(getUserPublicKey()!!).let { + val ourRecipient = fromSerialized(getUserPublicKey()!!).let { Recipient.from(context, it, false) } TextSecurePreferences.setProfilePictureURL(context, newValue) @@ -103,7 +103,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun persist(message: VisibleMessage, quotes: QuoteModel?, linkPreview: List, groupPublicKey: String?, openGroupID: String?, attachments: List): Long? { var messageID: Long? = null - val senderAddress = Address.fromSerialized(message.sender!!) + val senderAddress = fromSerialized(message.sender!!) val isUserSender = (message.sender!! == getUserPublicKey()) val group: Optional = when { openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT)) @@ -117,9 +117,9 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, it.toSignalAttachment() } val targetAddress = if (isUserSender && !message.syncTarget.isNullOrEmpty()) { - Address.fromSerialized(message.syncTarget!!) + fromSerialized(message.syncTarget!!) } else if (group.isPresent) { - Address.fromSerialized(GroupUtil.getEncodedId(group.get())) + fromSerialized(GroupUtil.getEncodedId(group.get())) } else { senderAddress } @@ -291,6 +291,16 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return getAllOpenGroups().values.firstOrNull { it.server == server && it.room == room } } + override fun updateOpenGroupCapabilities(server: String, capabilities: List) { + getAllOpenGroups().values.filter { it.server == server } + .map { it.copy(capabilities = it.capabilities) } + .forEach(this::updateOpenGroup) + } + + override fun getOpenGroupServer(server: String): List { + return getAllOpenGroups().values.firstOrNull { it.server == server }?.capabilities ?: emptyList() + } + override fun isDuplicateMessage(timestamp: Long): Boolean { return getReceivedMessageTimestamps().contains(timestamp) } @@ -317,7 +327,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun getMessageIdInDatabase(timestamp: Long, author: String): Long? { val database = DatabaseComponent.get(context).mmsSmsDatabase() - val address = Address.fromSerialized(author) + val address = fromSerialized(author) return database.getMessageFor(timestamp, address)?.getId() } @@ -435,7 +445,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, sentTimestamp: Long) { val group = SignalServiceGroup(type, GroupUtil.getDecodedGroupIDAsData(groupID), SignalServiceGroup.GroupType.SIGNAL, name, members.toList(), null, admins.toList()) - val m = IncomingTextMessage(Address.fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true) + val m = IncomingTextMessage(fromSerialized(senderPublicKey), 1, sentTimestamp, "", Optional.of(group), 0, true) val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() val infoMessage = IncomingGroupMessage(m, groupID, updateData, true) val smsDB = DatabaseComponent.get(context).smsDatabase() @@ -444,7 +454,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection, admins: Collection, threadID: Long, sentTimestamp: Long) { val userPublicKey = getUserPublicKey() - val recipient = Recipient.from(context, Address.fromSerialized(groupID), false) + val recipient = Recipient.from(context, fromSerialized(groupID), false) val updateData = UpdateMessageData.buildGroupUpdate(type, name, members)?.toJSON() ?: "" val infoMessage = OutgoingGroupMediaMessage(recipient, updateData, groupID, null, sentTimestamp, 0, true, null, listOf(), listOf()) @@ -457,7 +467,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun isClosedGroup(publicKey: String): Boolean { val isClosedGroup = DatabaseComponent.get(context).lokiAPIDatabase().isClosedGroup(publicKey) - val address = Address.fromSerialized(publicKey) + val address = fromSerialized(publicKey) return address.isClosedGroup || isClosedGroup } @@ -514,6 +524,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, return DatabaseComponent.get(context).lokiThreadDatabase().getAllOpenGroups() } + override fun updateOpenGroup(openGroup: OpenGroup) { + OpenGroupManager.updateOpenGroup(openGroup, context) + } + override fun getAllGroups(): List { return DatabaseComponent.get(context).groupDatabase().allGroups } @@ -534,20 +548,20 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, override fun getOrCreateThreadIdFor(publicKey: String, groupPublicKey: String?, openGroupID: String?): Long { val database = DatabaseComponent.get(context).threadDatabase() - if (!openGroupID.isNullOrEmpty()) { - val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false) - return database.getThreadIdIfExistsFor(recipient) + return if (!openGroupID.isNullOrEmpty()) { + val recipient = Recipient.from(context, fromSerialized(GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())), false) + database.getThreadIdIfExistsFor(recipient) } else if (!groupPublicKey.isNullOrEmpty()) { - val recipient = Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false) - return database.getOrCreateThreadIdFor(recipient) + val recipient = Recipient.from(context, fromSerialized(GroupUtil.doubleEncodeGroupID(groupPublicKey)), false) + database.getOrCreateThreadIdFor(recipient) } else { - val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) - return database.getOrCreateThreadIdFor(recipient) + val recipient = Recipient.from(context, fromSerialized(publicKey), false) + database.getOrCreateThreadIdFor(recipient) } } override fun getThreadId(publicKeyOrOpenGroupID: String): Long? { - val address = Address.fromSerialized(publicKeyOrOpenGroupID) + val address = fromSerialized(publicKeyOrOpenGroupID) return getThreadId(address) } @@ -595,7 +609,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, val recipientDatabase = DatabaseComponent.get(context).recipientDatabase() val threadDatabase = DatabaseComponent.get(context).threadDatabase() for (contact in contacts) { - val address = Address.fromSerialized(contact.publicKey) + val address = fromSerialized(contact.publicKey) val recipient = Recipient.from(context, address, true) if (!contact.profilePicture.isNullOrEmpty()) { recipientDatabase.setProfileAvatar(recipient, contact.profilePicture) @@ -725,11 +739,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } override fun setLastInboxMessageId(server: String, messageId: Long) { - // TODO("Not yet implemented") + } override fun removeLastInboxMessageId(server: String) { - // TODO("Not yet implemented") + } override fun getLastOutboxMessageId(server: String): Long? { @@ -737,11 +751,11 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, } override fun setLastOutboxMessageId(server: String, messageId: Long) { - // TODO("Not yet implemented") + } override fun removeLastOutboxMessageId(server: String) { - // TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt index 920ccaffd6..6e752c8ad2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -124,4 +124,11 @@ object OpenGroupManager { val publicKey = url.queryParameter("public_key") ?: return add(server.toString().removeSuffix("/"), room, publicKey, context) } + + fun updateOpenGroup(openGroup: OpenGroup, context: Context) { + val threadDB = DatabaseComponent.get(context).lokiThreadDatabase() + val openGroupID = "${openGroup.server}.${openGroup.room}" + val threadID = GroupManager.getOpenGroupThreadID(openGroupID, context) + threadDB.setOpenGroupChat(openGroup, threadID) + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index 7282d01c26..cc5806f6b0 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -55,10 +55,13 @@ interface StorageProtocol { // Open Groups fun getAllOpenGroups(): Map + fun updateOpenGroup(openGroup: OpenGroup) fun getOpenGroup(threadId: Long): OpenGroup? fun addOpenGroup(urlAsString: String) fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) fun getOpenGroup(room: String, server: String): OpenGroup? + fun updateOpenGroupCapabilities(server: String, capabilities: List) + fun getOpenGroupServer(server: String): List // Open Group Public Keys fun getOpenGroupPublicKey(server: String): String? diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt index a6827a1548..6412749141 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt @@ -16,8 +16,7 @@ sealed class Endpoint(val value: String) { // Messages - data class RoomMessage(val roomToken: String) : - Endpoint("room/$roomToken/message") + data class RoomMessage(val roomToken: String) : Endpoint("room/$roomToken/message") data class RoomMessageIndividual(val roomToken: String, val messageId: Long) : Endpoint("room/$roomToken/message/$messageId") @@ -42,8 +41,7 @@ sealed class Endpoint(val value: String) { data class RoomUnpinMessage(val roomToken: String, val messageId: Long) : Endpoint("room/$roomToken/unpin/$messageId") - data class RoomUnpinAll(val roomToken: String) : - Endpoint("room/$roomToken/unpin/all") + data class RoomUnpinAll(val roomToken: String) : Endpoint("room/$roomToken/unpin/all") // Files diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt index e6d3245fb8..4f2825ae59 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory import com.goterl.lazysodium.LazySodiumAndroid import com.goterl.lazysodium.SodiumAndroid import com.goterl.lazysodium.interfaces.GenericHash +import com.goterl.lazysodium.interfaces.Sign import java.util.concurrent.TimeUnit import kotlinx.coroutines.flow.MutableSharedFlow import nl.komponents.kovenant.Promise @@ -194,8 +195,7 @@ object OpenGroupApi { * Always `true` under normal circumstances. You might want to disable * this when running over Lokinet. */ - val useOnionRouting: Boolean = true, - val isBlinded: Boolean = true + val useOnionRouting: Boolean = true ) private fun createBody(parameters: Any?): RequestBody? { @@ -223,6 +223,7 @@ object OpenGroupApi { } } fun execute(): Promise { + val serverCapabilities = MessagingModuleConfiguration.shared.storage.getOpenGroupServer(request.server) val publicKey = MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server) ?: return Promise.ofFail(Error.NoPublicKey) @@ -233,32 +234,33 @@ object OpenGroupApi { val nonce = sodium.nonce(16) val timestamp = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) var pubKey = "" - var signature = ByteArray(0) - if (request.isBlinded) { + var signature = ByteArray(Sign.BYTES) + var bodyHash = ByteArray(0) + if (request.parameters != null) { + val parameterBytes = JsonUtil.toJson(request.parameters).toByteArray() + val parameterHash = ByteArray(GenericHash.BYTES_MAX) + if (sodium.cryptoGenericHash( + parameterHash, + parameterHash.size, + parameterBytes, + parameterBytes.size.toLong() + )) { + bodyHash = parameterHash + } + } + val messageBytes = Hex.fromStringCondensed(publicKey) + .plus(nonce) + .plus("$timestamp".toByteArray(Charsets.US_ASCII)) + .plus(request.verb.rawValue.toByteArray()) + .plus(urlRequest.encodedPath().toByteArray()) + .plus(bodyHash) + if (serverCapabilities.contains("blind")) { SodiumUtilities.blindedKeyPair(publicKey, ed25519KeyPair)?.let { keyPair -> pubKey = SodiumUtilities.SessionId( SodiumUtilities.IdPrefix.BLINDED, keyPair.publicKey.asBytes ).hexString - var bodyHash = ByteArray(0) - if (request.parameters != null) { - val parameterBytes = JsonUtil.toJson(request.parameters).toByteArray() - val parameterHash = ByteArray(GenericHash.BYTES_MAX) - if (sodium.cryptoGenericHash( - parameterHash, - parameterHash.size, - parameterBytes, - parameterBytes.size.toLong() - )) { - bodyHash = parameterHash - } - } - val messageBytes = Hex.fromStringCondensed(publicKey) - .plus(nonce) - .plus("$timestamp".toByteArray(Charsets.US_ASCII)) - .plus(request.verb.rawValue.toByteArray()) - .plus(urlRequest.encodedPath().toByteArray()) - .plus(bodyHash) + signature = SodiumUtilities.sogsSignature( messageBytes, ed25519KeyPair.secretKey.asBytes, @@ -271,7 +273,7 @@ object OpenGroupApi { SodiumUtilities.IdPrefix.UN_BLINDED, ed25519KeyPair.publicKey.asBytes ).hexString - signature = ByteArray(0) + sodium.cryptoSignDetached(signature, messageBytes, messageBytes.size.toLong(), ed25519KeyPair.secretKey.asBytes) } headers["X-SOGS-Nonce"] = encodeBytes(nonce) headers["X-SOGS-Timestamp"] = "$timestamp" @@ -415,7 +417,7 @@ object OpenGroupApi { @JvmStatic fun deleteMessage(serverID: Long, room: String, server: String): Promise { val request = - Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID") + Request(verb = DELETE, room = room, server = server, endpoint = Endpoint.RoomMessageIndividual(room, serverID)) return send(request).map { Log.d("Loki", "Message deletion successful.") } @@ -568,49 +570,51 @@ object OpenGroupApi { } ) } - requests.add( - if (lastInboxMessageId == null) { - BatchRequestInfo( - request = BatchRequest( - method = "GET", - path = "/inbox" - ), - endpoint = Endpoint.Inbox, - responseType = object : TypeReference>() {} - ) - } else { - BatchRequestInfo( - request = BatchRequest( - method = "GET", - path = "/inbox/since/$lastInboxMessageId" - ), - endpoint = Endpoint.InboxSince(lastInboxMessageId), - responseType = object : TypeReference>() {} - ) - } - ) - requests.add( - if (lastOutboxMessageId == null) { - BatchRequestInfo( - request = BatchRequest( - method = "GET", - path = "/outbox" - ), - endpoint = Endpoint.Outbox, - responseType = object : TypeReference>() {} - ) - } else { - BatchRequestInfo( - request = BatchRequest( - method = "GET", - path = "/outbox/since/$lastOutboxMessageId" - ), - endpoint = Endpoint.OutboxSince(lastOutboxMessageId), - responseType = object : TypeReference>() {} - ) - } - ) - + val serverCapabilities = storage.getOpenGroupServer(server) + if (serverCapabilities.contains("blind")) { + requests.add( + if (lastInboxMessageId == null) { + BatchRequestInfo( + request = BatchRequest( + method = "GET", + path = "/inbox" + ), + endpoint = Endpoint.Inbox, + responseType = object : TypeReference>() {} + ) + } else { + BatchRequestInfo( + request = BatchRequest( + method = "GET", + path = "/inbox/since/$lastInboxMessageId" + ), + endpoint = Endpoint.InboxSince(lastInboxMessageId), + responseType = object : TypeReference>() {} + ) + } + ) + requests.add( + if (lastOutboxMessageId == null) { + BatchRequestInfo( + request = BatchRequest( + method = "GET", + path = "/outbox" + ), + endpoint = Endpoint.Outbox, + responseType = object : TypeReference>() {} + ) + } else { + BatchRequestInfo( + request = BatchRequest( + method = "GET", + path = "/outbox/since/$lastOutboxMessageId" + ), + endpoint = Endpoint.OutboxSince(lastOutboxMessageId), + responseType = object : TypeReference>() {} + ) + } + ) + } return parallelBatch(server, requests) } @@ -666,7 +670,7 @@ object OpenGroupApi { fun getDefaultRoomsIfNeeded(): Promise, Exception> { val storage = MessagingModuleConfiguration.shared.storage storage.setOpenGroupPublicKey(defaultServer, defaultServerPublicKey) - return getAllRooms(defaultServer).map { groups -> + return getAllRooms().map { groups -> val earlyGroups = groups.map { group -> DefaultGroup(group.token, group.name, null) } @@ -705,11 +709,11 @@ object OpenGroupApi { } } - private fun getAllRooms(server: String): Promise, Exception> { + private fun getAllRooms(): Promise, Exception> { val request = Request( verb = GET, room = null, - server = server, + server = defaultServer, endpoint = Endpoint.Rooms ) return send(request).map { response -> diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index ad13ae474a..fe4d19a6bf 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -58,12 +58,17 @@ class OpenGroupPoller(private val server: String, private val executorService: S is Endpoint.RoomPollInfo -> { handleRoomPollInfo(server, response.endpoint.roomToken, response.body as OpenGroupApi.RoomPollInfo) } - is Endpoint.RoomMessagesRecent -> { + is Endpoint.RoomMessagesRecent -> { handleMessages(server, response.endpoint.roomToken, response.body as List) } - is Endpoint.Inbox, Endpoint.Outbox -> { - val fromOutbox = response.endpoint.value.startsWith("outbox", ignoreCase = true) - handleDirectMessages(server, fromOutbox, response.body as List) + is Endpoint.RoomMessagesSince -> { + handleMessages(server, response.endpoint.roomToken, response.body as List) + } + is Endpoint.Inbox, is Endpoint.InboxSince -> { + handleDirectMessages(server, false, response.body as List) + } + is Endpoint.Outbox, is Endpoint.OutboxSince -> { + handleDirectMessages(server, true, response.body as List) } } if (secondToLastJob == null && !isCaughtUp) { @@ -77,7 +82,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S private fun handleCapabilities(server: String, capabilities: OpenGroupApi.Capabilities) { val storage = MessagingModuleConfiguration.shared.storage - storage.setOpenGroupSever(server, capabilities.capabilities) + storage.updateOpenGroupCapabilities(server, capabilities.capabilities) } private fun handleRoomPollInfo( @@ -102,7 +107,7 @@ class OpenGroupPoller(private val server: String, private val executorService: S capabilities = listOf() ) // - Open Group changes - storage.setOpenGroup(openGroup) + storage.updateOpenGroup(openGroup) // - User Count storage.setUserCount(roomToken, server, pollInfo.active_users) @@ -123,7 +128,8 @@ class OpenGroupPoller(private val server: String, private val executorService: S messages: List ) { val openGroupId = "$server.$roomToken" - val msgs = messages.map { + val (additions, deletions) = messages.sortedBy { it.seqno }.partition { it.data.isNotBlank() } + handleNewMessages(roomToken, openGroupId, additions.map { OpenGroupMessageV2( serverID = it.id, sender = it.session_id, @@ -131,8 +137,10 @@ class OpenGroupPoller(private val server: String, private val executorService: S base64EncodedData = it.data, base64EncodedSignature = it.signature ) - } - handleNewMessages(roomToken, openGroupId, msgs) + }) + handleDeletedMessages(roomToken, openGroupId, deletions.map { + OpenGroupApi.MessageDeletion(it.id, it.seqno) + }) } private fun handleDirectMessages(