This commit is contained in:
charles 2022-05-12 15:05:44 +10:00
parent 46a92457bc
commit 55589b6893
12 changed files with 105 additions and 74 deletions

View File

@ -7,18 +7,15 @@ 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.database.LokiAPIDatabaseProtocol
import org.session.libsignal.utilities.*
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.PublicKeyValidation
import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.removingIdPrefixIfNeeded
import org.session.libsignal.utilities.toHexString
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import java.util.*
import kotlin.Array
import kotlin.Boolean
import kotlin.Int
import kotlin.Long
import kotlin.Pair
import kotlin.String
import kotlin.arrayOf
import kotlin.to
import java.util.Date
class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper), LokiAPIDatabaseProtocol {
@ -95,13 +92,18 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
public val groupPublicKey = "group_public_key"
@JvmStatic
val createClosedGroupPublicKeysTable = "CREATE TABLE $closedGroupPublicKeysTable ($groupPublicKey STRING PRIMARY KEY)"
// Open group server capabilities
private val serverCapabilitiesTable = "open_group_server_capabilities"
private val capabilities = "capabilities"
@JvmStatic
val createServerCapabilitiesCommand = "CREATE TABLE $serverCapabilitiesTable($server STRING PRIMARY KEY, $capabilities STRING)"
// Last inbox message server IDs
private val lastInboxMessageServerIdTable = "loki_api_last_inbox_message_server_id_cache"
private val lastInboxMessageServerIdTable = "open_group_last_inbox_message_server_id_cache"
private val lastInboxMessageServerId = "last_inbox_message_server_id"
@JvmStatic
val createLastInboxMessageServerIdCommand = "CREATE TABLE $lastInboxMessageServerIdTable($server STRING PRIMARY KEY, $lastInboxMessageServerId INTEGER DEFAULT 0)"
// Last outbox message server IDs
private val lastOutboxMessageServerIdTable = "loki_api_last_outbox_message_server_id_cache"
private val lastOutboxMessageServerIdTable = "open_group_last_outbox_message_server_id_cache"
private val lastOutboxMessageServerId = "last_outbox_message_server_id"
@JvmStatic
val createLastOutboxMessageServerIdCommand = "CREATE TABLE $lastOutboxMessageServerIdTable($server STRING PRIMARY KEY, $lastOutboxMessageServerId INTEGER DEFAULT 0)"
@ -450,6 +452,19 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
database.delete(closedGroupPublicKeysTable, "${Companion.groupPublicKey} = ?", wrap(groupPublicKey))
}
fun setServerCapabilities(serverName: String, serverCapabilities: List<String>) {
val database = databaseHelper.writableDatabase
val row = wrap(mapOf(server to serverName, capabilities to serverCapabilities.joinToString(",")))
database.insertOrUpdate(serverCapabilitiesTable, row, "$server = ?", wrap(serverName))
}
fun getServerCapabilities(serverName: String): List<String> {
val database = databaseHelper.writableDatabase
return database.get(serverCapabilitiesTable, "$server = ?", wrap(serverName)) { cursor ->
cursor.getString(server)
}?.split(",") ?: emptyList()
}
fun setLastInboxMessageId(serverName: String, newValue: Long) {
val database = databaseHelper.writableDatabase
val row = wrap(mapOf(server to serverName, lastInboxMessageServerId to newValue.toString()))

View File

@ -292,16 +292,6 @@ 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<String>) {
getAllOpenGroups().values.filter { it.server == server }
.map { it.copy(capabilities = it.capabilities) }
.forEach(this::updateOpenGroup)
}
override fun getOpenGroupServer(server: String): List<String> {
return getAllOpenGroups().values.firstOrNull { it.server == server }?.capabilities ?: emptyList()
}
override fun isDuplicateMessage(timestamp: Long): Boolean {
return getReceivedMessageTimestamps().contains(timestamp)
}
@ -521,6 +511,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
}
override fun setServerCapabilities(server: String, capabilities: List<String>) {
return DatabaseComponent.get(context).lokiAPIDatabase().setServerCapabilities(server, capabilities)
}
override fun getServerCapabilities(server: String): List<String> {
return DatabaseComponent.get(context).lokiAPIDatabase().getServerCapabilities(server)
}
override fun getAllOpenGroups(): Map<Long, OpenGroup> {
return DatabaseComponent.get(context).lokiThreadDatabase().getAllOpenGroups()
}

View File

@ -126,6 +126,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(LokiAPIDatabase.getCreateOpenGroupProfilePictureTableCommand());
db.execSQL(LokiAPIDatabase.getCreateClosedGroupEncryptionKeyPairsTable());
db.execSQL(LokiAPIDatabase.getCreateClosedGroupPublicKeysTable());
db.execSQL(LokiAPIDatabase.getCreateServerCapabilitiesCommand());
db.execSQL(LokiAPIDatabase.getCreateLastInboxMessageServerIdCommand());
db.execSQL(LokiAPIDatabase.getCreateLastOutboxMessageServerIdCommand());
db.execSQL(LokiMessageDatabase.getCreateMessageIDTableCommand());
@ -341,6 +342,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
}
if (oldVersion < lokiV33) {
db.execSQL(LokiAPIDatabase.getCreateServerCapabilitiesCommand());
db.execSQL(LokiAPIDatabase.getCreateLastInboxMessageServerIdCommand());
db.execSQL(LokiAPIDatabase.getCreateLastOutboxMessageServerIdCommand());
}

View File

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import nl.komponents.kovenant.functional.map
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.thoughtcrime.securesms.util.State
@ -13,8 +14,10 @@ typealias GroupState = State<DefaultGroups>
class DefaultGroupsViewModel : ViewModel() {
init {
OpenGroupApi.getDefaultServerCapabilities().map {
OpenGroupApi.getDefaultRoomsIfNeeded()
}
}
val defaultRooms = OpenGroupApi.defaultRooms.map<DefaultGroups, GroupState> {
State.Success(it)

View File

@ -68,14 +68,17 @@ object OpenGroupManager {
storage.removeLastOutboxMessageId(server)
// Store the public key
storage.setOpenGroupPublicKey(server,publicKey)
// Get capabilities and room info
val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(room, server).get()
// Get capabilities
val capabilities = OpenGroupApi.getCapabilities(server).get()
storage.setServerCapabilities(server, capabilities.capabilities)
// Get room info
val info = OpenGroupApi.getRoomInfo(room, server).get()
storage.setUserCount(room, server, info.activeUsers)
// Create the group locally if not available already
if (threadID < 0) {
threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId
}
val openGroup = OpenGroup(server, room, info.name, info.infoUpdates, publicKey, capabilities.capabilities)
val openGroup = OpenGroup(server, room, info.name, info.infoUpdates, publicKey)
threadDB.setOpenGroupChat(openGroup, threadID)
// Start the poller if needed
pollers[server]?.startIfNeeded() ?: run {

View File

@ -54,6 +54,10 @@ interface StorageProtocol {
fun setAuthToken(room: String, server: String, newValue: String)
fun removeAuthToken(room: String, server: String)
// Servers
fun setServerCapabilities(server: String, capabilities: List<String>)
fun getServerCapabilities(server: String): List<String>
// Open Groups
fun getAllOpenGroups(): Map<Long, OpenGroup>
fun updateOpenGroup(openGroup: OpenGroup)
@ -61,8 +65,6 @@ interface StorageProtocol {
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<String>)
fun getOpenGroupServer(server: String): List<String>
// Open Group Public Keys
fun getOpenGroupPublicKey(server: String): String?

View File

@ -20,9 +20,9 @@ sealed class Destination {
class OpenGroup(
val roomToken: String,
val server: String,
val whisperTo: String? = null,
val whisperTo: List<String>? = null,
val whisperMods: Boolean = false,
val fileIds: List<String> = emptyList()
val fileIds: List<String>? = null
) : Destination()
class OpenGroupInbox(
@ -33,7 +33,7 @@ sealed class Destination {
companion object {
fun from(address: Address): Destination {
fun from(address: Address, fileIds: List<String>? = null): Destination {
return when {
address.isContact -> {
Contact(address.contactIdentifier())
@ -46,11 +46,9 @@ sealed class Destination {
address.isOpenGroup -> {
val storage = MessagingModuleConfiguration.shared.storage
val threadID = storage.getThreadId(address)!!
when (val openGroup = storage.getOpenGroup(threadID)) {
is org.session.libsession.messaging.open_groups.OpenGroup
-> LegacyOpenGroup(openGroup.room, openGroup.server)
else -> throw Exception("Missing open group for thread with ID: $threadID.")
}
storage.getOpenGroup(threadID)?.let {
OpenGroup(roomToken = it.room, server = it.server, fileIds = fileIds)
} ?: throw Exception("Missing open group for thread with ID: $threadID.")
}
address.isOpenGroupInbox -> {
val groupInboxId = GroupUtil.getDecodedGroupID(address.serialize()).split(".")

View File

@ -11,17 +11,15 @@ data class OpenGroup(
val name: String,
val publicKey: String,
val infoUpdates: Int,
val capabilities: List<String>
) {
constructor(server: String, room: String, name: String, infoUpdates: Int, publicKey: String, capabilities: List<String>) : this(
constructor(server: String, room: String, name: String, infoUpdates: Int, publicKey: String) : this(
server = server,
room = room,
id = "$server.$room",
name = name,
publicKey = publicKey,
infoUpdates = infoUpdates,
capabilities = capabilities,
)
companion object {
@ -36,7 +34,7 @@ data class OpenGroup(
val publicKey = json.get("publicKey").asText()
val infoUpdates = json.get("infoUpdates")?.asText()?.toIntOrNull() ?: 0
val capabilities = json.get("capabilities")?.asText()?.split(",") ?: emptyList()
OpenGroup(server, room, displayName, infoUpdates, publicKey, capabilities)
OpenGroup(server, room, displayName, infoUpdates, publicKey)
} catch (e: Exception) {
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
null
@ -51,7 +49,6 @@ data class OpenGroup(
"displayName" to name,
"publicKey" to publicKey,
"infoUpdates" to infoUpdates.toString(),
"capabilities" to capabilities.joinToString(","),
)
val joinURL: String get() = "$server/$room?public_key=$publicKey"

View File

@ -184,6 +184,15 @@ object OpenGroupApi {
val signature: String? = null
)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class SendMessageRequest(
val data: String? = null,
val signature: String? = null,
val whisperTo: List<String>? = null,
val whisperMods: Boolean? = null,
val files: List<String>? = null
)
data class MessageDeletion(
@JsonProperty("id")
val id: Long = 0,
@ -242,7 +251,7 @@ object OpenGroupApi {
}
}
fun execute(): Promise<OnionResponse, Exception> {
val serverCapabilities = MessagingModuleConfiguration.shared.storage.getOpenGroupServer(request.server)
val serverCapabilities = MessagingModuleConfiguration.shared.storage.getServerCapabilities(request.server)
val publicKey =
MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
?: return Promise.ofFail(Error.NoPublicKey)
@ -364,16 +373,25 @@ object OpenGroupApi {
fun send(
message: OpenGroupMessage,
room: String,
server: String
server: String,
whisperTo: List<String>? = null,
whisperMods: Boolean? = null,
fileIds: List<String>? = null
): Promise<OpenGroupMessage, Exception> {
val signedMessage = message.sign(room, server, fallbackSigningType = IdPrefix.STANDARD) ?: return Promise.ofFail(Error.SigningFailed)
val jsonMessage = signedMessage.toJSON()
val messageRequest = SendMessageRequest(
data = signedMessage.base64EncodedData,
signature = signedMessage.base64EncodedSignature,
whisperTo = whisperTo,
whisperMods = whisperMods,
files = fileIds
)
val request = Request(
verb = POST,
room = room,
server = server,
endpoint = Endpoint.RoomMessage(room),
parameters = jsonMessage
parameters = messageRequest
)
return getResponseBodyJson(request).map { json ->
@Suppress("UNCHECKED_CAST") val rawMessage = json as? Map<String, Any>
@ -593,7 +611,7 @@ object OpenGroupApi {
}
)
}
val serverCapabilities = storage.getOpenGroupServer(server)
val serverCapabilities = storage.getServerCapabilities(server)
if (serverCapabilities.contains("blind")) {
requests.add(
if (lastInboxMessageId == null) {
@ -690,6 +708,13 @@ object OpenGroupApi {
}
}
fun getDefaultServerCapabilities(): Promise<Capabilities, Exception> {
return getCapabilities(defaultServer).map {
MessagingModuleConfiguration.shared.storage.setServerCapabilities(defaultServer, it.capabilities)
it
}
}
fun getDefaultRoomsIfNeeded(): Promise<List<DefaultGroup>, Exception> {
val storage = MessagingModuleConfiguration.shared.storage
storage.setOpenGroupPublicKey(defaultServer, defaultServerPublicKey)
@ -755,6 +780,13 @@ object OpenGroupApi {
}
}
fun getCapabilities(server: String): Promise<Capabilities, Exception> {
val request = Request(verb = GET, room = null, server = server, endpoint = Endpoint.Capabilities)
return getResponseBody(request).map { response ->
JsonUtil.fromJson(response, Capabilities::class.java)
}
}
fun getCapabilitiesAndRoomInfo(room: String, server: String): Promise<Pair<Capabilities, RoomInfo>, Exception> {
val requests = mutableListOf<BatchRequestInfo<*>>(
BatchRequestInfo(

View File

@ -49,8 +49,9 @@ data class OpenGroupMessage(
if (base64EncodedData.isEmpty()) return null
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null
val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(room, server) ?: return null
val serverCapabilities = MessagingModuleConfiguration.shared.storage.getServerCapabilities(server)
val signature = when {
openGroup.capabilities.contains("blind") -> {
serverCapabilities.contains("blind") -> {
val blindedKeyPair = SodiumUtilities.blindedKeyPair(openGroup.publicKey, userEdKeyPair) ?: return null
SodiumUtilities.sogsSignature(
decode(base64EncodedData),

View File

@ -205,9 +205,10 @@ object MessageSender {
message.sentTimestamp = System.currentTimeMillis()
}
val openGroup = storage.getOpenGroup(message.threadID!!)
val serverCapabilities = storage.getServerCapabilities(openGroup?.server!!)
val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
val blindedKeyPair = SodiumUtilities.blindedKeyPair(openGroup?.publicKey!!, userEdKeyPair)
message.sender = if (openGroup.capabilities.contains("blind") && blindedKeyPair != null) {
val blindedKeyPair = SodiumUtilities.blindedKeyPair(openGroup.publicKey, userEdKeyPair)
message.sender = if (serverCapabilities.contains("blind") && blindedKeyPair != null) {
SessionId(IdPrefix.BLINDED, blindedKeyPair.publicKey.asBytes).hexString
} else {
SessionId(IdPrefix.UN_BLINDED, userEdKeyPair.publicKey.asBytes).hexString
@ -230,29 +231,9 @@ object MessageSender {
}
}
when (destination) {
is Destination.LegacyOpenGroup -> {
message.recipient = "${destination.server}.${destination.roomToken}"
// Validate the message
if (message !is VisibleMessage || !message.isValid()) {
throw Error.InvalidMessage
}
val messageBody = message.toProto()?.toByteArray()!!
val plaintext = PushTransportDetails.getPaddedMessageBody(messageBody)
val openGroupMessage = OpenGroupMessage(
sender = message.sender,
sentTimestamp = message.sentTimestamp!!,
base64EncodedData = Base64.encodeBytes(plaintext),
)
OpenGroupApi.send(openGroupMessage, destination.roomToken, destination.server).success {
message.openGroupServerMessageID = it.serverID
handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = it.sentTimestamp)
deferred.resolve(Unit)
}.fail {
handleFailure(it)
}
}
is Destination.OpenGroup -> {
message.recipient = "${destination.server}.${destination.roomToken}"
val whisperMods = if (destination.whisperTo.isNullOrEmpty() && destination.whisperMods) "mods" else null
message.recipient = "${destination.server}.${destination.roomToken}.${destination.whisperTo}.$whisperMods"
// Validate the message
if (message !is VisibleMessage || !message.isValid()) {
throw Error.InvalidMessage
@ -264,7 +245,7 @@ object MessageSender {
sentTimestamp = message.sentTimestamp!!,
base64EncodedData = Base64.encodeBytes(plaintext),
)
OpenGroupApi.send(openGroupMessage, destination.roomToken, destination.server).success {
OpenGroupApi.send(openGroupMessage, destination.roomToken, destination.server, destination.whisperTo, destination.whisperMods, destination.fileIds).success {
message.openGroupServerMessageID = it.serverID
handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = it.sentTimestamp)
deferred.resolve(Unit)

View File

@ -86,7 +86,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.updateOpenGroupCapabilities(server, capabilities.capabilities)
storage.setServerCapabilities(server, capabilities.capabilities)
}
private fun handleRoomPollInfo(
@ -105,7 +105,6 @@ class OpenGroupPoller(private val server: String, private val executorService: S
name = pollInfo.details?.name ?: "",
infoUpdates = pollInfo.details?.infoUpdates ?: 0,
publicKey = publicKey,
capabilities = listOf()
)
// - Open Group changes
storage.updateOpenGroup(openGroup)