feat: Add Session Id blinding

Including modified version of lazysodium-android to expose missing libsodium functions, we could build from a fork which we still need to setup.
This commit is contained in:
ceokot 2022-03-25 05:34:31 +02:00
parent 2445418e3e
commit 85456b5ea2
28 changed files with 1097 additions and 54 deletions

View File

@ -104,7 +104,8 @@ dependencies {
implementation project(":libsession") implementation project(":libsession")
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxJsonVersion"
implementation "org.whispersystems:curve25519-java:$curve25519Version" implementation "org.whispersystems:curve25519-java:$curve25519Version"
implementation 'com.goterl:lazysodium-android:5.0.2@aar' implementation project(":liblazysodium")
// implementation 'com.goterl:lazysodium-android:5.0.2@aar'
implementation "net.java.dev.jna:jna:5.8.0@aar" implementation "net.java.dev.jna:jna:5.8.0@aar"
implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "com.google.protobuf:protobuf-java:$protobufVersion"
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion" implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion"

View File

@ -0,0 +1,28 @@
package network.loki.messenger
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.goterl.lazysodium.utils.Key
import com.goterl.lazysodium.utils.KeyPair
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.session.libsession.messaging.utilities.SodiumUtilities
@RunWith(AndroidJUnit4::class)
class SodiumUtilitiesTest {
private val serverPublicKey = "c3b3c6f32f0ab5a57f853cc4f30f5da7fda5624b0c77b3fb0829de562ada081d"
private val pubKey = Key.fromHexString("bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc")
private val secKey = Key.fromHexString("c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9")
@Test
fun blindedKeyPair() {
val blindedKey = "98932d4bccbe595a8789d7eb1629cefc483a0eaddc7e20e8fe5c771efafd9af5"
val keyPair = SodiumUtilities.blindedKeyPair(serverPublicKey, KeyPair(pubKey, secKey))!!
assertThat(keyPair.publicKey.asHexString.lowercase(), equalTo(blindedKey))
}
}

View File

@ -57,7 +57,7 @@ import org.session.libsession.messaging.messages.control.DataExtractionNotificat
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
@ -500,7 +500,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun getLatestOpenGroupInfoIfNeeded() { private fun getLatestOpenGroupInfoIfNeeded() {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) ?: return val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) ?: return
OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() } OpenGroupApiV4.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() }
} }
// called from onCreate // called from onCreate

View File

@ -7,7 +7,7 @@ import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import network.loki.messenger.databinding.ViewMentionCandidateBinding import network.loki.messenger.databinding.ViewMentionCandidateBinding
import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.Mention
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
class MentionCandidateView : LinearLayout { class MentionCandidateView : LinearLayout {
@ -34,7 +34,7 @@ class MentionCandidateView : LinearLayout {
profilePictureView.glide = glide!! profilePictureView.glide = glide!!
profilePictureView.update() profilePictureView.update()
if (openGroupServer != null && openGroupRoom != null) { if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupAPIV2.isUserModerator(mentionCandidate.publicKey, openGroupRoom!!, openGroupServer!!) val isUserModerator = OpenGroupApiV4.isUserModerator(mentionCandidate.publicKey, openGroupRoom!!, openGroupServer!!)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
} else { } else {
moderatorIconImageView.visibility = View.GONE moderatorIconImageView.visibility = View.GONE

View File

@ -7,7 +7,7 @@ import android.view.View
import android.widget.RelativeLayout import android.widget.RelativeLayout
import network.loki.messenger.databinding.ViewMentionCandidateV2Binding import network.loki.messenger.databinding.ViewMentionCandidateV2Binding
import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.Mention
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
class MentionCandidateView : RelativeLayout { class MentionCandidateView : RelativeLayout {
@ -34,7 +34,7 @@ class MentionCandidateView : RelativeLayout {
profilePictureView.glide = glide!! profilePictureView.glide = glide!!
profilePictureView.update() profilePictureView.update()
if (openGroupServer != null && openGroupRoom != null) { if (openGroupServer != null && openGroupRoom != null) {
val isUserModerator = OpenGroupAPIV2.isUserModerator(candidate.publicKey, openGroupRoom!!, openGroupServer!!) val isUserModerator = OpenGroupApiV4.isUserModerator(candidate.publicKey, openGroupRoom!!, openGroupServer!!)
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
} else { } else {
moderatorIconImageView.visibility = View.GONE moderatorIconImageView.visibility = View.GONE

View File

@ -5,7 +5,7 @@ import android.view.ActionMode
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
@ -41,13 +41,13 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (!ConversationActivityV2.IS_UNSEND_REQUESTS_ENABLED) { if (!ConversationActivityV2.IS_UNSEND_REQUESTS_ENABLED) {
if (openGroup == null) { return true } if (openGroup == null) { return true }
if (allSentByCurrentUser) { return true } if (allSentByCurrentUser) { return true }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server) return OpenGroupApiV4.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
} }
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing } val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
if (openGroup == null) { return allSentByCurrentUser || allReceivedByCurrentUser } if (openGroup == null) { return allSentByCurrentUser || allReceivedByCurrentUser }
if (allSentByCurrentUser) { return true } if (allSentByCurrentUser) { return true }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server) return OpenGroupApiV4.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
} }
fun userCanBanSelectedUsers(): Boolean { fun userCanBanSelectedUsers(): Boolean {
if (openGroup == null) { return false } if (openGroup == null) { return false }
@ -55,7 +55,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (anySentByCurrentUser) { return false } // Users can't ban themselves if (anySentByCurrentUser) { return false } // Users can't ban themselves
val selectedUsers = selectedItems.map { it.recipient.address.toString() }.toSet() val selectedUsers = selectedItems.map { it.recipient.address.toString() }.toSet()
if (selectedUsers.size > 1) { return false } if (selectedUsers.size > 1) { return false }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server) return OpenGroupApiV4.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
} }
// Delete message // Delete message
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems() menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()

View File

@ -24,7 +24,7 @@ import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageBinding import network.loki.messenger.databinding.ViewVisibleMessageBinding
import org.session.libsession.messaging.contacts.Contact.ContactContext import org.session.libsession.messaging.contacts.Contact.ContactContext
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.utilities.ViewUtil import org.session.libsession.utilities.ViewUtil
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
@ -127,7 +127,7 @@ class VisibleMessageView : LinearLayout {
} }
if (thread.isOpenGroupRecipient) { if (thread.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
val isModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, openGroup.room, openGroup.server) val isModerator = OpenGroupApiV4.isUserModerator(senderSessionID, openGroup.room, openGroup.server)
binding.moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE binding.moderatorIconImageView.visibility = if (isModerator) View.VISIBLE else View.INVISIBLE
} else { } else {
binding.moderatorIconImageView.visibility = View.INVISIBLE binding.moderatorIconImageView.visibility = View.INVISIBLE

View File

@ -4,19 +4,19 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.thoughtcrime.securesms.util.State import org.thoughtcrime.securesms.util.State
typealias DefaultGroups = List<OpenGroupAPIV2.DefaultGroup> typealias DefaultGroups = List<OpenGroupApiV4.DefaultGroup>
typealias GroupState = State<DefaultGroups> typealias GroupState = State<DefaultGroups>
class DefaultGroupsViewModel : ViewModel() { class DefaultGroupsViewModel : ViewModel() {
init { init {
OpenGroupAPIV2.getDefaultRoomsIfNeeded() OpenGroupApiV4.getDefaultRoomsIfNeeded()
} }
val defaultRooms = OpenGroupAPIV2.defaultRooms.map<DefaultGroups, GroupState> { val defaultRooms = OpenGroupApiV4.defaultRooms.map<DefaultGroups, GroupState> {
State.Success(it) State.Success(it)
}.onStart { }.onStart {
emit(State.Loading) emit(State.Loading)

View File

@ -25,7 +25,7 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ActivityJoinPublicChatBinding import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2.DefaultGroup import org.session.libsession.messaging.open_groups.OpenGroupApiV4.DefaultGroup
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient

View File

@ -4,18 +4,17 @@ import android.content.Context
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.messaging.open_groups.OpenGroupV2 import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2 import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV4
import org.session.libsession.utilities.Util import org.session.libsession.utilities.Util
import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.BitmapUtil
import java.util.concurrent.Executors import java.util.concurrent.Executors
object OpenGroupManager { object OpenGroupManager {
private val executorService = Executors.newScheduledThreadPool(4) private val executorService = Executors.newScheduledThreadPool(4)
private var pollers = mutableMapOf<String, OpenGroupPollerV2>() // One for each server private var pollers = mutableMapOf<String, OpenGroupPollerV4>() // One for each server
private var isPolling = false private var isPolling = false
val isAllCaughtUp: Boolean val isAllCaughtUp: Boolean
@ -42,7 +41,7 @@ object OpenGroupManager {
val servers = storage.getAllV2OpenGroups().values.map { it.server }.toSet() val servers = storage.getAllV2OpenGroups().values.map { it.server }.toSet()
servers.forEach { server -> servers.forEach { server ->
pollers[server]?.stop() // Shouldn't be necessary pollers[server]?.stop() // Shouldn't be necessary
val poller = OpenGroupPollerV2(server, executorService) val poller = OpenGroupPollerV4(server, executorService)
poller.startIfNeeded() poller.startIfNeeded()
pollers[server] = poller pollers[server] = poller
} }
@ -68,18 +67,20 @@ object OpenGroupManager {
// Store the public key // Store the public key
storage.setOpenGroupPublicKey(server,publicKey) storage.setOpenGroupPublicKey(server,publicKey)
// Get an auth token // Get an auth token
OpenGroupAPIV2.getAuthToken(room, server).get() OpenGroupApiV4.getAuthToken(room, server).get()
// Get capabilities
val capabilities = OpenGroupApiV4.getCapabilities(room, server).get()
// Get group info // Get group info
val info = OpenGroupAPIV2.getInfo(room, server).get() val info = OpenGroupApiV4.getInfo(room, server).get()
// Create the group locally if not available already // Create the group locally if not available already
if (threadID < 0) { if (threadID < 0) {
threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId threadID = GroupManager.createOpenGroup(openGroupID, context, null, info.name).threadId
} }
val openGroup = OpenGroupV2(server, room, info.name, publicKey) val openGroup = OpenGroupV2(server, room, info.name, publicKey, capabilities)
threadDB.setOpenGroupChat(openGroup, threadID) threadDB.setOpenGroupChat(openGroup, threadID)
// Start the poller if needed // Start the poller if needed
pollers[server]?.startIfNeeded() ?: run { pollers[server]?.startIfNeeded() ?: run {
val poller = OpenGroupPollerV2(server, executorService) val poller = OpenGroupPollerV4(server, executorService)
Util.runOnMain { poller.startIfNeeded() } Util.runOnMain { poller.startIfNeeded() }
pollers[server] = poller pollers[server] = poller
} }

View File

@ -4,6 +4,7 @@ import android.content.Context
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.dependencies.DatabaseComponent
@ -29,8 +30,8 @@ object OpenGroupUtilities {
throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId") throw IllegalStateException("Attempt to update open group info for non-existent DB record: $groupId")
} }
val info = OpenGroupAPIV2.getInfo(room, server).get() // store info again? val info = OpenGroupApiV4.getInfo(room, server).get() // store info again?
OpenGroupAPIV2.getMemberCount(room, server).get() OpenGroupApiV4.getMemberCount(room, server).get()
EventBus.getDefault().post(GroupInfoUpdatedEvent(server, room = room)) EventBus.getDefault().post(GroupInfoUpdatedEvent(server, room = room))
} }

View File

@ -3,14 +3,20 @@ package org.thoughtcrime.securesms.notifications
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.work.* import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all import nl.komponents.kovenant.all
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2 import org.session.libsession.messaging.sending_receiving.pollers.ClosedGroupPollerV2
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV2 import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV4
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -68,9 +74,9 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor
val v2OpenGroupServers = v2OpenGroups.map { it.value.server }.toSet() val v2OpenGroupServers = v2OpenGroups.map { it.value.server }.toSet()
for (server in v2OpenGroupServers) { for (server in v2OpenGroupServers) {
val poller = OpenGroupPollerV2(server, null) val poller = OpenGroupPollerV4(server, null)
poller.hasStarted = true poller.hasStarted = true
promises.add(poller.poll(true)) promises.add(poller.poll())
} }
// Wait until all the promises are resolved // Wait until all the promises are resolved

View File

@ -7,7 +7,7 @@ import org.session.libsession.messaging.messages.control.UnsendRequest
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.OpenGroupInvitation import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address
@ -154,7 +154,7 @@ class DefaultConversationRepository @Inject constructor(
val openGroup = lokiThreadDb.getOpenGroupChat(threadId) val openGroup = lokiThreadDb.getOpenGroupChat(threadId)
if (openGroup != null) { if (openGroup != null) {
lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID -> lokiMessageDb.getServerID(message.id, !message.isMms)?.let { messageServerID ->
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server) OpenGroupApiV4.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success { .success {
messageDataProvider.deleteMessage(message.id, !message.isMms) messageDataProvider.deleteMessage(message.id, !message.isMms)
continuation.resume(ResultOf.Success(Unit)) continuation.resume(ResultOf.Success(Unit))
@ -206,7 +206,7 @@ class DefaultConversationRepository @Inject constructor(
messageServerIDs[messageServerID] = message messageServerIDs[messageServerID] = message
} }
for ((messageServerID, message) in messageServerIDs) { for ((messageServerID, message) in messageServerIDs) {
OpenGroupAPIV2.deleteMessage(messageServerID, openGroup.room, openGroup.server) OpenGroupApiV4.deleteMessage(messageServerID, openGroup.room, openGroup.server)
.success { .success {
messageDataProvider.deleteMessage(message.id, !message.isMms) messageDataProvider.deleteMessage(message.id, !message.isMms)
}.fail { error -> }.fail { error ->
@ -229,7 +229,7 @@ class DefaultConversationRepository @Inject constructor(
suspendCoroutine { continuation -> suspendCoroutine { continuation ->
val sessionID = recipient.address.toString() val sessionID = recipient.address.toString()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!! val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
OpenGroupAPIV2.ban(sessionID, openGroup.room, openGroup.server) OpenGroupApiV4.ban(sessionID, openGroup.room, openGroup.server)
.success { .success {
continuation.resume(ResultOf.Success(Unit)) continuation.resume(ResultOf.Success(Unit))
}.fail { error -> }.fail { error ->
@ -241,7 +241,7 @@ class DefaultConversationRepository @Inject constructor(
suspendCoroutine { continuation -> suspendCoroutine { continuation ->
val sessionID = recipient.address.toString() val sessionID = recipient.address.toString()
val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!! val openGroup = lokiThreadDb.getOpenGroupChat(threadId)!!
OpenGroupAPIV2.banAndDeleteAll(sessionID, openGroup.room, openGroup.server) OpenGroupApiV4.banAndDeleteAll(sessionID, openGroup.room, openGroup.server)
.success { .success {
continuation.resume(ResultOf.Success(Unit)) continuation.resume(ResultOf.Success(Unit))
}.fail { error -> }.fail { error ->

View File

@ -0,0 +1,2 @@
configurations.maybeCreate("default")
artifacts.add("default", file('lazysodium.aar'))

Binary file not shown.

View File

@ -18,7 +18,8 @@ android {
dependencies { dependencies {
implementation project(":libsignal") implementation project(":libsignal")
implementation 'com.goterl:lazysodium-android:5.0.2@aar' implementation project(":liblazysodium")
// implementation 'com.goterl:lazysodium-android:5.0.2@aar'
implementation "net.java.dev.jna:jna:5.8.0@aar" implementation "net.java.dev.jna:jna:5.8.0@aar"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.3.2'

View File

@ -6,10 +6,10 @@ import okhttp3.Headers
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
@ -53,7 +53,7 @@ object FileServerAPIV2 {
} }
private fun send(request: Request): Promise<Map<*, *>, Exception> { private fun send(request: Request): Promise<Map<*, *>, Exception> {
val url = HttpUrl.parse(server) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL) val url = HttpUrl.parse(server) ?: return Promise.ofFail(OpenGroupApiV4.Error.InvalidURL)
val urlBuilder = HttpUrl.Builder() val urlBuilder = HttpUrl.Builder()
.scheme(url.scheme()) .scheme(url.scheme())
.host(url.host()) .host(url.host())
@ -87,7 +87,7 @@ object FileServerAPIV2 {
val parameters = mapOf( "file" to base64EncodedFile ) val parameters = mapOf( "file" to base64EncodedFile )
val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters) val request = Request(verb = HTTP.Verb.POST, endpoint = "files", parameters = parameters)
return send(request).map { json -> return send(request).map { json ->
json["result"] as? Long ?: throw OpenGroupAPIV2.Error.ParsingFailed json["result"] as? Long ?: throw OpenGroupApiV4.Error.ParsingFailed
} }
} }

View File

@ -3,6 +3,7 @@ package org.session.libsession.messaging.jobs
import okhttp3.HttpUrl import okhttp3.HttpUrl
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
@ -102,7 +103,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
} else { } else {
val url = HttpUrl.parse(attachment.url)!! val url = HttpUrl.parse(attachment.url)!!
val fileID = url.pathSegments().last() val fileID = url.pathSegments().last()
OpenGroupAPIV2.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let { OpenGroupApiV4.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let {
tempFile.writeBytes(it) tempFile.writeBytes(it)
} }
} }

View File

@ -9,6 +9,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.file_server.FileServerAPIV2 import org.session.libsession.messaging.file_server.FileServerAPIV2
import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.Data
import org.session.libsession.utilities.DecodedAudio import org.session.libsession.utilities.DecodedAudio
@ -53,7 +54,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
val v2OpenGroup = storage.getV2OpenGroup(threadID.toLong()) val v2OpenGroup = storage.getV2OpenGroup(threadID.toLong())
if (v2OpenGroup != null) { if (v2OpenGroup != null) {
val keyAndResult = upload(attachment, v2OpenGroup.server, false) { val keyAndResult = upload(attachment, v2OpenGroup.server, false) {
OpenGroupAPIV2.upload(it, v2OpenGroup.room, v2OpenGroup.server) OpenGroupApiV4.upload(it, v2OpenGroup.room, v2OpenGroup.server)
} }
handleSuccess(attachment, keyAndResult.first, keyAndResult.second) handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
} else { } else {

View File

@ -1,7 +1,7 @@
package org.session.libsession.messaging.jobs package org.session.libsession.messaging.jobs
import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.Data
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
@ -15,8 +15,8 @@ class GroupAvatarDownloadJob(val room: String, val server: String) : Job {
override fun execute() { override fun execute() {
val storage = MessagingModuleConfiguration.shared.storage val storage = MessagingModuleConfiguration.shared.storage
try { try {
val info = OpenGroupAPIV2.getInfo(room, server).get() val info = OpenGroupApiV4.getInfo(room, server).get()
val bytes = OpenGroupAPIV2.downloadOpenGroupProfilePicture(info.id, server).get() val bytes = OpenGroupApiV4.downloadOpenGroupProfilePicture(info.id, server).get()
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray()) val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
storage.updateProfilePicture(groupId, bytes) storage.updateProfilePicture(groupId, bytes)
storage.updateTimestampUpdated(groupId, System.currentTimeMillis()) storage.updateTimestampUpdated(groupId, System.currentTimeMillis())

View File

@ -0,0 +1,665 @@
package org.session.libsession.messaging.open_groups
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.annotation.JsonNaming
import com.fasterxml.jackson.databind.type.TypeFactory
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import kotlinx.coroutines.flow.MutableSharedFlow
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPollerV4.Companion.maxInactivityPeriod
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.AESGCM
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Base64.decode
import org.session.libsignal.utilities.Base64.encodeBytes
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.HTTP.Verb.DELETE
import org.session.libsignal.utilities.HTTP.Verb.GET
import org.session.libsignal.utilities.HTTP.Verb.POST
import org.session.libsignal.utilities.HTTP.Verb.PUT
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.toHexString
import org.whispersystems.curve25519.Curve25519
import java.util.concurrent.TimeUnit
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
object OpenGroupApiV4 {
private val moderators: HashMap<String, Set<String>> =
hashMapOf() // Server URL to (channel ID to set of moderator IDs)
private val curve = Curve25519.getInstance(Curve25519.BEST)
val defaultRooms = MutableSharedFlow<List<DefaultGroup>>(replay = 1)
private val hasPerformedInitialPoll = mutableMapOf<String, Boolean>()
private var hasUpdatedLastOpenDate = false
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
private val timeSinceLastOpen by lazy {
val context = MessagingModuleConfiguration.shared.context
val lastOpenDate = TextSecurePreferences.getLastOpenTimeDate(context)
val now = System.currentTimeMillis()
now - lastOpenDate
}
private const val defaultServerPublicKey =
"a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238"
const val defaultServer = "http://116.203.70.33"
sealed class Error(message: String) : Exception(message) {
object Generic : Error("An error occurred.")
object ParsingFailed : Error("Invalid response.")
object DecryptionFailed : Error("Couldn't decrypt response.")
object SigningFailed : Error("Couldn't sign message.")
object InvalidURL : Error("Invalid URL.")
object NoPublicKey : Error("Couldn't find server public key.")
object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.")
}
data class DefaultGroup(val id: String, val name: String, val image: ByteArray?) {
val joinURL: String get() = "$defaultServer/$id?public_key=$defaultServerPublicKey"
}
data class Info(val id: String, val name: String, val imageID: String?)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class BatchRequest(
val roomID: String,
val authToken: String,
val fromDeletionServerID: Long?,
val fromMessageServerID: Long?
)
data class BatchResult(
val messages: List<OpenGroupMessageV2>,
val deletions: List<MessageDeletion>,
val moderators: List<String>
)
data class MessageDeletion(
@JsonProperty("id")
val id: Long = 0,
@JsonProperty("deleted_message_id")
val deletedMessageServerID: Long = 0
) {
companion object {
val empty = MessageDeletion()
}
}
data class Request(
val verb: HTTP.Verb,
val room: String?,
val server: String,
val endpoint: String,
val queryParameters: Map<String, String> = mapOf(),
val parameters: Any? = null,
val headers: Map<String, String> = mapOf(),
val isAuthRequired: Boolean = true,
/**
* Always `true` under normal circumstances. You might want to disable
* this when running over Lokinet.
*/
val useOnionRouting: Boolean = true,
val isBlinded: Boolean = true
)
private fun createBody(parameters: Any?): RequestBody? {
if (parameters == null) return null
val parametersAsJSON = JsonUtil.toJson(parameters)
return RequestBody.create(MediaType.get("application/json"), parametersAsJSON)
}
private fun send(request: Request): Promise<Map<*, *>, Exception> {
val url = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL)
val urlBuilder = HttpUrl.Builder()
.scheme(url.scheme())
.host(url.host())
.port(url.port())
.addPathSegments(request.endpoint)
if (request.verb == GET) {
for ((key, value) in request.queryParameters) {
urlBuilder.addQueryParameter(key, value)
}
}
fun execute(token: String?): Promise<Map<*, *>, Exception> {
val publicKey =
MessagingModuleConfiguration.shared.storage.getOpenGroupPublicKey(request.server)
?: return Promise.ofFail(Error.NoPublicKey)
val ed25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()
?: return Promise.ofFail(Error.NoEd25519KeyPair)
val urlRequest = urlBuilder.build()
val headers = request.headers.toMutableMap()
val nonce = sodium.nonce(16)
val timestamp = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
var pubKey = ""
var signature = ByteArray(0)
if (request.isBlinded) {
SodiumUtilities.blindedKeyPair(publicKey, ed25519KeyPair)?.let { keyPair ->
pubKey = SodiumUtilities.SessionId(
SodiumUtilities.IdPrefix.BLINDED,
keyPair.publicKey.asBytes
).hexString
signature = SodiumUtilities.sogsSignature(
urlRequest.toString().toByteArray(),
ed25519KeyPair.secretKey.asBytes,
keyPair.secretKey.asBytes,
keyPair.publicKey.asBytes
) ?: return Promise.ofFail(Error.SigningFailed)
} ?: return Promise.ofFail(Error.SigningFailed)
} else {
pubKey = SodiumUtilities.SessionId(
SodiumUtilities.IdPrefix.UN_BLINDED,
ed25519KeyPair.publicKey.asBytes
).hexString
signature = ByteArray(0)
}
headers["X-SOGS-Nonce"] = encodeBytes(nonce)
headers["X-SOGS-Timestamp"] = "$timestamp"
headers["X-SOGS-Pubkey"] = pubKey
headers["X-SOGS-Signature"] = encodeBytes(signature)
headers.forEach { entry -> Log.d("Loki", "${entry.key}: ${entry.value}") }
val requestBuilder = okhttp3.Request.Builder()
.url(urlRequest)
.headers(Headers.of(headers))
if (request.isAuthRequired) {
if (token.isNullOrEmpty()) throw IllegalStateException("No auth token for request.")
requestBuilder.header("Authorization", token)
}
when (request.verb) {
GET -> requestBuilder.get()
PUT -> requestBuilder.put(createBody(request.parameters)!!)
POST -> requestBuilder.post(createBody(request.parameters)!!)
DELETE -> requestBuilder.delete(createBody(request.parameters))
}
if (!request.room.isNullOrEmpty()) {
requestBuilder.header("Room", request.room)
}
if (request.useOnionRouting) {
return OnionRequestAPI.sendOnionRequest(
requestBuilder.build(),
request.server,
publicKey
).fail { e ->
// A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
if (e is OnionRequestAPI.HTTPRequestFailedAtDestinationException && e.statusCode == 401) {
val storage = MessagingModuleConfiguration.shared.storage
if (request.room != null) {
storage.removeAuthToken(request.room, request.server)
}
}
}
} else {
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
}
}
return if (request.isAuthRequired) {
getAuthToken(request.room!!, request.server).bind { execute(it) }
} else {
execute(null)
}
}
fun downloadOpenGroupProfilePicture(
roomID: String,
server: String
): Promise<ByteArray, Exception> {
val request = Request(
verb = GET,
room = roomID,
server = server,
endpoint = "rooms/$roomID/image",
isAuthRequired = false
)
return send(request).map { json ->
val result = json["result"] as? String ?: throw Error.ParsingFailed
decode(result)
}
}
// region Authorization
fun getAuthToken(room: String, server: String): Promise<String, Exception> {
val storage = MessagingModuleConfiguration.shared.storage
return storage.getAuthToken(room, server)?.let {
Promise.of(it)
} ?: run {
requestNewAuthToken(room, server)
.bind { claimAuthToken(it, room, server) }
.success { authToken ->
storage.setAuthToken(room, server, authToken)
}
}
}
fun requestNewAuthToken(room: String, server: String): Promise<String, Exception> {
val (publicKey, privateKey) = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair()
.let { it.publicKey.serialize() to it.privateKey.serialize() }
?: return Promise.ofFail(Error.Generic)
val queryParameters = mutableMapOf("public_key" to publicKey.toHexString())
val request = Request(
GET,
room,
server,
"auth_token_challenge",
queryParameters,
isAuthRequired = false,
parameters = null
)
return send(request).map { json ->
val challenge = json["challenge"] as? Map<*, *> ?: throw Error.ParsingFailed
val base64EncodedCiphertext =
challenge["ciphertext"] as? String ?: throw Error.ParsingFailed
val base64EncodedEphemeralPublicKey =
challenge["ephemeral_public_key"] as? String ?: throw Error.ParsingFailed
val ciphertext = decode(base64EncodedCiphertext)
val ephemeralPublicKey = decode(base64EncodedEphemeralPublicKey)
val symmetricKey = AESGCM.generateSymmetricKey(ephemeralPublicKey, privateKey)
val tokenAsData = try {
AESGCM.decrypt(ciphertext, symmetricKey)
} catch (e: Exception) {
throw Error.DecryptionFailed
}
tokenAsData.toHexString()
}
}
fun claimAuthToken(
authToken: String,
room: String,
server: String
): Promise<String, Exception> {
val parameters =
mapOf("public_key" to MessagingModuleConfiguration.shared.storage.getUserPublicKey()!!)
val headers = mapOf("Authorization" to authToken)
val request = Request(
verb = POST, room = room, server = server, endpoint = "claim_auth_token",
parameters = parameters, headers = headers, isAuthRequired = false
)
return send(request).map { authToken }
}
// endregion
// region Upload/Download
fun upload(file: ByteArray, room: String, server: String): Promise<Long, Exception> {
val base64EncodedFile = encodeBytes(file)
val parameters = mapOf("file" to base64EncodedFile)
val request = Request(
verb = POST,
room = room,
server = server,
endpoint = "files",
parameters = parameters
)
return send(request).map { json ->
(json["result"] as? Number)?.toLong() ?: throw Error.ParsingFailed
}
}
fun download(file: Long, room: String, server: String): Promise<ByteArray, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "files/$file")
return send(request).map { json ->
val base64EncodedFile = json["result"] as? String ?: throw Error.ParsingFailed
decode(base64EncodedFile) ?: throw Error.ParsingFailed
}
}
// endregion
// region Sending
fun send(
message: OpenGroupMessageV2,
room: String,
server: String
): Promise<OpenGroupMessageV2, Exception> {
val signedMessage = message.sign() ?: return Promise.ofFail(Error.SigningFailed)
val jsonMessage = signedMessage.toJSON()
val request = Request(
verb = POST,
room = room,
server = server,
endpoint = "messages",
parameters = jsonMessage
)
return send(request).map { json ->
@Suppress("UNCHECKED_CAST") val rawMessage = json["message"] as? Map<String, Any>
?: throw Error.ParsingFailed
val result = OpenGroupMessageV2.fromJSON(rawMessage) ?: throw Error.ParsingFailed
val storage = MessagingModuleConfiguration.shared.storage
storage.addReceivedMessageTimestamp(result.sentTimestamp)
result
}
}
// endregion
// region Messages
fun getMessages(room: String, server: String): Promise<List<OpenGroupMessageV2>, Exception> {
val storage = MessagingModuleConfiguration.shared.storage
val queryParameters = mutableMapOf<String, String>()
storage.getLastMessageServerID(room, server)?.let { lastId ->
queryParameters += "from_server_id" to lastId.toString()
}
val request = Request(
verb = GET,
room = room,
server = server,
endpoint = "messages",
queryParameters = queryParameters
)
return send(request).map { json ->
@Suppress("UNCHECKED_CAST") val rawMessages =
json["messages"] as? List<Map<String, Any>>
?: throw Error.ParsingFailed
parseMessages(room, server, rawMessages)
}
}
private fun parseMessages(
room: String,
server: String,
rawMessages: List<Map<*, *>>
): List<OpenGroupMessageV2> {
val messages = rawMessages.mapNotNull { json ->
json as Map<String, Any>
try {
val message = OpenGroupMessageV2.fromJSON(json) ?: return@mapNotNull null
if (message.serverID == null || message.sender.isNullOrEmpty()) return@mapNotNull null
val sender = message.sender
val data = decode(message.base64EncodedData)
val signature = decode(message.base64EncodedSignature)
val publicKey = Hex.fromStringCondensed(sender.removing05PrefixIfNeeded())
val isValid = curve.verifySignature(publicKey, data, signature)
if (!isValid) {
Log.d("Loki", "Ignoring message with invalid signature.")
return@mapNotNull null
}
message
} catch (e: Exception) {
null
}
}
return messages
}
// endregion
// region Message Deletion
@JvmStatic
fun deleteMessage(serverID: Long, room: String, server: String): Promise<Unit, Exception> {
val request =
Request(verb = DELETE, room = room, server = server, endpoint = "messages/$serverID")
return send(request).map {
Log.d("Loki", "Message deletion successful.")
}
}
fun getDeletedMessages(
room: String,
server: String
): Promise<List<MessageDeletion>, Exception> {
val storage = MessagingModuleConfiguration.shared.storage
val queryParameters = mutableMapOf<String, String>()
storage.getLastDeletionServerID(room, server)?.let { last ->
queryParameters["from_server_id"] = last.toString()
}
val request = Request(
verb = GET,
room = room,
server = server,
endpoint = "deleted_messages",
queryParameters = queryParameters
)
return send(request).map { json ->
val type = TypeFactory.defaultInstance()
.constructCollectionType(List::class.java, MessageDeletion::class.java)
val idsAsString = JsonUtil.toJson(json["ids"])
val serverIDs = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type)
?: throw Error.ParsingFailed
val lastMessageServerId = storage.getLastDeletionServerID(room, server) ?: 0
val serverID = serverIDs.maxByOrNull { it.id } ?: MessageDeletion.empty
if (serverID.id > lastMessageServerId) {
storage.setLastDeletionServerID(room, server, serverID.id)
}
serverIDs
}
}
// endregion
// region Moderation
private fun handleModerators(serverRoomId: String, moderatorList: List<String>) {
moderators[serverRoomId] = moderatorList.toMutableSet()
}
fun getModerators(room: String, server: String): Promise<List<String>, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "moderators")
return send(request).map { json ->
@Suppress("UNCHECKED_CAST") val moderatorsJson = json["moderators"] as? List<String>
?: throw Error.ParsingFailed
val id = "$server.$room"
handleModerators(id, moderatorsJson)
moderatorsJson
}
}
@JvmStatic
fun ban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
val parameters = mapOf("public_key" to publicKey)
val request = Request(
verb = POST,
room = room,
server = server,
endpoint = "block_list",
parameters = parameters
)
return send(request).map {
Log.d("Loki", "Banned user: $publicKey from: $server.$room.")
}
}
fun banAndDeleteAll(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
val parameters = mapOf("public_key" to publicKey)
val request = Request(
verb = POST,
room = room,
server = server,
endpoint = "ban_and_delete_all",
parameters = parameters
)
return send(request).map {
Log.d("Loki", "Banned user: $publicKey from: $server.$room.")
}
}
fun unban(publicKey: String, room: String, server: String): Promise<Unit, Exception> {
val request =
Request(verb = DELETE, room = room, server = server, endpoint = "block_list/$publicKey")
return send(request).map {
Log.d("Loki", "Unbanned user: $publicKey from: $server.$room")
}
}
@JvmStatic
fun isUserModerator(publicKey: String, room: String, server: String): Boolean =
moderators["$server.$room"]?.contains(publicKey) ?: false
// endregion
// region General
@Suppress("UNCHECKED_CAST")
fun batch(
rooms: List<String>,
server: String
): Promise<Map<String, BatchResult>, Exception> {
val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) }
val storage = MessagingModuleConfiguration.shared.storage
val context = MessagingModuleConfiguration.shared.context
val timeSinceLastOpen = this.timeSinceLastOpen
val useMessageLimit = (hasPerformedInitialPoll[server] != true
&& timeSinceLastOpen > maxInactivityPeriod)
hasPerformedInitialPoll[server] = true
if (!hasUpdatedLastOpenDate) {
hasUpdatedLastOpenDate = true
TextSecurePreferences.setLastOpenDate(context)
}
val requests = rooms.mapNotNull { room ->
val authToken = try {
authTokenRequests[room]?.get()
} catch (e: Exception) {
Log.e("Loki", "Failed to get auth token for $room.", e)
null
} ?: return@mapNotNull null
BatchRequest(
roomID = room,
authToken = authToken,
fromDeletionServerID = if (useMessageLimit) null else storage.getLastDeletionServerID(
room,
server
),
fromMessageServerID = if (useMessageLimit) null else storage.getLastMessageServerID(
room,
server
)
)
}
val request = Request(
verb = POST,
room = null,
server = server,
endpoint = "batch",
isAuthRequired = false,
parameters = mapOf("requests" to requests)
)
return send(request = request).map { json ->
val results = json["results"] as? List<*> ?: throw Error.ParsingFailed
results.mapNotNull { json ->
if (json !is Map<*, *>) return@mapNotNull null
val roomID = json["room_id"] as? String ?: return@mapNotNull null
// A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an
// indication that the token we're using has expired. Note that a 403 has a different meaning; it means that
// we provided a valid token but it doesn't have a high enough permission level for the route in question.
val statusCode = json["status_code"] as? Int ?: return@mapNotNull null
if (statusCode == 401) {
// delete auth token and return null
storage.removeAuthToken(roomID, server)
}
// Moderators
val moderators = json["moderators"] as? List<String> ?: return@mapNotNull null
handleModerators("$server.$roomID", moderators)
// Deletions
val type = TypeFactory.defaultInstance()
.constructCollectionType(List::class.java, MessageDeletion::class.java)
val idsAsString = JsonUtil.toJson(json["deletions"])
val deletions = JsonUtil.fromJson<List<MessageDeletion>>(idsAsString, type)
?: throw Error.ParsingFailed
// Messages
val rawMessages =
json["messages"] as? List<Map<String, Any>> ?: return@mapNotNull null
val messages = parseMessages(roomID, server, rawMessages)
roomID to BatchResult(
messages = messages,
deletions = deletions,
moderators = moderators
)
}.toMap()
}
}
fun getDefaultRoomsIfNeeded(): Promise<List<DefaultGroup>, Exception> {
val storage = MessagingModuleConfiguration.shared.storage
storage.setOpenGroupPublicKey(defaultServer, defaultServerPublicKey)
return getAllRooms(defaultServer).map { groups ->
val earlyGroups = groups.map { group ->
DefaultGroup(group.id, group.name, null)
}
// See if we have any cached rooms, and if they already have images don't overwrite them with early non-image results
defaultRooms.replayCache.firstOrNull()?.let { replayed ->
if (replayed.none { it.image?.isNotEmpty() == true }) {
defaultRooms.tryEmit(earlyGroups)
}
}
val images = groups.associate { group ->
group.id to downloadOpenGroupProfilePicture(group.id, defaultServer)
}
groups.map { group ->
val image = try {
images[group.id]!!.get()
} catch (e: Exception) {
// No image or image failed to download
null
}
DefaultGroup(group.id, group.name, image)
}
}.success { new ->
defaultRooms.tryEmit(new)
}
}
fun getInfo(room: String, server: String): Promise<Info, Exception> {
val request = Request(
verb = GET,
room = null,
server = server,
endpoint = "rooms/$room",
isAuthRequired = false
)
return send(request).map { json ->
val rawRoom = json["room"] as? Map<*, *> ?: throw Error.ParsingFailed
val id = rawRoom["id"] as? String ?: throw Error.ParsingFailed
val name = rawRoom["name"] as? String ?: throw Error.ParsingFailed
val imageID = rawRoom["image_id"] as? String
Info(id = id, name = name, imageID = imageID)
}
}
fun getAllRooms(server: String): Promise<List<Info>, Exception> {
val request = Request(
verb = GET,
room = null,
server = server,
endpoint = "rooms",
isAuthRequired = false
)
return send(request).map { json ->
val rawRooms = json["rooms"] as? List<Map<*, *>> ?: throw Error.ParsingFailed
rawRooms.mapNotNull {
val roomJson = it as? Map<*, *> ?: return@mapNotNull null
val id = roomJson["id"] as? String ?: return@mapNotNull null
val name = roomJson["name"] as? String ?: return@mapNotNull null
val imageID = roomJson["image_id"] as? String
Info(id, name, imageID)
}
}
}
fun getMemberCount(room: String, server: String): Promise<Int, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "active_users")
return send(request).map { json ->
val activeUserCount = json["active_users"] as? Int ?: throw Error.ParsingFailed
val storage = MessagingModuleConfiguration.shared.storage
storage.setUserCount(room, server, activeUserCount)
activeUserCount
}
}
fun getCapabilities(room: String, server: String): Promise<List<String>, Exception> {
val request = Request(verb = GET, room = room, server = server, endpoint = "capabilities")
return send(request).map { json ->
json["capabilities"] as? List<String> ?: throw Error.ParsingFailed
}
}
// endregion
}

View File

@ -9,15 +9,17 @@ data class OpenGroupV2(
val room: String, val room: String,
val id: String, val id: String,
val name: String, val name: String,
val publicKey: String val publicKey: String,
val capabilities: List<String>
) { ) {
constructor(server: String, room: String, name: String, publicKey: String) : this( constructor(server: String, room: String, name: String, publicKey: String, capabilities: List<String>) : this(
server = server, server = server,
room = room, room = room,
id = "$server.$room", id = "$server.$room",
name = name, name = name,
publicKey = publicKey, publicKey = publicKey,
capabilities = capabilities,
) )
companion object { companion object {
@ -30,7 +32,8 @@ data class OpenGroupV2(
val server = json.get("server").asText().toLowerCase(Locale.US) val server = json.get("server").asText().toLowerCase(Locale.US)
val displayName = json.get("displayName").asText() val displayName = json.get("displayName").asText()
val publicKey = json.get("publicKey").asText() val publicKey = json.get("publicKey").asText()
OpenGroupV2(server, room, displayName, publicKey) val capabilities = json.get("capabilities")?.asText()?.split(",") ?: emptyList()
OpenGroupV2(server, room, displayName, publicKey, capabilities)
} catch (e: Exception) { } catch (e: Exception) {
Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e); Log.w("Loki", "Couldn't parse open group from JSON: $jsonAsString.", e);
null null
@ -44,6 +47,7 @@ data class OpenGroupV2(
"server" to server, "server" to server,
"displayName" to name, "displayName" to name,
"publicKey" to publicKey, "publicKey" to publicKey,
"capabilities" to capabilities.joinToString(","),
) )
val joinURL: String get() = "$server/$room?public_key=$publicKey" val joinURL: String get() = "$server/$room?public_key=$publicKey"

View File

@ -235,7 +235,7 @@ object MessageSender {
sentTimestamp = message.sentTimestamp!!, sentTimestamp = message.sentTimestamp!!,
base64EncodedData = Base64.encodeBytes(plaintext), base64EncodedData = Base64.encodeBytes(plaintext),
) )
OpenGroupAPIV2.send(openGroupMessage,room,server).success { OpenGroupApiV4.send(openGroupMessage,room,server).success {
message.openGroupServerMessageID = it.serverID message.openGroupServerMessageID = it.serverID
handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = it.sentTimestamp) handleSuccessfulMessageSend(message, destination, openGroupSentTimestamp = it.sentTimestamp)
deferred.resolve(Unit) deferred.resolve(Unit)

View File

@ -0,0 +1,126 @@
package org.session.libsession.messaging.sending_receiving.pollers
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
import org.session.libsession.messaging.jobs.GroupAvatarDownloadJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.jobs.MessageReceiveParameters
import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.messaging.open_groups.OpenGroupMessageV2
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.successBackground
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import kotlin.math.max
class OpenGroupPollerV4(private val server: String, private val executorService: ScheduledExecutorService?) {
var hasStarted = false
var isCaughtUp = false
var secondToLastJob: MessageReceiveJob? = null
private var future: ScheduledFuture<*>? = null
companion object {
private const val pollInterval: Long = 4000L
const val maxInactivityPeriod = 14 * 24 * 60 * 60 * 1000
}
fun startIfNeeded() {
if (hasStarted) { return }
hasStarted = true
future = executorService?.schedule(::poll, 0, TimeUnit.MILLISECONDS)
}
fun stop() {
future?.cancel(false)
hasStarted = false
}
fun poll(): Promise<Unit, Exception> {
val storage = MessagingModuleConfiguration.shared.storage
val rooms = storage.getAllV2OpenGroups().values.filter { it.server == server }.map { it.room }
rooms.forEach { downloadGroupAvatarIfNeeded(it) }
return OpenGroupApiV4.batch(rooms, server).successBackground { responses ->
responses.forEach { (room, response) ->
val openGroupID = "$server.$room"
handleNewMessages(room, openGroupID, response.messages)
handleDeletedMessages(room, openGroupID, response.deletions)
if (secondToLastJob == null && !isCaughtUp) {
isCaughtUp = true
}
}
}.always {
executorService?.schedule(this@OpenGroupPollerV4::poll, pollInterval, TimeUnit.MILLISECONDS)
}.map { }
}
private fun handleNewMessages(
room: String,
openGroupID: String,
messages: List<OpenGroupMessageV2>
) {
val storage = MessagingModuleConfiguration.shared.storage
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
// check thread still exists
val threadId = storage.getThreadId(Address.fromSerialized(groupID)) ?: -1
val threadExists = threadId >= 0
if (!hasStarted || !threadExists) { return }
val envelopes = messages.sortedBy { it.serverID!! }.map { message ->
val senderPublicKey = message.sender!!
val builder = SignalServiceProtos.Envelope.newBuilder()
builder.type = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE
builder.source = senderPublicKey
builder.sourceDevice = 1
builder.content = message.toProto().toByteString()
builder.timestamp = message.sentTimestamp
builder.build() to message.serverID
}
envelopes.chunked(256).forEach { list ->
val parameters = list.map { (message, serverId) ->
MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId)
}
JobQueue.shared.add(BatchMessageReceiveJob(parameters, openGroupID))
}
val currentLastMessageServerID = storage.getLastMessageServerID(room, server) ?: 0
val actualMax = max(messages.mapNotNull { it.serverID }.maxOrNull() ?: 0, currentLastMessageServerID)
if (actualMax > 0) {
storage.setLastMessageServerID(room, server, actualMax)
}
}
private fun handleDeletedMessages(room: String, openGroupID: String, deletions: List<OpenGroupApiV4.MessageDeletion>) {
val storage = MessagingModuleConfiguration.shared.storage
val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
val threadID = storage.getThreadId(Address.fromSerialized(groupID)) ?: return
val deletedMessageIDs = deletions.mapNotNull { deletion ->
dataProvider.getMessageID(deletion.deletedMessageServerID, threadID)
}
deletedMessageIDs.forEach { (messageId, isSms) ->
MessagingModuleConfiguration.shared.messageDataProvider.deleteMessage(messageId, isSms)
}
val currentMax = storage.getLastDeletionServerID(room, server) ?: 0L
val latestMax = deletions.map { it.id }.maxOrNull() ?: 0L
if (latestMax > currentMax && latestMax != 0L) {
storage.setLastDeletionServerID(room, server, latestMax)
}
}
private fun downloadGroupAvatarIfNeeded(room: String) {
val storage = MessagingModuleConfiguration.shared.storage
if (storage.getGroupAvatarDownloadJob(server, room) != null) return
val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray())
storage.getGroup(groupId)?.let {
if (System.currentTimeMillis() > it.updatedTimestamp + TimeUnit.DAYS.toMillis(7)) {
JobQueue.shared.add(GroupAvatarDownloadJob(room, server))
}
}
}
}

View File

@ -0,0 +1,202 @@
package org.session.libsession.messaging.utilities
import com.goterl.lazysodium.LazySodiumAndroid
import com.goterl.lazysodium.SodiumAndroid
import com.goterl.lazysodium.interfaces.GenericHash
import com.goterl.lazysodium.interfaces.Hash
import com.goterl.lazysodium.interfaces.Sign
import com.goterl.lazysodium.utils.Key
import com.goterl.lazysodium.utils.KeyPair
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.toHexString
import kotlin.experimental.inv
object SodiumUtilities {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
/* 64-byte blake2b hash then reduce to get the blinding factor */
private fun generateBlindingFactor(serverPublicKey: String): ByteArray? {
// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
val serverPubKeyData = Hex.fromStringCondensed(serverPublicKey)
val serverPubKeyHash = ByteArray(GenericHash.BYTES_MAX)
if (!sodium.cryptoGenericHash(serverPubKeyHash, serverPubKeyHash.size, serverPubKeyData, serverPubKeyData.size.toLong())) {
return null
}
// Reduce the server public key into an ed25519 scalar (`k`)
val x25519PublicKey = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)
sodium.cryptoCoreEd25519ScalarReduce(x25519PublicKey, serverPubKeyHash)
return if (x25519PublicKey.any { it.toInt() != 0 }) {
x25519PublicKey
} else null
}
/*
Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to
convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
same secret scalar secret (and so this is just the most convenient way to get 'a' out of
a sodium Ed25519 secret key)
*/
private fun generatePrivateKeyScalar(secretKey: ByteArray): ByteArray? {
// a = s.to_curve25519_private_key().encode()
val aBytes = ByteArray(Sign.PUBLICKEYBYTES)
return if (sodium.convertSecretKeyEd25519ToCurve25519(aBytes, secretKey)) {
aBytes
} else null
}
/* Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` */
fun blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair): KeyPair? {
// if (edKeyPair.publicKey.asBytes.size != Sign.PUBLICKEYBYTES ||
// edKeyPair.secretKey.asBytes.size != Sign.SECRETKEYBYTES) return null
val kBytes = generateBlindingFactor(serverPublicKey)
val aBytes = generatePrivateKeyScalar(edKeyPair.secretKey.asBytes)
// Generate the blinded key pair `ka`, `kA`
val kaBytes = ByteArray(Sign.SECRETKEYBYTES)
sodium.cryptoCoreEd25519ScalarMul(kaBytes, kBytes, aBytes)
if (kaBytes.all { it.toInt() == 0 }) {
return null
}
val kABytes = ByteArray(Sign.PUBLICKEYBYTES)
sodium.cryptoScalarMultE25519BaseNoClamp(kABytes, kaBytes)
return if (kABytes.any { it.toInt() != 0 }) {
KeyPair(Key.fromBytes(kABytes), Key.fromBytes(kaBytes))
} else {
null
}
}
/*
Constructs an Ed25519 signature from a root Ed25519 key and a blinded scalar/pubkey pair, with one tweak to the
construction: we add kA into the hashed value that yields r so that we have domain separation for different blinded
pubkeys (this doesn't affect verification at all)
*/
fun sogsSignature(
message: ByteArray,
secretKey: ByteArray,
ka: ByteArray, /*blindedSecretKey*/
kA: ByteArray /*blindedPublicKey*/
): ByteArray? {
// H_rh = sha512(s.encode()).digest()[32:]
val h_rh = ByteArray(Hash.SHA512_BYTES)
if (!sodium.cryptoHashSha512(h_rh, secretKey, secretKey.size.toLong())) return null
// r = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(H_rh, kA, message_parts))
val combinedData = h_rh + kA + message
val combinedHash = ByteArray(Hash.SHA512_BYTES)
if (!sodium.cryptoHashSha512(combinedHash, combinedData, combinedData.size.toLong())) return null
val rHash = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)
sodium.cryptoCoreEd25519ScalarReduce(rHash, combinedHash)
if (rHash.all { it.toInt() == 0 }) {
return null
}
// sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r)
val sig_R = ByteArray(Sign.SECRETKEYBYTES)
if (!sodium.cryptoScalarMultBase(sig_R, rHash)) return null
// HRAM = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(sig_R, kA, message_parts))
val hRamData = sig_R + kA + message
val hRamHash = ByteArray(Hash.SHA512_BYTES)
if (!sodium.cryptoHashSha512(hRamHash, hRamData, hRamData.size.toLong())) return null
val hRam = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)
sodium.cryptoCoreEd25519ScalarReduce(hRam, hRamHash)
if (hRam.all { it.toInt() == 0 }) {
return null
}
// sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka))
val sig_sMul = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)
val sig_s = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES)
if (sodium.cryptoScalarMult(sig_sMul, hRam, ka)) {
sodium.cryptoCoreEd25519ScalarReduce(sig_s/*, rHash*/, sig_sMul)
} else return null
return sig_R + sig_s
}
/* Combines two keys (`kA`) */
private fun combineKeys(lhsKey: ByteArray, rhsKey: ByteArray): ByteArray? {
return sodium.cryptoScalarMult(Key.fromBytes(lhsKey), Key.fromBytes(rhsKey)).asBytes
}
/*
Calculate a shared secret for a message from A to B:
BLAKE2b(a kB || kA || kB)
The receiver can calculate the same value via:
BLAKE2b(b kA || kA || kB)
*/
fun sharedBlindedEncryptionKey(
secretKey: ByteArray,
otherBlindedPublicKey: ByteArray,
kA: ByteArray, /*fromBlindedPublicKey*/
kB: ByteArray /*toBlindedPublicKey*/
): ByteArray? {
val aBytes = generatePrivateKeyScalar(secretKey) ?: return null
val combinedKeyBytes = combineKeys(aBytes, otherBlindedPublicKey) ?: return null
val outputHash = ByteArray(GenericHash.KEYBYTES)
val inputBytes = combinedKeyBytes + kA + kB
return if (sodium.cryptoGenericHash(outputHash, outputHash.size, inputBytes, inputBytes.size.toLong())) {
outputHash
} else null
}
/* This method should be used to check if a users standard sessionId matches a blinded one */
fun sessionId(
standardSessionId: String,
blindedSessionId: String,
serverPublicKey: String
): Boolean {
// Only support generating blinded keys for standard session ids
val sessionId = SessionId(standardSessionId)
if (sessionId.prefix != IdPrefix.STANDARD) return false
val blindedId = SessionId(blindedSessionId)
if (blindedId.prefix != IdPrefix.BLINDED) return false
val k = generateBlindingFactor(serverPublicKey) ?: return false
// From the session id (ignoring 05 prefix) we have two possible ed25519 pubkeys; the first is the positive (which is what
// Signal's XEd25519 conversion always uses)
val xEd25519Key = Key.fromHexString(sessionId.publicKey).asBytes
// Blind the positive public key
val pk1 = combineKeys(k, xEd25519Key) ?: return false
// For the negative, what we're going to get out of the above is simply the negative of pk1, so flip the sign bit to get pk2
// pk2 = pk1[0:31] + bytes([pk1[31] ^ 0b1000_0000])
val pk2 = pk1.take(31).toByteArray() + listOf(pk1.last().inv()).toByteArray()
return SessionId(IdPrefix.BLINDED, pk1).publicKey == blindedId.publicKey ||
SessionId(IdPrefix.BLINDED, pk2).publicKey == blindedId.publicKey
}
class SessionId {
var prefix: IdPrefix?
var publicKey: String
constructor(id: String) {
prefix = IdPrefix.fromValue(id.take(2))
publicKey = id.drop(2)
}
constructor(prefix: IdPrefix, publicKey: ByteArray) {
this.prefix = prefix
this.publicKey = publicKey.toHexString()
}
val hexString
get() = prefix?.prefix + publicKey
}
enum class IdPrefix(val prefix: String) {
STANDARD("05"), BLINDED("15"), UN_BLINDED("00");
companion object {
fun fromValue(prefix: String): IdPrefix? = when(prefix) {
STANDARD.prefix -> STANDARD
BLINDED.prefix -> BLINDED
UN_BLINDED.prefix -> UN_BLINDED
else -> null
}
}
}
}

View File

@ -441,7 +441,10 @@ object OnionRequestAPI {
* Sends an onion request to `snode`. Builds new paths as needed. * Sends an onion request to `snode`. Builds new paths as needed.
*/ */
internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, publicKey: String? = null): Promise<Map<*, *>, Exception> { internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, publicKey: String? = null): Promise<Map<*, *>, Exception> {
val payload = mapOf( "method" to method.rawValue, "params" to parameters ) val payload = mapOf(
"method" to method.rawValue,
"params" to parameters
)
return sendOnionRequest(Destination.Snode(snode), payload).recover { exception -> return sendOnionRequest(Destination.Snode(snode), payload).recover { exception ->
val error = when (exception) { val error = when (exception) {
is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey) is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)

View File

@ -530,7 +530,7 @@ object SnodeAPI {
400, 500, 502, 503 -> { // Usually indicates that the snode isn't up to date 400, 500, 502, 503 -> { // Usually indicates that the snode isn't up to date
handleBadSnode() handleBadSnode()
} }
406 -> { 425 -> {
Log.d("Loki", "The user's clock is out of sync with the service node network.") Log.d("Loki", "The user's clock is out of sync with the service node network.")
broadcaster.broadcast("clockOutOfSync") broadcaster.broadcast("clockOutOfSync")
return Error.ClockOutOfSync return Error.ClockOutOfSync

View File

@ -1,5 +1,6 @@
rootProject.name = "session-android" rootProject.name = "session-android"
include ':app' include ':app'
include ':liblazysodium'
include ':libsession' include ':libsession'
include ':libsignal' include ':libsignal'