Browse Source

Add Session Id blinding (#862)

* 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.

* Add v4 onion request handling

* Update SOGS signature construction

* Fix SOGS signature construction

* Update onion request

* Update signature data

* Keep path prefixes for v4 endpoints

* Update SOGS signature message

* Rename to remove api version suffix

* Update onion response parsing

* Refactor file download paths

* Implement request batching

* Refactor batch response handling

* Handle batch endpoint responses

* Update batch endpoint responses

* Update attachment download handling

* Handle file downloads

* Handle inbox messages

* Fix issue with file downloads

* Preserve image bytearray encoding

* Refactor

* Open group message requests

* Check id blinding in user detail bottom sheet rather

* Message validation refactor

* Cache last inbox/outbox server ids

* Update message encryption/decryption

* Refactor

* Refactor

* Bypass user details bottom sheet in open groups for blinded session ids

* Fix capabilities call auth

* Refactor

* Revert default server details

* Update sodium dependency to forked repo

* Fix attachment upload

* Revert "Update sodium dependency to forked repo"

This reverts commit c7db9529f9.

* Add signed sodium lib

* Update contact id truncation and mention logic

* Open group inbox messaging fix

* Refactor

* Update blinded id check

* Fix open group message sends

* Fix crash on open group direct message send

* Direct message refactor

* Direct message encrypt/decrypt fixes

* Use updated curve25519 version

* Updated lazysodium dependency

* Update encryption/decryption calls

* Handle direct message parse errors

* Minor refactor

* Existing chat refactor

* Update encryption & decryption parameters

* Fix authenticated ciphertext size

* Set direct message sync target

* Update direct message thread lookup

* Add blinded id mapping table

* Add blinded id mapping table

* Update threads after sends

* Update open group message timestamp handling

* Filter unblinded contacts

* Format blinded id mentions

* Add message deleted field

* Hide open group inbox id

* Update message request response handling

* Update message request response sender handling

* Fix mentions of blinded ids

* Handle open group poll failure

* fix: add log for failed open group onion request, add decoding body for blinding required error at destination

* fix: change the error check

* Persist group members

* Reschedule polling after capabilities update

* Retry on other exceptions

* Minor refactor

* Open group profile fix

* Group member db schema update

* Fix ban request key

* Update ban response type

* Ban endpoint updates

* Ban endpoint updates

* Delete messages

Co-authored-by: charles <charles@oxen.io>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
pull/940/head
ceokot 4 months ago committed by GitHub
parent
commit
bee287bb7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      app/build.gradle
  2. 166
      app/src/androidTest/java/network/loki/messenger/SodiumUtilitiesTest.kt
  3. 6
      app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt
  4. 32
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
  5. 4
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt
  6. 4
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt
  7. 15
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
  8. 32
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
  9. 25
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt
  10. 88
      app/src/main/java/org/thoughtcrime/securesms/database/BlindedIdMappingDatabase.kt
  11. 10
      app/src/main/java/org/thoughtcrime/securesms/database/Database.java
  12. 72
      app/src/main/java/org/thoughtcrime/securesms/database/GroupMemberDatabase.kt
  13. 68
      app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt
  14. 16
      app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt
  15. 2
      app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java
  16. 10
      app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
  17. 7
      app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
  18. 4
      app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
  19. 11
      app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
  20. 192
      app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
  21. 6
      app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
  22. 18
      app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
  23. 2
      app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java
  24. 2
      app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt
  25. 8
      app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt
  26. 11
      app/src/main/java/org/thoughtcrime/securesms/groups/DefaultGroupsViewModel.kt
  27. 2
      app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt
  28. 52
      app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt
  29. 5
      app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupUtilities.kt
  30. 2
      app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
  31. 6
      app/src/main/java/org/thoughtcrime/securesms/home/UserDetailsBottomSheet.kt
  32. 12
      app/src/main/java/org/thoughtcrime/securesms/mms/PushMediaConstraints.java
  33. 12
      app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt
  34. 37
      app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
  35. 14
      app/src/main/java/org/thoughtcrime/securesms/notifications/LokiPushNotificationManager.kt
  36. 12
      app/src/main/java/org/thoughtcrime/securesms/repository/ConversationRepository.kt
  37. 3
      app/src/main/java/org/thoughtcrime/securesms/util/AvatarPlaceholderGenerator.kt
  38. 1
      app/src/main/java/org/thoughtcrime/securesms/util/ContactUtilities.kt
  39. 2
      gradle.properties
  40. 2
      liblazysodium/build.gradle
  41. BIN
      liblazysodium/lazysodium.aar
  42. 5
      libsession/build.gradle
  43. 27
      libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt
  44. 8
      libsession/src/main/java/org/session/libsession/messaging/BlindedIdMapping.kt
  45. 2
      libsession/src/main/java/org/session/libsession/messaging/contacts/Contact.kt
  46. 41
      libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt
  47. 8
      libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt
  48. 39
      libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt
  49. 28
      libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt
  50. 10
      libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt
  51. 7
      libsession/src/main/java/org/session/libsession/messaging/jobs/GroupAvatarDownloadJob.kt
  52. 6
      libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt
  53. 2
      libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt
  54. 7
      libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt
  55. 4
      libsession/src/main/java/org/session/libsession/messaging/jobs/OpenGroupDeleteJob.kt
  56. 35
      libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt
  57. 8
      libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupControlMessage.kt
  58. 10
      libsession/src/main/java/org/session/libsession/messaging/messages/control/ConfigurationMessage.kt
  59. 72
      libsession/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt
  60. 11
      libsession/src/main/java/org/session/libsession/messaging/open_groups/GroupMember.kt
  61. 17
      libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroup.kt
  62. 499
      libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupAPIV2.kt
  63. 830
      libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt
  64. 93
      libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt
  65. 72
      libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessageV2.kt
  66. 67
      libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt
  67. 35
      libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageEncrypter.kt
  68. 36
      libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt
  69. 132
      libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt
  70. 6
      libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt
  71. 16
      libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt
  72. 19
      libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt
  73. 274
      libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt
  74. 132
      libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPollerV2.kt
  75. 251
      libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt
  76. 375
      libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt
  77. 57
      libsession/src/main/java/org/session/libsession/snode/OnionRequestEncryption.kt
  78. 48
      libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt
  79. 4
      libsession/src/main/java/org/session/libsession/utilities/Address.kt
  80. 5
      libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt
  81. 20
      libsession/src/main/java/org/session/libsession/utilities/GroupUtil.kt
  82. 6
      libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt
  83. 10
      libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java
  84. 2
      libsignal/build.gradle
  85. 28
      libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt
  86. 15
      libsignal/src/main/java/org/session/libsignal/utilities/IdPrefix.kt
  87. 5
      libsignal/src/main/java/org/session/libsignal/utilities/JsonUtil.java
  88. 10
      libsignal/src/main/java/org/session/libsignal/utilities/Trimming.kt
  89. 4
      libsignal/src/main/java/org/session/libsignal/utilities/Validation.kt
  90. 1
      settings.gradle

4
app/build.gradle

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

166
app/src/androidTest/java/network/loki/messenger/SodiumUtilitiesTest.kt

@ -0,0 +1,166 @@
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.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.toHexString
@RunWith(AndroidJUnit4::class)
class SodiumUtilitiesTest {
private val publicKey: String = "88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"
private val privateKey: String = "30d796c1ddb4dc455fd998a98aa275c247494a9a7bde9c1fee86ae45cd585241"
private val edKeySeed: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9"
private val edPublicKey: String = "bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"
private val edSecretKey: String = "c010d89eccbaf5d1c6d19df766c6eedf965d4a28a56f87c9fc819edb59896dd9bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"
private val blindedPublicKey: String = "98932d4bccbe595a8789d7eb1629cefc483a0eaddc7e20e8fe5c771efafd9af5"
private val serverPublicKey: String = "c3b3c6f32f0ab5a57f853cc4f30f5da7fda5624b0c77b3fb0829de562ada081d"
private val edKeyPair = KeyPair(Key.fromHexString(edPublicKey), Key.fromHexString(edSecretKey))
@Test
fun generateBlindingFactorSuccess() {
val result = SodiumUtilities.generateBlindingFactor(serverPublicKey)
assertThat(result?.toHexString(), equalTo("84e3eb75028a9b73fec031b7448e322a68ca6485fad81ab1bead56f759ebeb0f"))
}
@Test
fun generateBlindingFactorFailure() {
val result = SodiumUtilities.generateBlindingFactor("Test")
assertNull(result?.toHexString())
}
@Test
fun blindedKeyPairSuccess() {
val result = SodiumUtilities.blindedKeyPair(serverPublicKey, edKeyPair)!!
assertThat(result.publicKey.asHexString.lowercase(), equalTo(blindedPublicKey))
assertThat(result.secretKey.asHexString.take(64).lowercase(), equalTo("16663322d6b684e1c9dcc02b9e8642c3affd3bc431a9ea9e63dbbac88ce7a305"))
}
@Test
fun blindedKeyPairFailurePublicKeyLength() {
val result = SodiumUtilities.blindedKeyPair(
serverPublicKey,
KeyPair(Key.fromHexString(edPublicKey.take(4)), Key.fromHexString(edKeySeed))
)
assertNull(result)
}
@Test
fun blindedKeyPairFailureSecretKeyLength() {
val result = SodiumUtilities.blindedKeyPair(
serverPublicKey,
KeyPair(Key.fromHexString(edPublicKey), Key.fromHexString(edSecretKey.take(4)))
)
assertNull(result)
}
@Test
fun blindedKeyPairFailureBlindingFactor() {
val result = SodiumUtilities.blindedKeyPair("Test", edKeyPair)
assertNull(result)
}
@Test
fun sogsSignature() {
val expectedSignature = "dcc086abdd2a740d9260b008fb37e12aa0ff47bd2bd9e177bbbec37fd46705a9072ce747bda66c788c3775cdd7ad60ad15a478e0886779aad5d795fd7bf8350d"
val result = SodiumUtilities.sogsSignature(
"TestMessage".toByteArray(),
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed("44d82cc15c0a5056825cae7520b6b52d000a23eb0c5ed94c4be2d9dc41d2d409"),
Hex.fromStringCondensed("0bb7815abb6ba5142865895f3e5286c0527ba4d31dbb75c53ce95e91ffe025a2")
)
assertThat(result?.toHexString(), equalTo(expectedSignature))
}
@Test
fun combineKeysSuccess() {
val result = SodiumUtilities.combineKeys(
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed(edPublicKey)
)
assertThat(result?.toHexString(), equalTo("1159b5d0fcfba21228eb2121a0f59712fa8276fc6e5547ff519685a40b9819e6"))
}
@Test
fun combineKeysFailure() {
val result = SodiumUtilities.combineKeys(
SodiumUtilities.generatePrivateKeyScalar(Hex.fromStringCondensed(edSecretKey))!!,
Hex.fromStringCondensed(publicKey)
)
assertNull(result?.toHexString())
}
@Test
fun sharedBlindedEncryptionKeySuccess() {
val result = SodiumUtilities.sharedBlindedEncryptionKey(
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed(blindedPublicKey),
Hex.fromStringCondensed(publicKey),
Hex.fromStringCondensed(blindedPublicKey)
)
assertThat(result?.toHexString(), equalTo("388ee09e4c356b91f1cce5cc0aa0cf59e8e8cade69af61685d09c2d2731bc99e"))
}
@Test
fun sharedBlindedEncryptionKeyFailure() {
val result = SodiumUtilities.sharedBlindedEncryptionKey(
Hex.fromStringCondensed(edSecretKey),
Hex.fromStringCondensed(publicKey),
Hex.fromStringCondensed(edPublicKey),
Hex.fromStringCondensed(publicKey)
)
assertNull(result?.toHexString())
}
@Test
fun sessionIdSuccess() {
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", serverPublicKey)
assertTrue(result)
}
@Test
fun sessionIdFailureInvalidSessionId() {
val result = SodiumUtilities.sessionId("AB$publicKey", "15$blindedPublicKey", serverPublicKey)
assertFalse(result)
}
@Test
fun sessionIdFailureInvalidBlindedId() {
val result = SodiumUtilities.sessionId("05$publicKey", "AB$blindedPublicKey", serverPublicKey)
assertFalse(result)
}
@Test
fun sessionIdFailureBlindingFactor() {
val result = SodiumUtilities.sessionId("05$publicKey", "15$blindedPublicKey", "Test")
assertFalse(result)
}
}

6
app/src/main/java/org/thoughtcrime/securesms/components/ProfilePictureView.kt

@ -15,6 +15,7 @@ import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.avatars.ResourceContactPhoto
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.mms.GlideRequests
@ -57,6 +58,11 @@ class ProfilePictureView @JvmOverloads constructor(
val apk = members.getOrNull(1)?.serialize() ?: ""
additionalPublicKey = apk
additionalDisplayName = getUserDisplayName(apk)
} else if(recipient.isOpenGroupInboxRecipient) {
val publicKey = GroupUtil.getDecodedOpenGroupInbox(recipient.address.serialize())
this.publicKey = publicKey
displayName = getUserDisplayName(publicKey)
additionalPublicKey = null
} else {
val publicKey = recipient.address.toString()
this.publicKey = publicKey

32
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt

@ -58,19 +58,22 @@ import org.session.libsession.messaging.messages.control.DataExtractionNotificat
import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupApi
import org.session.libsession.messaging.sending_receiving.MessageSender
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.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.Address.Companion.fromSerialized
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.MediaTypes
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.concurrent.SimpleTask
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.recipients.RecipientModifiedListener
import org.session.libsignal.crypto.MnemonicCodec
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.guava.Optional
import org.session.libsignal.utilities.hexEncodedPrivateKey
@ -109,6 +112,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.SessionContactDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.Storage
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
@ -167,6 +171,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
@Inject lateinit var smsDb: SmsDatabase
@Inject lateinit var mmsDb: MmsDatabase
@Inject lateinit var lokiMessageDb: LokiMessageDatabase
@Inject lateinit var storage: Storage
@Inject lateinit var viewModelFactory: ConversationViewModel.AssistedFactory
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
@ -177,9 +182,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private val viewModel: ConversationViewModel by viewModels {
var threadId = intent.getLongExtra(THREAD_ID, -1L)
if (threadId == -1L) {
intent.getParcelableExtra<Address>(ADDRESS)?.let { address ->
val recipient = Recipient.from(this, address, false)
threadId = threadDb.getOrCreateThreadIdFor(recipient)
intent.getParcelableExtra<Address>(ADDRESS)?.let { it ->
threadId = threadDb.getThreadIdIfExistsFor(it.serialize())
if (threadId == -1L) {
val sessionId = SessionId(it.serialize())
val openGroup = lokiThreadDb.getOpenGroupChat(intent.getLongExtra(FROM_GROUP_THREAD_ID, -1))
val address = if (sessionId.prefix == IdPrefix.BLINDED && openGroup != null) {
storage.getOrCreateBlindedIdMapping(sessionId.hexString, openGroup.server, openGroup.publicKey).sessionId?.let {
fromSerialized(it)
} ?: run {
val openGroupInboxId =
"${openGroup.server}!${openGroup.publicKey}!${sessionId.hexString}".toByteArray()
fromSerialized(GroupUtil.getEncodedOpenGroupInboxID(openGroupInboxId))
}
} else {
it
}
val recipient = Recipient.from(this, address, false)
threadId = threadDb.getOrCreateThreadIdFor(recipient)
}
} ?: finish()
}
viewModelFactory.create(threadId)
@ -263,6 +284,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// Extras
const val THREAD_ID = "thread_id"
const val ADDRESS = "address"
const val FROM_GROUP_THREAD_ID = "from_group_thread_id"
const val SCROLL_MESSAGE_ID = "scroll_message_id"
const val SCROLL_MESSAGE_AUTHOR = "scroll_message_author"
// Request codes
@ -508,7 +530,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun getLatestOpenGroupInfoIfNeeded() {
val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) ?: return
OpenGroupAPIV2.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() }
OpenGroupApi.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() }
}
// called from onCreate

4
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/components/MentionCandidateView.kt

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

4
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt

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

15
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt

@ -5,13 +5,17 @@ import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import network.loki.messenger.R
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.IdPrefix
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.groups.OpenGroupManager
class ConversationActionModeCallback(private val adapter: ConversationAdapter, private val threadID: Long,
private val context: Context) : ActionMode.Callback {
@ -34,6 +38,9 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
val openGroup = DatabaseComponent.get(context).lokiThreadDatabase().getOpenGroupChat(threadID)
val thread = DatabaseComponent.get(context).threadDatabase().getRecipientForThreadId(threadID)!!
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val edKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!
val blindedPublicKey = openGroup?.publicKey?.let { SodiumUtilities.blindedKeyPair(it, edKeyPair)?.publicKey?.asBytes }
?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString
fun userCanDeleteSelectedItems(): Boolean {
val allSentByCurrentUser = selectedItems.all { it.isOutgoing }
@ -41,13 +48,13 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (!ConversationActivityV2.IS_UNSEND_REQUESTS_ENABLED) {
if (openGroup == null) { return true }
if (allSentByCurrentUser) { return true }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
}
val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing }
if (openGroup == null) { return allSentByCurrentUser || allReceivedByCurrentUser }
if (allSentByCurrentUser) { return true }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
}
fun userCanBanSelectedUsers(): Boolean {
if (openGroup == null) { return false }
@ -55,7 +62,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p
if (anySentByCurrentUser) { return false } // Users can't ban themselves
val selectedUsers = selectedItems.map { it.recipient.address.toString() }.toSet()
if (selectedUsers.size > 1) { return false }
return OpenGroupAPIV2.isUserModerator(userPublicKey, openGroup.room, openGroup.server)
return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey)
}
// Delete message
menu.findItem(R.id.menu_context_delete_message).isVisible = userCanDeleteSelectedItems()

32
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.v2.messages
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Rect
@ -23,16 +24,19 @@ import network.loki.messenger.R
import network.loki.messenger.databinding.ViewVisibleMessageBinding
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.contacts.Contact.ContactContext
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ViewUtil
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.ThreadUtils
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
import org.thoughtcrime.securesms.database.LokiThreadDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.MmsSmsDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
@ -140,15 +144,27 @@ class VisibleMessageView : LinearLayout {
binding.profilePictureView.root.glide = glide
binding.profilePictureView.root.update(message.individualRecipient)
binding.profilePictureView.root.setOnClickListener {
showUserDetails(senderSessionID, threadID)
if (thread.isOpenGroupRecipient) {
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED) {
val intent = Intent(context, ConversationActivityV2::class.java)
intent.putExtra(ConversationActivityV2.FROM_GROUP_THREAD_ID, threadID)
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(senderSessionID))
context.startActivity(intent)
}
} else {
maybeShowUserDetails(senderSessionID, threadID)
}
}
if (thread.isOpenGroupRecipient) {
val openGroup = lokiThreadDb.getOpenGroupChat(threadID) ?: return
val isModerator = OpenGroupAPIV2.isUserModerator(
senderSessionID,
openGroup.room,
openGroup.server
)
var standardPublicKey = ""
var blindedPublicKey: String? = null
if (IdPrefix.fromValue(senderSessionID) == IdPrefix.BLINDED) {
blindedPublicKey = senderSessionID
} else {
standardPublicKey = senderSessionID
}
val isModerator = OpenGroupManager.isUserModerator(context, openGroup.groupId, standardPublicKey, blindedPublicKey)
binding.moderatorIconImageView.isVisible = !message.isOutgoing && isModerator
}
}
@ -403,7 +419,7 @@ class VisibleMessageView : LinearLayout {
pressCallback = null
}
private fun showUserDetails(publicKey: String, threadID: Long) {
private fun maybeShowUserDetails(publicKey: String, threadID: Long) {
val userDetailsBottomSheet = UserDetailsBottomSheet()
val bundle = bundleOf(
UserDetailsBottomSheet.ARGUMENT_PUBLIC_KEY to publicKey,

25
app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/MentionUtilities.kt

@ -11,6 +11,7 @@ import androidx.core.content.res.ResourcesCompat
import network.loki.messenger.R
import nl.komponents.kovenant.combine.Tuple2
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
import org.thoughtcrime.securesms.util.UiModeUtilities
@ -20,39 +21,27 @@ object MentionUtilities {
@JvmStatic
fun highlightMentions(text: CharSequence, threadID: Long, context: Context): String {
val threadDB = DatabaseComponent.get(context).threadDatabase()
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
return highlightMentions(text, false, isOpenGroup, context).toString() // isOutgoingMessage is irrelevant
}
@JvmStatic
fun highlightMentions(text:CharSequence, isOpenGroup: Boolean, context: Context): String {
return highlightMentions(text, false, isOpenGroup, context).toString()
return highlightMentions(text, false, threadID, context).toString() // isOutgoingMessage is irrelevant
}
@JvmStatic
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, threadID: Long, context: Context): SpannableString {
val threadDB = DatabaseComponent.get(context).threadDatabase()
val isOpenGroup = threadDB.getRecipientForThreadId(threadID)?.isOpenGroupRecipient ?: false
return highlightMentions(text, isOutgoingMessage, isOpenGroup, context) // isOutgoingMessage is irrelevant
}
@JvmStatic
fun highlightMentions(text: CharSequence, isOutgoingMessage: Boolean, isOpenGroup: Boolean, context: Context): SpannableString {
@Suppress("NAME_SHADOWING") var text = text
val pattern = Pattern.compile("@[0-9a-fA-F]*")
var matcher = pattern.matcher(text)
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()
var startIndex = 0
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val openGroup = DatabaseComponent.get(context).storage().getOpenGroup(threadID)
if (matcher.find(startIndex)) {
while (true) {
val publicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
val userDisplayName: String? = if (publicKey.equals(userPublicKey, ignoreCase = true)) {
TextSecurePreferences.getProfileName(context)
val isUserBlindedPublicKey = openGroup?.let { SodiumUtilities.sessionId(userPublicKey, publicKey, it.publicKey) } ?: false
val userDisplayName: String? = if (publicKey.equals(userPublicKey, ignoreCase = true) || isUserBlindedPublicKey) {
context.getString(R.string.MessageRecord_you)
} else {
val contact = DatabaseComponent.get(context).sessionContactDatabase().getContactWithSessionID(publicKey)
@Suppress("NAME_SHADOWING") val context = if (isOpenGroup) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
@Suppress("NAME_SHADOWING") val context = if (openGroup != null) Contact.ContactContext.OPEN_GROUP else Contact.ContactContext.REGULAR
contact?.displayName(context)
}
if (userDisplayName != null) {

88
app/src/main/java/org/thoughtcrime/securesms/database/BlindedIdMappingDatabase.kt

@ -0,0 +1,88 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import androidx.core.database.getStringOrNull
import org.session.libsession.messaging.BlindedIdMapping
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
class BlindedIdMappingDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
companion object {
const val TABLE_NAME = "blinded_id_mapping"
const val ROW_ID = "_id"
const val BLINDED_PK = "blinded_pk"
const val SESSION_PK = "session_pk"
const val SERVER_URL = "server_url"
const val SERVER_PK = "server_pk"
@JvmField
val CREATE_BLINDED_ID_MAPPING_TABLE_COMMAND = """
CREATE TABLE $TABLE_NAME (
$ROW_ID INTEGER PRIMARY KEY,
$BLINDED_PK TEXT NOT NULL,
$SESSION_PK TEXT DEFAULT NULL,
$SERVER_URL TEXT NOT NULL,
$SERVER_PK TEXT NOT NULL
)
""".trimIndent()
private fun readBlindedIdMapping(cursor: Cursor): BlindedIdMapping {
return BlindedIdMapping(
blindedId = cursor.getString(cursor.getColumnIndexOrThrow(BLINDED_PK)),
sessionId = cursor.getStringOrNull(cursor.getColumnIndexOrThrow(SESSION_PK)),
serverUrl = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_URL)),
serverId = cursor.getString(cursor.getColumnIndexOrThrow(SERVER_PK)),
)
}
}
fun getBlindedIdMapping(blindedId: String): List<BlindedIdMapping> {
val query = "$BLINDED_PK = ?"
val args = arrayOf(blindedId)
val mappings: MutableList<BlindedIdMapping> = mutableListOf()
readableDatabase.query(TABLE_NAME, null, query, args, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
mappings += readBlindedIdMapping(cursor)
}
}
return mappings
}
fun addBlindedIdMapping(blindedIdMapping: BlindedIdMapping) {
writableDatabase.beginTransaction()
try {
val values = ContentValues().apply {
put(BLINDED_PK, blindedIdMapping.blindedId)
put(SERVER_PK, blindedIdMapping.sessionId)
put(SERVER_URL, blindedIdMapping.serverUrl)
put(SERVER_PK, blindedIdMapping.serverId)
}
writableDatabase.insert(TABLE_NAME, null, values)
writableDatabase.setTransactionSuccessful()
} finally {
writableDatabase.endTransaction()
}
}
fun getBlindedIdMappingsExceptFor(server: String): List<BlindedIdMapping> {
val query = "$SESSION_PK IS NOT NULL AND $SERVER_URL <> ?"
val args = arrayOf(server)
val mappings: MutableList<BlindedIdMapping> = mutableListOf()
readableDatabase.query(TABLE_NAME, null, query, args, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
mappings += readBlindedIdMapping(cursor)
}
}
return mappings
}
}

10
app/src/main/java/org/thoughtcrime/securesms/database/Database.java

@ -23,6 +23,8 @@ import android.database.Cursor;
import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.session.libsession.utilities.WindowDebouncer;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@ -92,4 +94,12 @@ public abstract class Database {
this.databaseHelper = databaseHelper;
}
protected SQLiteDatabase getReadableDatabase() {
return databaseHelper.getReadableDatabase();
}
protected SQLiteDatabase getWritableDatabase() {
return databaseHelper.getWritableDatabase();
}
}

72
app/src/main/java/org/thoughtcrime/securesms/database/GroupMemberDatabase.kt

@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
import org.session.libsession.messaging.open_groups.GroupMember
import org.session.libsession.messaging.open_groups.GroupMemberRole
class GroupMemberDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) {
companion object {
const val TABLE_NAME = "group_member"
const val GROUP_ID = "group_id"
const val PROFILE_ID = "profile_id"
const val ROLE = "role"
private val allColumns = arrayOf(GROUP_ID, PROFILE_ID, ROLE)
@JvmField
val CREATE_GROUP_MEMBER_TABLE_COMMAND = """
CREATE TABLE $TABLE_NAME (
$GROUP_ID TEXT NOT NULL,
$PROFILE_ID TEXT NOT NULL,
$ROLE TEXT NOT NULL,
PRIMARY KEY ($GROUP_ID, $PROFILE_ID)
)
""".trimIndent()
private fun readGroupMember(cursor: Cursor): GroupMember {
return GroupMember(
groupId = cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)),
profileId = cursor.getString(cursor.getColumnIndexOrThrow(PROFILE_ID)),
role = GroupMemberRole.valueOf(cursor.getString(cursor.getColumnIndexOrThrow(ROLE))),
)
}
}
fun getGroupMemberRoles(groupId: String, profileId: String): List<GroupMemberRole> {
val query = "$GROUP_ID = ? AND $PROFILE_ID = ?"
val args = arrayOf(groupId, profileId)
val mappings: MutableList<GroupMember> = mutableListOf()
readableDatabase.query(TABLE_NAME, allColumns, query, args, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
mappings += readGroupMember(cursor)
}
}
return mappings.map { it.role }
}
fun addGroupMember(member: GroupMember) {
writableDatabase.beginTransaction()
try {
val values = ContentValues().apply {
put(GROUP_ID, member.groupId)
put(PROFILE_ID, member.profileId)
put(ROLE, member.role.name)
}
val query = "$GROUP_ID = ? AND $PROFILE_ID = ?"
val args = arrayOf(member.groupId, member.profileId)
writableDatabase.insertOrUpdate(TABLE_NAME, values, query, args)
writableDatabase.setTransactionSuccessful()
} finally {
writableDatabase.endTransaction()
}
}
}

68
app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt

@ -12,7 +12,7 @@ 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.removing05PrefixIfNeeded
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
@ -127,6 +127,21 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
"""
const val INSERT_RECEIVED_HASHES_DATA = "INSERT OR IGNORE INTO $receivedMessageHashValuesTable($publicKey, $receivedMessageHashValues) SELECT $publicKey, $receivedMessageHashValues FROM $legacyReceivedMessageHashValuesTable3;"
const val DROP_LEGACY_RECEIVED_HASHES = "DROP TABLE $legacyReceivedMessageHashValuesTable3;"
// 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 = "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 = "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)"
// region Deprecated
private val deviceLinkCache = "loki_pairing_authorisation_cache"
@ -423,14 +438,14 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
override fun getUserX25519KeyPair(): ECKeyPair {
val keyPair = IdentityKeyUtil.getIdentityKeyPair(context)
return ECKeyPair(DjbECPublicKey(keyPair.publicKey.serialize().removing05PrefixIfNeeded()), DjbECPrivateKey(keyPair.privateKey.serialize()))
return ECKeyPair(DjbECPublicKey(keyPair.publicKey.serialize().removingIdPrefixIfNeeded()), DjbECPrivateKey(keyPair.privateKey.serialize()))
}
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String) {
val database = databaseHelper.writableDatabase
val timestamp = Date().time.toString()
val index = "$groupPublicKey-$timestamp"
val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removing05PrefixIfNeeded()
val encryptionKeyPairPublicKey = encryptionKeyPair.publicKey.serialize().toHexString().removingIdPrefixIfNeeded()
val encryptionKeyPairPrivateKey = encryptionKeyPair.privateKey.serialize().toHexString()
val row = wrap(mapOf(closedGroupsEncryptionKeyPairIndex to index, Companion.encryptionKeyPairPublicKey to encryptionKeyPairPublicKey,
Companion.encryptionKeyPairPrivateKey to encryptionKeyPairPrivateKey ))
@ -481,6 +496,53 @@ 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(capabilities)
}?.split(",") ?: emptyList()
}
fun setLastInboxMessageId(serverName: String, newValue: Long) {
val database = databaseHelper.writableDatabase
val row = wrap(mapOf(server to serverName, lastInboxMessageServerId to newValue.toString()))
database.insertOrUpdate(lastInboxMessageServerIdTable, row, "$server = ?", wrap(serverName))
}
fun getLastInboxMessageId(serverName: String): Long? {
val database = databaseHelper.writableDatabase
return database.get(lastInboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor ->
cursor.getInt(lastInboxMessageServerId)
}?.toLong()
}
fun removeLastInboxMessageId(serverName: String) {
databaseHelper.writableDatabase.delete(lastInboxMessageServerIdTable, "$server = ?", wrap(serverName))
}
fun setLastOutboxMessageId(serverName: String, newValue: Long) {
val database = databaseHelper.writableDatabase
val row = wrap(mapOf(server to serverName, lastOutboxMessageServerId to newValue.toString()))
database.insertOrUpdate(lastOutboxMessageServerIdTable, row, "$server = ?", wrap(serverName))
}
fun getLastOutboxMessageId(serverName: String): Long? {
val database = databaseHelper.writableDatabase
return database.get(lastOutboxMessageServerIdTable, "$server = ?", wrap(serverName)) { cursor ->
cursor.getInt(lastOutboxMessageServerId)
}?.toLong()
}
fun removeLastOutboxMessageId(serverName: String) {
databaseHelper.writableDatabase.delete(lastOutboxMessageServerIdTable, "$server = ?", wrap(serverName))
}
override fun getForkInfo(): ForkInfo {
val database = databaseHelper.readableDatabase
val queryCursor = database.query(FORK_INFO_TABLE, arrayOf(HF_VALUE, SF_VALUE), "$DUMMY_KEY = $DUMMY_VALUE", null, null, null, null)

16
app/src/main/java/org/thoughtcrime/securesms/database/LokiThreadDatabase.kt

@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.JsonUtil
@ -30,16 +30,16 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
return DatabaseComponent.get(context).threadDatabase().getOrCreateThreadIdFor(recipient)
}
fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
fun getAllOpenGroups(): Map<Long, OpenGroup> {
val database = databaseHelper.readableDatabase
var cursor: Cursor? = null
val result = mutableMapOf<Long, OpenGroupV2>()
val result = mutableMapOf<Long, OpenGroup>()
try {
cursor = database.rawQuery("select * from $publicChatTable", null)
while (cursor != null && cursor.moveToNext()) {
val threadID = cursor.getLong(threadID)
val string = cursor.getString(publicChat)
val openGroup = OpenGroupV2.fromJSON(string)
val openGroup = OpenGroup.fromJSON(string)
if (openGroup != null) result[threadID] = openGroup
}
} catch (e: Exception) {
@ -50,25 +50,25 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
return result
}
fun getOpenGroupChat(threadID: Long): OpenGroupV2? {
fun getOpenGroupChat(threadID: Long): OpenGroup? {
if (threadID < 0) {
return null
}
val database = databaseHelper.readableDatabase
return database.get(publicChatTable, "${Companion.threadID} = ?", arrayOf(threadID.toString())) { cursor ->
val json = cursor.getString(publicChat)
OpenGroupV2.fromJSON(json)
OpenGroup.fromJSON(json)
}
}
fun setOpenGroupChat(openGroupV2: OpenGroupV2, threadID: Long) {
fun setOpenGroupChat(openGroup: OpenGroup, threadID: Long) {
if (threadID < 0) {
return
}
val database = databaseHelper.writableDatabase
val contentValues = ContentValues(2)
contentValues.put(Companion.threadID, threadID)
contentValues.put(publicChat, JsonUtil.toJson(openGroupV2.toJson()))
contentValues.put(publicChat, JsonUtil.toJson(openGroup.toJson()))
database.insertOrUpdate(publicChatTable, contentValues, "${Companion.threadID} = ?", arrayOf(threadID.toString()))
}

2
app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java

@ -42,6 +42,8 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
public abstract boolean deleteMessage(long messageId);
public abstract void updateThreadId(long fromId, long toId);
public void addMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) {
try {
addToDocument(messageId, MISMATCHED_IDENTITIES,

10
app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt

@ -1031,6 +1031,16 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa
return threadDeleted
}
override fun updateThreadId(fromId: Long, toId: Long) {
val contentValues = ContentValues(1)
contentValues.put(THREAD_ID, toId)
val db = databaseHelper.writableDatabase
db.update(SmsDatabase.TABLE_NAME, contentValues, "$THREAD_ID = ?", arrayOf("$fromId"))
notifyConversationListeners(toId)
notifyConversationListListeners()
}
fun deleteThread(threadId: Long) {
deleteThreads(setOf(threadId))
}

7
app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java

@ -93,12 +93,11 @@ public class MmsSmsDatabase extends Database {
MmsSmsDatabase.Reader reader = readerFor(cursor);
MessageRecord messageRecord;
boolean isOwnNumber = Util.isOwnNumber(context, serializedAuthor);
while ((messageRecord = reader.getNext()) != null) {
if ((Util.isOwnNumber(context, serializedAuthor) && messageRecord.isOutgoing()) ||
(!Util.isOwnNumber(context, serializedAuthor)
&& messageRecord.getIndividualRecipient().getAddress().serialize().equals(serializedAuthor)
))
if ((isOwnNumber && messageRecord.isOutgoing()) ||
(!isOwnNumber && messageRecord.getIndividualRecipient().getAddress().serialize().equals(serializedAuthor)))
{
return messageRecord;
}

4
app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java

@ -255,10 +255,6 @@ public class RecipientDatabase extends Database {
recipient.resolve().setApproved(approved);
}
public void setAllApproved(List<String> addresses) {
}
public void setApprovedMe(@NonNull Recipient recipient, boolean approvedMe) {
ContentValues values = new ContentValues();
values.put(APPROVED_ME, approvedMe ? 1 : 0);

11
app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java

@ -575,6 +575,17 @@ public class SmsDatabase extends MessagingDatabase {
return threadDeleted;
}
@Override
public void updateThreadId(long fromId, long toId) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(MmsSmsColumns.THREAD_ID, toId);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.update(TABLE_NAME, contentValues, THREAD_ID + " = ?", new String[] {fromId + ""});
notifyConversationListeners(toId);
notifyConversationListListeners();
}
private boolean isDuplicate(IncomingTextMessage message, long threadId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?",

192
app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
import org.session.libsession.database.StorageProtocol
import org.session.libsession.messaging.BlindedIdMapping
import org.session.libsession.messaging.calls.CallMessageType
import org.session.libsession.messaging.contacts.Contact
import org.session.libsession.messaging.jobs.AttachmentUploadJob
@ -22,12 +23,15 @@ import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.open_groups.OpenGroupV2
import org.session.libsession.messaging.open_groups.GroupMember
import org.session.libsession.messaging.open_groups.OpenGroup
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
import org.session.libsession.messaging.utilities.SessionId
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.messaging.utilities.UpdateMessageData
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.Address
@ -40,6 +44,7 @@ import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.crypto.ecc.ECKeyPair
import org.session.libsignal.messages.SignalServiceAttachmentPointer
import org.session.libsignal.messages.SignalServiceGroup
import org.session.libsignal.utilities.IdPrefix
import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.ApplicationContext
@ -73,7 +78,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)
@ -125,8 +130,10 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
runIncrement: Boolean,
runThreadUpdate: Boolean): Long? {
var messageID: Long? = null
val senderAddress = Address.fromSerialized(message.sender!!)
val senderAddress = fromSerialized(message.sender!!)
val isUserSender = (message.sender!! == getUserPublicKey())
val isUserBlindedSender = message.threadID?.takeIf { it >= 0 }?.let { getOpenGroup(it)?.publicKey }
?.let { SodiumUtilities.sessionId(getUserPublicKey()!!, message.sender!!, it) } ?: false
val group: Optional<SignalServiceGroup> = when {
openGroupID != null -> Optional.of(SignalServiceGroup(openGroupID.toByteArray(), SignalServiceGroup.GroupType.PUBLIC_CHAT))
groupPublicKey != null -> {
@ -138,17 +145,17 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val pointers = attachments.mapNotNull {
it.toSignalAttachment()
}
val targetAddress = if (isUserSender && !message.syncTarget.isNullOrEmpty()) {
Address.fromSerialized(message.syncTarget!!)
val targetAddress = if ((isUserSender || isUserBlindedSender) && !message.syncTarget.isNullOrEmpty()) {
fromSerialized(message.syncTarget!!)
} else if (group.isPresent) {
Address.fromSerialized(GroupUtil.getEncodedId(group.get()))
fromSerialized(GroupUtil.getEncodedId(group.get()))
} else {
senderAddress
}
val targetRecipient = Recipient.from(context, targetAddress, false)
if (!targetRecipient.isGroupRecipient) {
val recipientDb = DatabaseComponent.get(context).recipientDatabase()
if (isUserSender) {
if (isUserSender || isUserBlindedSender) {
recipientDb.setApproved(targetRecipient, true)
} else {
recipientDb.setApprovedMe(targetRecipient, true)
@ -158,7 +165,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val quote: Optional<QuoteModel> = if (quotes != null) Optional.of(quotes) else Optional.absent()
val linkPreviews: Optional<List<LinkPreview>> = if (linkPreview.isEmpty()) Optional.absent() else Optional.of(linkPreview.mapNotNull { it!! })
val mmsDatabase = DatabaseComponent.get(context).mmsDatabase()
val insertResult = if (message.sender == getUserPublicKey()) {
val insertResult = if (isUserSender || isUserBlindedSender) {
val mediaMessage = OutgoingMediaMessage.from(message, targetRecipient, pointers, quote.orNull(), linkPreviews.orNull()?.firstOrNull())
mmsDatabase.insertSecureDecryptedMessageOutbox(mediaMessage, message.threadID ?: -1, message.sentTimestamp!!, runThreadUpdate)
} else {
@ -176,7 +183,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
val smsDatabase = DatabaseComponent.get(context).smsDatabase()
val isOpenGroupInvitation = (message.openGroupInvitation != null)
val insertResult = if (message.sender == getUserPublicKey()) {
val insertResult = if (isUserSender || isUserBlindedSender) {
val textMessage = if (isOpenGroupInvitation) OutgoingTextMessage.fromOpenGroupInvitation(message.openGroupInvitation, targetRecipient, message.sentTimestamp)
else OutgoingTextMessage.from(message, targetRecipient)
smsDatabase.insertMessageOutbox(message.threadID ?: -1, textMessage, message.sentTimestamp!!, runThreadUpdate)
@ -259,12 +266,12 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).lokiAPIDatabase().setAuthToken(id, null)
}
override fun getV2OpenGroup(threadId: Long): OpenGroupV2? {
override fun getOpenGroup(threadId: Long): OpenGroup? {
if (threadId.toInt() < 0) { return null }
val database = databaseHelper.readableDatabase
return database.get(LokiThreadDatabase.publicChatTable, "${LokiThreadDatabase.threadID} = ?", arrayOf( threadId.toString() )) { cursor ->
val publicChatAsJson = cursor.getString(LokiThreadDatabase.publicChat)
OpenGroupV2.fromJSON(publicChatAsJson)
OpenGroup.fromJSON(publicChatAsJson)
}
}
@ -309,6 +316,14 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).lokiMessageDatabase().setOriginalThreadID(messageID, serverID, threadID)
}
override fun getOpenGroup(room: String, server: String): OpenGroup? {
return getAllOpenGroups().values.firstOrNull { it.server == server && it.room == room }
}
override fun addGroupMember(member: GroupMember) {
DatabaseComponent.get(context).groupMemberDatabase().addGroupMember(member)
}
override fun isDuplicateMessage(timestamp: Long): Boolean {
return getReceivedMessageTimestamps().contains(timestamp)
}
@ -335,7 +350,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()
}
@ -453,7 +468,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<String>, admins: Collection<String>, 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()
@ -462,7 +477,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
override fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceGroup.Type, name: String, members: Collection<String>, admins: Collection<String>, 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())
@ -475,7 +490,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
}
@ -528,8 +543,20 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context,
DatabaseComponent.get(context).recipientDatabase().setExpireMessages(recipient, duration);
}
override fun getAllV2OpenGroups(): Map<Long, OpenGroupV2> {
return DatabaseComponent.get(context).lokiThreadDatabase().getAllV2OpenGroups()
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> {