From 888eda4ba9aa1611bac962628472300fe019f1b1 Mon Sep 17 00:00:00 2001 From: Brice Date: Fri, 27 Nov 2020 15:56:16 +1100 Subject: [PATCH 01/24] change package name + start of implementation --- libsession/build.gradle | 3 + .../messaging/messages/Destination.kt | 8 +- .../libsession/messaging/messages/Message.kt | 32 +++- .../messages/control/ClosedGroupUpdate.kt | 154 +++++++++++++++++- .../messages/control/ControlMessage.kt | 6 +- .../messages/control/ExpirationTimerUpdate.kt | 11 +- .../messaging/messages/control/ReadReceipt.kt | 11 +- .../messages/control/TypingIndicator.kt | 11 +- .../messages/control/unused/NullMessage.kt | 12 +- .../messages/control/unused/SessionRequest.kt | 12 +- .../messaging/messages/visible/Contact.kt | 13 +- .../messaging/messages/visible/LinkPreview.kt | 13 +- .../messaging/messages/visible/Profile.kt | 13 +- .../messaging/messages/visible/Quote.kt | 13 +- .../messages/visible/VisibleMessage.kt | 6 +- .../visible/attachments/Attachment.kt | 14 +- 16 files changed, 303 insertions(+), 29 deletions(-) diff --git a/libsession/build.gradle b/libsession/build.gradle index aea50a3003..47fc374def 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -38,7 +38,10 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.2.1' + implementation "com.google.protobuf:protobuf-java:$protobufVersion" testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + + implementation project(":libsignal") } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt index bd39ccbbce..8c255bdce5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt @@ -1,5 +1,9 @@ -package org.session.messaging.messages +package org.session.libsession.messaging.messages -enum class Destination { +sealed class Destination { + + class Contact(val publicKey: String) + class ClosedGroup(val groupPublicKey: String) + class OpenGroup(val channel: Long, val server: String) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt index b1ca1ce0ca..6176cee7c8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt @@ -1,5 +1,33 @@ -package org.session.messaging.messages +package org.session.libsession.messaging.messages -open class Message { +import org.session.libsignal.service.internal.push.SignalServiceProtos + +abstract class Message { + + var id: String? = null + var threadID: String? = null + var sentTimestamp: Long? = null + var receivedTimestamp: Long? = null + var recipient: String? = null + var sender: String? = null + var groupPublicKey: String? = null + var openGroupServerMessageID: Long? = null + + companion object { + @JvmStatic + val ttl = 2 * 24 * 60 * 60 * 1000 + + //fun fromProto(proto: SignalServiceProtos.Content): Message? {} + } + + open fun isValid(): Boolean { + sentTimestamp = if (sentTimestamp!! > 0) sentTimestamp else return false + receivedTimestamp = if (receivedTimestamp!! > 0) receivedTimestamp else return false + return sender != null && recipient != null + } + + + + abstract fun toProto(): SignalServiceProtos.Content? } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt index e6c673c7ee..18f424ba8e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt @@ -1,4 +1,154 @@ -package org.session.messaging.messages.control +package org.session.libsession.messaging.messages.control -class ClosedGroupUpdate : ControlMessage() { +import com.google.protobuf.ByteString +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey + +class ClosedGroupUpdate() : ControlMessage() { + + companion object { + const val TAG = "ClosedGroupUpdate" + + fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupUpdate? { + val closedGroupUpdateProto = proto.dataMessage?.closedGroupUpdate ?: return null + val groupPublicKey = closedGroupUpdateProto.groupPublicKey + var kind: Kind? = null + when(closedGroupUpdateProto.type) { + SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> { + val name = closedGroupUpdateProto.name ?: return null + val groupPrivateKey = closedGroupUpdateProto.groupPrivateKey ?: return null + val senderKeys = closedGroupUpdateProto.senderKeysList.map { ClosedGroupSenderKey.fromProto(it) } + kind = Kind.New( + groupPublicKey = groupPublicKey.toByteArray(), + name = name, + groupPrivateKey = groupPrivateKey.toByteArray(), + senderKeys = senderKeys, + members = closedGroupUpdateProto.membersList.map { it.toByteArray() }, + admins = closedGroupUpdateProto.adminsList.map { it.toByteArray() } + ) + } + SignalServiceProtos.ClosedGroupUpdate.Type.INFO -> { + val name = closedGroupUpdateProto.name ?: return null + val senderKeys = closedGroupUpdateProto.senderKeysList.map { ClosedGroupSenderKey.fromProto(it) } + kind = Kind.Info( + groupPublicKey = groupPublicKey.toByteArray(), + name = name, + senderKeys = senderKeys, + members = closedGroupUpdateProto.membersList.map { it.toByteArray() }, + admins = closedGroupUpdateProto.adminsList.map { it.toByteArray() } + ) + } + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST -> { + kind = Kind.SenderKeyRequest(groupPublicKey = groupPublicKey.toByteArray()) + } + SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY -> { + val senderKeyProto = closedGroupUpdateProto.senderKeysList?.first() ?: return null + kind = Kind.SenderKey( + groupPublicKey = groupPublicKey.toByteArray(), + senderKey = ClosedGroupSenderKey.fromProto(senderKeyProto) + ) + } + } + return ClosedGroupUpdate(kind) + } + } + //private val TAG: String = ClosedGroupUpdate::class.java.simpleName + + // Kind enum + sealed class Kind { + class New(val groupPublicKey: ByteArray, val name: String, val groupPrivateKey: ByteArray, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class Info(val groupPublicKey: ByteArray, val name: String, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class SenderKeyRequest(val groupPublicKey: ByteArray) : Kind() + class SenderKey(val groupPublicKey: ByteArray, val senderKey: org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey) : Kind() + } + + var kind: Kind? = null + + // constructors + internal constructor(kind: Kind?) : this() { + this.kind = kind + } + + override fun isValid(): Boolean { + if (!super.isValid() || kind == null) return false + val kind = kind ?: return false + when(kind) { + is Kind.New -> { + return !kind.groupPublicKey.isEmpty() && !kind.name.isEmpty() && !kind.groupPrivateKey.isEmpty() && !kind.members.isEmpty() && !kind.admins.isEmpty() + } + is Kind.Info -> { + return !kind.groupPublicKey.isEmpty() && !kind.name.isEmpty() && !kind.members.isEmpty() && !kind.admins.isEmpty() + } + is Kind.SenderKeyRequest -> { + return !kind.groupPublicKey.isEmpty() + } + is Kind.SenderKey -> { + return !kind.groupPublicKey.isEmpty() + } + } + } + + override fun toProto(): SignalServiceProtos.Content? { + val kind = kind + if (kind == null) { + Log.w(TAG, "Couldn't construct closed group update proto from: $this") + return null + } + try { + val closedGroupUpdate: SignalServiceProtos.ClosedGroupUpdate.Builder = SignalServiceProtos.ClosedGroupUpdate.newBuilder() + when (kind) { + is Kind.New -> { + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.NEW + closedGroupUpdate.name = kind.name + closedGroupUpdate.groupPrivateKey = ByteString.copyFrom(kind.groupPrivateKey) + closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() }) + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) }) + } + is Kind.Info -> { + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.INFO + closedGroupUpdate.name = kind.name + closedGroupUpdate.addAllSenderKeys(kind.senderKeys.map { it.toProto() }) + closedGroupUpdate.addAllMembers(kind.members.map { ByteString.copyFrom(it) }) + closedGroupUpdate.addAllAdmins(kind.admins.map { ByteString.copyFrom(it) }) + } + is Kind.SenderKeyRequest -> { + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY_REQUEST + } + is Kind.SenderKey -> { + closedGroupUpdate.groupPublicKey = ByteString.copyFrom(kind.groupPublicKey) + closedGroupUpdate.type = SignalServiceProtos.ClosedGroupUpdate.Type.SENDER_KEY + closedGroupUpdate.addAllSenderKeys(listOf( kind.senderKey.toProto() )) + } + } + val contentProto = SignalServiceProtos.Content.newBuilder() + val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() + dataMessageProto.closedGroupUpdate = closedGroupUpdate.build() + contentProto.dataMessage = dataMessageProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct closed group update proto from: $this") + return null + } + return null + } + +} + +// extension functions to class ClosedGroupSenderKey + +private fun ClosedGroupSenderKey.Companion.fromProto(proto: SignalServiceProtos.ClosedGroupUpdate.SenderKey): org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey { + return ClosedGroupSenderKey(chainKey = proto.chainKey.toByteArray(), keyIndex = proto.keyIndex, publicKey = proto.publicKey.toByteArray()) +} + +private fun ClosedGroupSenderKey.toProto(): SignalServiceProtos.ClosedGroupUpdate.SenderKey { + val proto = SignalServiceProtos.ClosedGroupUpdate.SenderKey.newBuilder() + proto.chainKey = ByteString.copyFrom(chainKey) + proto.keyIndex = keyIndex + proto.publicKey = ByteString.copyFrom(publicKey) + return proto.build() } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ControlMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ControlMessage.kt index 670553854c..44cd7ee4d8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ControlMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ControlMessage.kt @@ -1,6 +1,6 @@ -package org.session.messaging.messages.control +package org.session.libsession.messaging.messages.control -import org.session.messaging.messages.Message +import org.session.libsession.messaging.messages.Message -open class ControlMessage : Message() { +abstract class ControlMessage : Message() { } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt index 24d9140792..8ba8b802bd 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt @@ -1,4 +1,13 @@ -package org.session.messaging.messages.control +package org.session.libsession.messaging.messages.control + +import org.session.libsignal.service.internal.push.SignalServiceProtos class ExpirationTimerUpdate : ControlMessage() { + override fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + TODO("Not yet implemented") + } + + override fun toProto(): SignalServiceProtos.Content? { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt index 74f96aec81..2aa507065b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt @@ -1,4 +1,13 @@ -package org.session.messaging.messages.control +package org.session.libsession.messaging.messages.control + +import org.session.libsignal.service.internal.push.SignalServiceProtos class ReadReceipt : ControlMessage() { + override fun fromProto(proto: SignalServiceProtos.Content): ReadReceipt? { + TODO("Not yet implemented") + } + + override fun toProto(): SignalServiceProtos.Content? { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt index 9610e0b88d..a08d8f3ff0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt @@ -1,4 +1,13 @@ -package org.session.messaging.messages.control +package org.session.libsession.messaging.messages.control + +import org.session.libsignal.service.internal.push.SignalServiceProtos class TypingIndicator : ControlMessage() { + override fun fromProto(proto: SignalServiceProtos.Content): TypingIndicator?{ + TODO("Not yet implemented") + } + + override fun toProto(): SignalServiceProtos.Content? { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt index a791c32ca3..392d56caf5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt @@ -1,6 +1,14 @@ -package org.session.messaging.messages.control.unused +package org.session.libsession.messaging.messages.control.unused -import org.session.messaging.messages.control.ControlMessage +import org.session.libsession.messaging.messages.control.ControlMessage +import org.session.libsignal.service.internal.push.SignalServiceProtos class NullMessage : ControlMessage() { + override fun fromProto(proto: SignalServiceProtos.Content): NullMessage? { + TODO("Not yet implemented") + } + + override fun toProto(): SignalServiceProtos.Content? { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt index 51f99a7c16..8090d68f68 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt @@ -1,6 +1,14 @@ -package org.session.messaging.messages.control.unused +package org.session.libsession.messaging.messages.control.unused -import org.session.messaging.messages.control.ControlMessage +import org.session.libsession.messaging.messages.control.ControlMessage +import org.session.libsignal.service.internal.push.SignalServiceProtos class SessionRequest : ControlMessage() { + override fun fromProto(proto: SignalServiceProtos.Content): SessionRequest? { + TODO("Not yet implemented") + } + + override fun toProto(): SignalServiceProtos.Content? { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt index 7efb865119..cd67a4764e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt @@ -1,4 +1,13 @@ -package org.session.messaging.messages.visible +package org.session.libsession.messaging.messages.visible -internal class Contact { +import org.session.libsignal.service.internal.push.SignalServiceProtos + +internal class Contact : VisibleMessage() { + override fun fromProto(proto: SignalServiceProtos.Content): Contact? { + TODO("Not yet implemented") + } + + override fun toProto(): SignalServiceProtos.Content? { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt index a385545b51..355d8bd0c0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt @@ -1,4 +1,13 @@ -package org.session.messaging.messages.visible +package org.session.libsession.messaging.messages.visible -internal class LinkPreview { +import org.session.libsignal.service.internal.push.SignalServiceProtos + +internal class LinkPreview : VisibleMessage(){ + override fun fromProto(proto: SignalServiceProtos.Content): LinkPreview? { + TODO("Not yet implemented") + } + + override fun toProto(): SignalServiceProtos.Content? { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt index 22740911ea..0b125bb1f9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt @@ -1,4 +1,13 @@ -package org.session.messaging.messages.visible +package org.session.libsession.messaging.messages.visible -internal class Profile { +import org.session.libsignal.service.internal.push.SignalServiceProtos + +internal class Profile : VisibleMessage() { + override fun fromProto(proto: SignalServiceProtos.Content): Profile? { + TODO("Not yet implemented") + } + + override fun toProto(): SignalServiceProtos.Content? { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt index 90e2c287c5..7c0179e293 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt @@ -1,4 +1,13 @@ -package org.session.messaging.messages.visible +package org.session.libsession.messaging.messages.visible -internal class Quote { +import org.session.libsignal.service.internal.push.SignalServiceProtos + +internal class Quote : VisibleMessage() { + override fun fromProto(proto: SignalServiceProtos.Content): Quote? { + TODO("Not yet implemented") + } + + override fun toProto(): SignalServiceProtos.Content? { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index 0b332aaaca..5ea2cea79e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -1,6 +1,6 @@ -package org.session.messaging.messages.visible +package org.session.libsession.messaging.messages.visible -import org.session.messaging.messages.Message +import org.session.libsession.messaging.messages.Message -class VisibleMessage : Message() { +abstract class VisibleMessage : Message() { } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt index fa94a1808e..e0093d9831 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt @@ -1,4 +1,14 @@ -package org.session.messaging.messages.visible.attachments +package org.session.libsession.messaging.messages.visible.attachments -internal class Attachment { +import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsignal.service.internal.push.SignalServiceProtos + +internal class Attachment : VisibleMessage() { + override fun fromProto(proto: SignalServiceProtos.Content): Attachment? { + TODO("Not yet implemented") + } + + override fun toProto(): SignalServiceProtos.Content? { + TODO("Not yet implemented") + } } \ No newline at end of file From 8f409faefcfb9af2de2dc2c162ee6fed4ad1920f Mon Sep 17 00:00:00 2001 From: Brice Date: Fri, 27 Nov 2020 16:41:21 +1100 Subject: [PATCH 02/24] ExpirationTimerUpdate implementation + classes structure changes --- .../messages/control/ClosedGroupUpdate.kt | 4 +- .../messages/control/ExpirationTimerUpdate.kt | 46 +++++++++++++++++-- .../messaging/messages/control/ReadReceipt.kt | 10 ++-- .../messages/control/TypingIndicator.kt | 7 ++- .../messages/control/unused/NullMessage.kt | 11 +++-- .../messages/control/unused/SessionRequest.kt | 10 ++-- .../messaging/messages/visible/Contact.kt | 8 +++- .../messaging/messages/visible/LinkPreview.kt | 8 +++- .../messaging/messages/visible/Profile.kt | 8 +++- .../messaging/messages/visible/Quote.kt | 8 +++- .../visible/attachments/Attachment.kt | 8 +++- 11 files changed, 101 insertions(+), 27 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt index 18f424ba8e..9ff406e9e7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt @@ -53,7 +53,6 @@ class ClosedGroupUpdate() : ControlMessage() { return ClosedGroupUpdate(kind) } } - //private val TAG: String = ClosedGroupUpdate::class.java.simpleName // Kind enum sealed class Kind { @@ -65,11 +64,12 @@ class ClosedGroupUpdate() : ControlMessage() { var kind: Kind? = null - // constructors + // constructor internal constructor(kind: Kind?) : this() { this.kind = kind } + // validation override fun isValid(): Boolean { if (!super.isValid() || kind == null) return false val kind = kind ?: return false diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt index 8ba8b802bd..c392db9884 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt @@ -1,13 +1,51 @@ package org.session.libsession.messaging.messages.control +import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos -class ExpirationTimerUpdate : ControlMessage() { - override fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { - TODO("Not yet implemented") +class ExpirationTimerUpdate() : ControlMessage() { + + var duration: Int? = 0 + + companion object { + const val TAG = "ExpirationTimerUpdate" + + fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + val dataMessageProto = proto.dataMessage ?: return null + val isExpirationTimerUpdate = (dataMessageProto.flags and SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0 + if (!isExpirationTimerUpdate) return null + val duration = dataMessageProto.expireTimer + return ExpirationTimerUpdate(duration) + } + } + + //constructor + internal constructor(duration: Int) : this() { + this.duration = duration + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + return duration != null } override fun toProto(): SignalServiceProtos.Content? { - TODO("Not yet implemented") + val duration = duration + if (duration == null) { + Log.w(TAG, "Couldn't construct expiration timer update proto from: $this") + return null + } + val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() + dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE + dataMessageProto.expireTimer = duration + val contentProto = SignalServiceProtos.Content.newBuilder() + try { + contentProto.dataMessage = dataMessageProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct expiration timer update proto from: $this") + return null + } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt index 2aa507065b..152eac8729 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt @@ -2,9 +2,13 @@ package org.session.libsession.messaging.messages.control import org.session.libsignal.service.internal.push.SignalServiceProtos -class ReadReceipt : ControlMessage() { - override fun fromProto(proto: SignalServiceProtos.Content): ReadReceipt? { - TODO("Not yet implemented") +class ReadReceipt() : ControlMessage() { + + + companion object { + fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + TODO("Not yet implemented") + } } override fun toProto(): SignalServiceProtos.Content? { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt index a08d8f3ff0..da022a84c1 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt @@ -3,8 +3,11 @@ package org.session.libsession.messaging.messages.control import org.session.libsignal.service.internal.push.SignalServiceProtos class TypingIndicator : ControlMessage() { - override fun fromProto(proto: SignalServiceProtos.Content): TypingIndicator?{ - TODO("Not yet implemented") + + companion object { + fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + TODO("Not yet implemented") + } } override fun toProto(): SignalServiceProtos.Content? { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt index 392d56caf5..df0887ee8d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt @@ -1,11 +1,16 @@ package org.session.libsession.messaging.messages.control.unused import org.session.libsession.messaging.messages.control.ControlMessage +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsignal.service.internal.push.SignalServiceProtos -class NullMessage : ControlMessage() { - override fun fromProto(proto: SignalServiceProtos.Content): NullMessage? { - TODO("Not yet implemented") +class NullMessage() : ControlMessage() { + + + companion object { + fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + TODO("Not yet implemented") + } } override fun toProto(): SignalServiceProtos.Content? { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt index 8090d68f68..7285d5c4cb 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt @@ -1,11 +1,15 @@ package org.session.libsession.messaging.messages.control.unused import org.session.libsession.messaging.messages.control.ControlMessage +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsignal.service.internal.push.SignalServiceProtos -class SessionRequest : ControlMessage() { - override fun fromProto(proto: SignalServiceProtos.Content): SessionRequest? { - TODO("Not yet implemented") +class SessionRequest() : ControlMessage() { + + companion object { + fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + TODO("Not yet implemented") + } } override fun toProto(): SignalServiceProtos.Content? { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt index cd67a4764e..f5f818e86e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt @@ -1,10 +1,14 @@ package org.session.libsession.messaging.messages.visible +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsignal.service.internal.push.SignalServiceProtos internal class Contact : VisibleMessage() { - override fun fromProto(proto: SignalServiceProtos.Content): Contact? { - TODO("Not yet implemented") + + companion object { + fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + TODO("Not yet implemented") + } } override fun toProto(): SignalServiceProtos.Content? { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt index 355d8bd0c0..d758a46cca 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt @@ -1,10 +1,14 @@ package org.session.libsession.messaging.messages.visible +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsignal.service.internal.push.SignalServiceProtos internal class LinkPreview : VisibleMessage(){ - override fun fromProto(proto: SignalServiceProtos.Content): LinkPreview? { - TODO("Not yet implemented") + + companion object { + fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + TODO("Not yet implemented") + } } override fun toProto(): SignalServiceProtos.Content? { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt index 0b125bb1f9..5b172bf5e1 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt @@ -1,10 +1,14 @@ package org.session.libsession.messaging.messages.visible +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsignal.service.internal.push.SignalServiceProtos internal class Profile : VisibleMessage() { - override fun fromProto(proto: SignalServiceProtos.Content): Profile? { - TODO("Not yet implemented") + + companion object { + fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + TODO("Not yet implemented") + } } override fun toProto(): SignalServiceProtos.Content? { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt index 7c0179e293..aecf3dc3da 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt @@ -1,10 +1,14 @@ package org.session.libsession.messaging.messages.visible +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsignal.service.internal.push.SignalServiceProtos internal class Quote : VisibleMessage() { - override fun fromProto(proto: SignalServiceProtos.Content): Quote? { - TODO("Not yet implemented") + + companion object { + fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + TODO("Not yet implemented") + } } override fun toProto(): SignalServiceProtos.Content? { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt index e0093d9831..94b7ace5d4 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt @@ -1,11 +1,15 @@ package org.session.libsession.messaging.messages.visible.attachments +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsignal.service.internal.push.SignalServiceProtos internal class Attachment : VisibleMessage() { - override fun fromProto(proto: SignalServiceProtos.Content): Attachment? { - TODO("Not yet implemented") + + companion object { + fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + TODO("Not yet implemented") + } } override fun toProto(): SignalServiceProtos.Content? { From 746df2240a7bc5705e653b93ef48eff6865395cc Mon Sep 17 00:00:00 2001 From: Brice Date: Fri, 27 Nov 2020 17:27:09 +1100 Subject: [PATCH 03/24] ReadReceipt implementation + small corrections --- .../messaging/messages/control/ReadReceipt.kt | 42 +++++++++++++++++-- .../messages/control/TypingIndicator.kt | 2 +- .../messages/control/unused/NullMessage.kt | 2 +- .../messages/control/unused/SessionRequest.kt | 2 +- .../messaging/messages/visible/Contact.kt | 2 +- .../messaging/messages/visible/LinkPreview.kt | 2 +- .../messaging/messages/visible/Profile.kt | 2 +- .../messaging/messages/visible/Quote.kt | 2 +- .../visible/attachments/Attachment.kt | 2 +- 9 files changed, 47 insertions(+), 11 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt index 152eac8729..c735820b13 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt @@ -1,17 +1,53 @@ package org.session.libsession.messaging.messages.control +import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos class ReadReceipt() : ControlMessage() { + var timestamps: LongArray? = null companion object { - fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { - TODO("Not yet implemented") + const val TAG = "ReadReceipt" + + fun fromProto(proto: SignalServiceProtos.Content): ReadReceipt? { + val receiptProto = proto.receiptMessage ?: return null + if (receiptProto.type != SignalServiceProtos.ReceiptMessage.Type.READ) return null + val timestamps = receiptProto.timestampList + if (timestamps.isEmpty()) return null + return ReadReceipt(timestamps = timestamps.toLongArray()) } } + //constructor + internal constructor(timestamps: LongArray?) : this() { + this.timestamps = timestamps + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + val timestamps = timestamps ?: return false + if (timestamps.isNotEmpty()) { return true } + return false + } + override fun toProto(): SignalServiceProtos.Content? { - TODO("Not yet implemented") + val timestamps = timestamps ?: return null + if (timestamps == null) { + Log.w(ExpirationTimerUpdate.TAG, "Couldn't construct read receipt proto from: $this") + return null + } + val receiptProto = SignalServiceProtos.ReceiptMessage.newBuilder() + receiptProto.type = SignalServiceProtos.ReceiptMessage.Type.READ + receiptProto.addAllTimestamp(timestamps.asIterable()) + val contentProto = SignalServiceProtos.Content.newBuilder() + try { + contentProto.receiptMessage = receiptProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(ExpirationTimerUpdate.TAG, "Couldn't construct read receipt proto from: $this") + return null + } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt index da022a84c1..41e9498a60 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt @@ -5,7 +5,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos class TypingIndicator : ControlMessage() { companion object { - fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + fun fromProto(proto: SignalServiceProtos.Content): TypingIndicator? { TODO("Not yet implemented") } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt index df0887ee8d..a2bb642008 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt @@ -8,7 +8,7 @@ class NullMessage() : ControlMessage() { companion object { - fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + fun fromProto(proto: SignalServiceProtos.Content): NullMessage? { TODO("Not yet implemented") } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt index 7285d5c4cb..173fb141fc 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt @@ -7,7 +7,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos class SessionRequest() : ControlMessage() { companion object { - fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + fun fromProto(proto: SignalServiceProtos.Content): SessionRequest? { TODO("Not yet implemented") } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt index f5f818e86e..e0c9d2ab34 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt @@ -6,7 +6,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos internal class Contact : VisibleMessage() { companion object { - fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + fun fromProto(proto: SignalServiceProtos.Content): Contact? { TODO("Not yet implemented") } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt index d758a46cca..3bcafd71a7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt @@ -6,7 +6,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos internal class LinkPreview : VisibleMessage(){ companion object { - fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + fun fromProto(proto: SignalServiceProtos.Content): LinkPreview? { TODO("Not yet implemented") } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt index 5b172bf5e1..a5b09b4f79 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt @@ -6,7 +6,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos internal class Profile : VisibleMessage() { companion object { - fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + fun fromProto(proto: SignalServiceProtos.Content): Profile? { TODO("Not yet implemented") } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt index aecf3dc3da..55b6481c94 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt @@ -6,7 +6,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos internal class Quote : VisibleMessage() { companion object { - fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + fun fromProto(proto: SignalServiceProtos.Content): Quote? { TODO("Not yet implemented") } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt index 94b7ace5d4..b7ab2ec8e2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt @@ -7,7 +7,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos internal class Attachment : VisibleMessage() { companion object { - fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { + fun fromProto(proto: SignalServiceProtos.Content): Attachment? { TODO("Not yet implemented") } } From 3a0ba29a7202557c851c4ced4369583b193654e2 Mon Sep 17 00:00:00 2001 From: Brice Date: Mon, 30 Nov 2020 10:08:56 +1100 Subject: [PATCH 04/24] TypingIndicator implementation --- .../messages/control/ClosedGroupUpdate.kt | 20 +++--- .../messaging/messages/control/ReadReceipt.kt | 2 +- .../messages/control/TypingIndicator.kt | 63 ++++++++++++++++++- 3 files changed, 71 insertions(+), 14 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt index 9ff406e9e7..9b0ba71cf6 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt @@ -7,6 +7,16 @@ import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSende class ClosedGroupUpdate() : ControlMessage() { + // Kind enum + sealed class Kind { + class New(val groupPublicKey: ByteArray, val name: String, val groupPrivateKey: ByteArray, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class Info(val groupPublicKey: ByteArray, val name: String, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() + class SenderKeyRequest(val groupPublicKey: ByteArray) : Kind() + class SenderKey(val groupPublicKey: ByteArray, val senderKey: org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey) : Kind() + } + + var kind: Kind? = null + companion object { const val TAG = "ClosedGroupUpdate" @@ -54,16 +64,6 @@ class ClosedGroupUpdate() : ControlMessage() { } } - // Kind enum - sealed class Kind { - class New(val groupPublicKey: ByteArray, val name: String, val groupPrivateKey: ByteArray, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() - class Info(val groupPublicKey: ByteArray, val name: String, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() - class SenderKeyRequest(val groupPublicKey: ByteArray) : Kind() - class SenderKey(val groupPublicKey: ByteArray, val senderKey: org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey) : Kind() - } - - var kind: Kind? = null - // constructor internal constructor(kind: Kind?) : this() { this.kind = kind diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt index c735820b13..960b5aeaf9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ReadReceipt.kt @@ -33,7 +33,7 @@ class ReadReceipt() : ControlMessage() { } override fun toProto(): SignalServiceProtos.Content? { - val timestamps = timestamps ?: return null + val timestamps = timestamps if (timestamps == null) { Log.w(ExpirationTimerUpdate.TAG, "Couldn't construct read receipt proto from: $this") return null diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt index 41e9498a60..005b11d155 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt @@ -1,16 +1,73 @@ package org.session.libsession.messaging.messages.control +import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos -class TypingIndicator : ControlMessage() { +class TypingIndicator() : ControlMessage() { companion object { + const val TAG = "TypingIndicator" + fun fromProto(proto: SignalServiceProtos.Content): TypingIndicator? { - TODO("Not yet implemented") + val typingIndicatorProto = proto.typingMessage ?: return null + val kind = Kind.fromProto(typingIndicatorProto.action) + return TypingIndicator(kind = kind) } } + // Kind enum + enum class Kind { + STARTED, + STOPPED, + ; + + companion object { + @JvmStatic + fun fromProto(proto: SignalServiceProtos.TypingMessage.Action): Kind = + when (proto) { + SignalServiceProtos.TypingMessage.Action.STARTED -> STARTED + SignalServiceProtos.TypingMessage.Action.STOPPED -> STOPPED + } + } + + fun toProto(): SignalServiceProtos.TypingMessage.Action { + when (this) { + STARTED -> return SignalServiceProtos.TypingMessage.Action.STARTED + STOPPED -> return SignalServiceProtos.TypingMessage.Action.STOPPED + } + } + } + + var kind: Kind? = null + + //constructor + internal constructor(kind: Kind) : this() { + this.kind = kind + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + return kind != null + } + override fun toProto(): SignalServiceProtos.Content? { - TODO("Not yet implemented") + val timestamp = sentTimestamp + val kind = kind + if (timestamp == null || kind == null) { + Log.w(TAG, "Couldn't construct typing indicator proto from: $this") + return null + } + val typingIndicatorProto = SignalServiceProtos.TypingMessage.newBuilder() + typingIndicatorProto.timestamp = timestamp + typingIndicatorProto.action = kind.toProto() + val contentProto = SignalServiceProtos.Content.newBuilder() + try { + contentProto.typingMessage = typingIndicatorProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct typing indicator proto from: $this") + return null + } } } \ No newline at end of file From 2c167b0cc0046812a2244f9ee726a6e808f937f4 Mon Sep 17 00:00:00 2001 From: Brice Date: Mon, 30 Nov 2020 10:29:06 +1100 Subject: [PATCH 05/24] NullMessage implementation --- .../messages/control/unused/NullMessage.kt | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt index a2bb642008..b46bbc0231 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt @@ -1,19 +1,37 @@ package org.session.libsession.messaging.messages.control.unused +import com.google.protobuf.ByteString import org.session.libsession.messaging.messages.control.ControlMessage -import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos +import java.security.SecureRandom class NullMessage() : ControlMessage() { companion object { + const val TAG = "NullMessage" + fun fromProto(proto: SignalServiceProtos.Content): NullMessage? { - TODO("Not yet implemented") + if (proto.nullMessage == null) return null + return NullMessage() } } override fun toProto(): SignalServiceProtos.Content? { - TODO("Not yet implemented") + val nullMessageProto = SignalServiceProtos.NullMessage.newBuilder() + val sr = SecureRandom() + val paddingSize = sr.nextInt(512) + val padding = ByteArray(paddingSize) + nullMessageProto.padding = ByteString.copyFrom(padding) + val contentProto = SignalServiceProtos.Content.newBuilder() + try { + contentProto.nullMessage = nullMessageProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct null message proto from: $this") + return null + } } } \ No newline at end of file From 3f0e456002753000c7011ee82122014554bd9b1e Mon Sep 17 00:00:00 2001 From: Brice Date: Mon, 30 Nov 2020 11:23:27 +1100 Subject: [PATCH 06/24] SessionRequest unfinished implementation --- .../libsession/messaging/messages/Message.kt | 6 +- .../messages/control/TypingIndicator.kt | 2 + .../messages/control/unused/SessionRequest.kt | 67 ++++++++++++++++++- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt index 6176cee7c8..3f2c5b0fee 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt @@ -15,9 +15,9 @@ abstract class Message { companion object { @JvmStatic - val ttl = 2 * 24 * 60 * 60 * 1000 + val ttl = 2 * 24 * 60 * 60 * 1000 //TODO not sure about that declaration - //fun fromProto(proto: SignalServiceProtos.Content): Message? {} + //TODO how to declare fromProto? } open fun isValid(): Boolean { @@ -26,8 +26,6 @@ abstract class Message { return sender != null && recipient != null } - - abstract fun toProto(): SignalServiceProtos.Content? } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt index 005b11d155..ec9ae23358 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/TypingIndicator.kt @@ -8,6 +8,8 @@ class TypingIndicator() : ControlMessage() { companion object { const val TAG = "TypingIndicator" + //val ttl: 30 * 1000 //TODO + fun fromProto(proto: SignalServiceProtos.Content): TypingIndicator? { val typingIndicatorProto = proto.typingMessage ?: return null val kind = Kind.fromProto(typingIndicatorProto.action) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt index 173fb141fc..8a25b5dccc 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt @@ -1,18 +1,81 @@ package org.session.libsession.messaging.messages.control.unused +import com.google.protobuf.ByteString import org.session.libsession.messaging.messages.control.ControlMessage import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.libsignal.state.PreKeyBundle import org.session.libsignal.service.internal.push.SignalServiceProtos +import java.security.SecureRandom class SessionRequest() : ControlMessage() { + var preKeyBundle: PreKeyBundle? = null + companion object { + const val TAG = "SessionRequest" + fun fromProto(proto: SignalServiceProtos.Content): SessionRequest? { - TODO("Not yet implemented") + if (proto.nullMessage == null) return null + val preKeyBundleProto = proto.preKeyBundleMessage ?: return null + val registrationID: Int = 0 + //TODO looks like database stuff here + /*iOS code: Configuration.shared.storage.with { transaction in + registrationID = Configuration.shared.storage.getOrGenerateRegistrationID(using: transaction) + }*/ + val preKeyBundle = PreKeyBundle( + registrationID, + 1, + preKeyBundleProto.preKeyId, + null, //TODO preKeyBundleProto.preKey, + 0, //TODO preKeyBundleProto.signedKey, + null, //TODO preKeyBundleProto.signedKeyId, + preKeyBundleProto.signature.toByteArray(), + null, //TODO preKeyBundleProto.identityKey + ) ?: return null + return SessionRequest(preKeyBundle) } } + //constructor + internal constructor(preKeyBundle: PreKeyBundle) : this() { + this.preKeyBundle = preKeyBundle + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + return preKeyBundle != null + } + override fun toProto(): SignalServiceProtos.Content? { - TODO("Not yet implemented") + val preKeyBundle = preKeyBundle + if (preKeyBundle == null) { + Log.w(TAG, "Couldn't construct session request proto from: $this") + return null + } + val nullMessageProto = SignalServiceProtos.NullMessage.newBuilder() + val sr = SecureRandom() + val paddingSize = sr.nextInt(512) + val padding = ByteArray(paddingSize) + nullMessageProto.padding = ByteString.copyFrom(padding) + val preKeyBundleProto = SignalServiceProtos.PreKeyBundleMessage.newBuilder() + //TODO preKeyBundleProto.identityKey = preKeyBundle.identityKey + preKeyBundleProto.deviceId = preKeyBundle.deviceId + preKeyBundleProto.preKeyId = preKeyBundle.preKeyId + //TODO preKeyBundleProto.preKey = preKeyBundle.preKeyPublic + preKeyBundleProto.signedKeyId = preKeyBundle.signedPreKeyId + //TODO preKeyBundleProto.signedKey = preKeyBundle.signedPreKeyPublic + preKeyBundleProto.signature = ByteString.copyFrom(preKeyBundle.signedPreKeySignature) + val contentProto = SignalServiceProtos.Content.newBuilder() + try { + contentProto.nullMessage = nullMessageProto.build() + contentProto.preKeyBundleMessage = preKeyBundleProto.build() + return contentProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct session request proto from: $this") + return null + } } } \ No newline at end of file From f5a583e7c86869da97212113b46f1d36108d2298 Mon Sep 17 00:00:00 2001 From: Brice Date: Tue, 1 Dec 2020 16:24:50 +1100 Subject: [PATCH 07/24] classes structure redesign + LinkPreview & BaseVisibleMessage implementations --- .../messages/visible/BaseVisibleMessage.kt | 98 +++++++++++++++++++ .../messaging/messages/visible/Contact.kt | 5 +- .../messaging/messages/visible/LinkPreview.kt | 53 ++++++++-- .../messaging/messages/visible/Profile.kt | 9 +- .../messaging/messages/visible/Quote.kt | 14 ++- .../messages/visible/VisibleMessage.kt | 11 ++- .../visible/attachments/Attachment.kt | 5 +- 7 files changed, 172 insertions(+), 23 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt new file mode 100644 index 0000000000..1ea7768381 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt @@ -0,0 +1,98 @@ +package org.session.libsession.messaging.messages.visible + +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos + +class BaseVisibleMessage() : VisibleMessage() { + + var text: String? = null + var attachmentIDs = ArrayList() + var quote: Quote? = null + var linkPreview: LinkPreview? = null + var contact: Contact? = null + var profile: Profile? = null + + companion object { + const val TAG = "BaseVisibleMessage" + + fun fromProto(proto: SignalServiceProtos.Content): BaseVisibleMessage? { + val dataMessage = proto.dataMessage ?: return null + val result = BaseVisibleMessage() + result.text = dataMessage.body + // Attachments are handled in MessageReceiver + val quoteProto = dataMessage.quote + val quote = Quote.fromProto(quoteProto) + quote?.let { result.quote = quote } + val linkPreviewProto = dataMessage.previewList.first() + val linkPreview = LinkPreview.fromProto(linkPreviewProto) + linkPreview?.let { result.linkPreview = linkPreview } + // TODO Contact + val profile = Profile.fromProto(dataMessage) + if (profile != null) { result.profile = profile } + return result + } + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + if (attachmentIDs.isNotEmpty()) return true + val text = text?.trim() ?: return false + if (text.isEmpty()) return true + return false + } + + override fun toProto(transaction: String): SignalServiceProtos.Content? { + val proto = SignalServiceProtos.Content.newBuilder() + var attachmentIDs = this.attachmentIDs + val dataMessage: SignalServiceProtos.DataMessage.Builder + // Profile + val profile = profile + val profileProto = profile?.toProto("") //TODO + if (profileProto != null) { + dataMessage = profileProto.toBuilder() + } else { + dataMessage = SignalServiceProtos.DataMessage.newBuilder() + } + // Text + text?.let { dataMessage.body = text } + // Quote + val quotedAttachmentID = quote?.attachmentID + quotedAttachmentID?.let { + val index = attachmentIDs.indexOf(quotedAttachmentID) + if (index >= 0) { attachmentIDs.removeAt(index) } + } + val quote = quote + quote?.let { + val quoteProto = quote.toProto(transaction) + if (quoteProto != null) dataMessage.quote = quoteProto + } + //Link preview + val linkPreviewAttachmentID = linkPreview?.attachmentID + linkPreviewAttachmentID?.let { + val index = attachmentIDs.indexOf(quotedAttachmentID) + if (index >= 0) { attachmentIDs.removeAt(index) } + } + val linkPreview = linkPreview + linkPreview?.let { + val linkPreviewProto = linkPreview.toProto(transaction) + linkPreviewProto?.let { + dataMessage.addAllPreview(listOf(linkPreviewProto)) + } + } + //Attachments + // TODO I'm blocking on that one... + //swift: let attachments = attachmentIDs.compactMap { TSAttachmentStream.fetch(uniqueId: $0, transaction: transaction) } + + + // Build + try { + proto.dataMessage = dataMessage.build() + return proto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct visible message proto from: $this") + return null + } + } + +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt index e0c9d2ab34..cbca11cd2d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt @@ -1,9 +1,8 @@ package org.session.libsession.messaging.messages.visible -import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsignal.service.internal.push.SignalServiceProtos -internal class Contact : VisibleMessage() { +class Contact : VisibleMessage() { companion object { fun fromProto(proto: SignalServiceProtos.Content): Contact? { @@ -11,7 +10,7 @@ internal class Contact : VisibleMessage() { } } - override fun toProto(): SignalServiceProtos.Content? { + override fun toProto(transaction: String): SignalServiceProtos.DataMessage.Contact? { TODO("Not yet implemented") } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt index 3bcafd71a7..90521b6e30 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt @@ -1,17 +1,58 @@ package org.session.libsession.messaging.messages.visible -import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos -internal class LinkPreview : VisibleMessage(){ +class LinkPreview() : VisibleMessage(){ + + var title: String? = null + var url: String? = null + var attachmentID: String? = null companion object { - fun fromProto(proto: SignalServiceProtos.Content): LinkPreview? { - TODO("Not yet implemented") + const val TAG = "LinkPreview" + + fun fromProto(proto: SignalServiceProtos.DataMessage.Preview): LinkPreview? { + val title = proto.title + val url = proto.url + return LinkPreview(title, url, null) } } - override fun toProto(): SignalServiceProtos.Content? { - TODO("Not yet implemented") + //constructor + internal constructor(title: String?, url: String, attachmentID: String?) : this() { + this.title = title + this.url = url + this.attachmentID = attachmentID + } + + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + return (title != null && url != null && attachmentID != null) + } + + override fun toProto(transaction: String): SignalServiceProtos.DataMessage.Preview? { + val url = url + if (url == null) { + Log.w(TAG, "Couldn't construct link preview proto from: $this") + return null + } + val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder() + linkPreviewProto.url = url + title?. let { linkPreviewProto.title = title } + val attachmentID = attachmentID + attachmentID?.let { + //TODO database stuff + } + // Build + try { + return linkPreviewProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct link preview proto from: $this") + return null + } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt index a5b09b4f79..193966e003 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt @@ -1,17 +1,16 @@ package org.session.libsession.messaging.messages.visible -import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsignal.service.internal.push.SignalServiceProtos -internal class Profile : VisibleMessage() { +class Profile() : VisibleMessage() { companion object { - fun fromProto(proto: SignalServiceProtos.Content): Profile? { + fun fromProto(proto: SignalServiceProtos.DataMessage): Profile? { TODO("Not yet implemented") } } - override fun toProto(): SignalServiceProtos.Content? { - TODO("Not yet implemented") + override fun toProto(transaction: String): SignalServiceProtos.DataMessage? { + return null } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt index 55b6481c94..0f60db1865 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt @@ -1,17 +1,21 @@ package org.session.libsession.messaging.messages.visible -import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsignal.service.internal.push.SignalServiceProtos -internal class Quote : VisibleMessage() { +class Quote() : VisibleMessage() { + + var timestamp: Long? = 0 + var publicKey: String? = null + var text: String? = null + var attachmentID: String? = null companion object { - fun fromProto(proto: SignalServiceProtos.Content): Quote? { + fun fromProto(proto: SignalServiceProtos.DataMessage.Quote): Quote? { TODO("Not yet implemented") } } - override fun toProto(): SignalServiceProtos.Content? { - TODO("Not yet implemented") + override fun toProto(transaction: String): SignalServiceProtos.DataMessage.Quote? { + return null } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index 5ea2cea79e..bb60a51817 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -1,6 +1,15 @@ package org.session.libsession.messaging.messages.visible import org.session.libsession.messaging.messages.Message +import org.session.libsignal.service.internal.push.SignalServiceProtos -abstract class VisibleMessage : Message() { +abstract class VisibleMessage : Message() { + + abstract fun toProto(transaction: String): T + + final override fun toProto(): SignalServiceProtos.Content? { + //we don't need to implement this method in subclasses + //TODO it just needs an equivalent to swift: preconditionFailure("Use toProto(using:) instead.") + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt index b7ab2ec8e2..f50ffb6baa 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt @@ -1,10 +1,9 @@ package org.session.libsession.messaging.messages.visible.attachments -import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate -import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.messages.visible.BaseVisibleMessage import org.session.libsignal.service.internal.push.SignalServiceProtos -internal class Attachment : VisibleMessage() { +internal class Attachment : BaseVisibleMessage() { companion object { fun fromProto(proto: SignalServiceProtos.Content): Attachment? { From feec22bf722aeb83bdf6603c34230939e2406554 Mon Sep 17 00:00:00 2001 From: Brice Date: Tue, 1 Dec 2020 17:35:47 +1100 Subject: [PATCH 08/24] Profile implementation --- .../messaging/messages/visible/Profile.kt | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt index 193966e003..417599962c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt @@ -1,16 +1,65 @@ package org.session.libsession.messaging.messages.visible +import com.google.protobuf.ByteString +import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos class Profile() : VisibleMessage() { + var displayName: String? = null + var profileKey: ByteArray? = null + var profilePictureURL: String? = null + companion object { + const val TAG = "Profile" + fun fromProto(proto: SignalServiceProtos.DataMessage): Profile? { - TODO("Not yet implemented") + val profileProto = proto.profile ?: return null + val displayName = profileProto.displayName ?: return null + val profileKey = proto.profileKey + val profilePictureURL = profileProto.profilePictureURL + profileKey?.let { + val profilePictureURL = profilePictureURL + profilePictureURL?.let { + return Profile(displayName = displayName, profileKey = profileKey.toByteArray(), profilePictureURL = profilePictureURL) + } + return Profile(displayName) + } + } } + //constructor + internal constructor(displayName: String, profileKey: ByteArray? = nil, profilePictureURL: String? = nil) : this() { + this.displayName = displayName + this.profileKey = profileKey + this.profilePictureURL = profilePictureURL + } + + fun toProto(): SignalServiceProtos.DataMessage? { + return this.toProto("") + } + override fun toProto(transaction: String): SignalServiceProtos.DataMessage? { - return null + val displayName = displayName + if (displayName == null) { + Log.w(TAG, "Couldn't construct link preview proto from: $this") + return null + } + val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder() + val profileProto = SignalServiceProtos.LokiUserProfile.newBuilder() + profileProto.displayName = displayName + val profileKey = profileKey + profileKey?.let { dataMessageProto.profileKey = ByteString.copyFrom(profileKey) } + val profilePictureURL = profilePictureURL + profilePictureURL?.let { profileProto.profilePictureURL = profilePictureURL } + // Build + try { + dataMessageProto.profile = profileProto.build() + return dataMessageProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct profile proto from: $this") + return null + } } } \ No newline at end of file From 344af77f0f6408238a2f70f3b9b9ee02390355d8 Mon Sep 17 00:00:00 2001 From: Brice Date: Wed, 2 Dec 2020 11:44:55 +1100 Subject: [PATCH 09/24] incomplete Quote implementation --- .../messaging/messages/visible/Profile.kt | 7 +-- .../messaging/messages/visible/Quote.kt | 55 ++++++++++++++++++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt index 417599962c..5a2590b56d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt @@ -23,20 +23,19 @@ class Profile() : VisibleMessage() { profilePictureURL?.let { return Profile(displayName = displayName, profileKey = profileKey.toByteArray(), profilePictureURL = profilePictureURL) } - return Profile(displayName) } - + return Profile(displayName) } } //constructor - internal constructor(displayName: String, profileKey: ByteArray? = nil, profilePictureURL: String? = nil) : this() { + internal constructor(displayName: String, profileKey: ByteArray? = null, profilePictureURL: String? = null) : this() { this.displayName = displayName this.profileKey = profileKey this.profilePictureURL = profilePictureURL } - fun toProto(): SignalServiceProtos.DataMessage? { + fun toSSProto(): SignalServiceProtos.DataMessage? { return this.toProto("") } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt index 0f60db1865..47a9e99626 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt @@ -1,5 +1,6 @@ package org.session.libsession.messaging.messages.visible +import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos class Quote() : VisibleMessage() { @@ -10,12 +11,62 @@ class Quote() : VisibleMessage() { var attachmentID: String? = null companion object { + const val TAG = "Quote" + fun fromProto(proto: SignalServiceProtos.DataMessage.Quote): Quote? { - TODO("Not yet implemented") + val timestamp = proto.id + val publicKey = proto.author + val text = proto.text + return Quote(timestamp, publicKey, text, null) } } + //constructor + internal constructor(timestamp: Long, publicKey: String, text: String?, attachmentID: String?) : this() { + this.timestamp = timestamp + this.publicKey = publicKey + this.text = text + this.attachmentID = attachmentID + } + + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + return (timestamp != null && publicKey != null) + } + override fun toProto(transaction: String): SignalServiceProtos.DataMessage.Quote? { - return null + val timestamp = timestamp + val publicKey = publicKey + if (timestamp == null || publicKey == null) { + Log.w(TAG, "Couldn't construct quote proto from: $this") + return null + } + val quoteProto = SignalServiceProtos.DataMessage.Quote.newBuilder() + quoteProto.id = timestamp + quoteProto.author = publicKey + text?.let { quoteProto.text = text } + //TODO addAttachmentsIfNeeded(quoteProto, transaction) + // Build + try { + return quoteProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct quote proto from: $this") + return null + } + } + + private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder, transaction: String) { + val attachmentID = attachmentID ?: return + //TODO databas stuff + val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder() + //TODO more database related stuff + //quotedAttachmentProto.contentType = + try { + quoteProto.addAttachments(quotedAttachmentProto.build()) + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct quoted attachment proto from: $this") + } } } \ No newline at end of file From aefe721fa41b078c1a04873201b93b04ef288426 Mon Sep 17 00:00:00 2001 From: Brice Date: Wed, 2 Dec 2020 15:02:46 +1100 Subject: [PATCH 10/24] Attachment implementation --- .../messages/control/ClosedGroupUpdate.kt | 2 +- .../messaging/messages/visible/Attachment.kt | 71 +++++++++++++++++++ .../visible/attachments/Attachment.kt | 17 ----- 3 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt delete mode 100644 libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt index 9b0ba71cf6..f72c8a1dce 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt @@ -141,7 +141,7 @@ class ClosedGroupUpdate() : ControlMessage() { // extension functions to class ClosedGroupSenderKey -private fun ClosedGroupSenderKey.Companion.fromProto(proto: SignalServiceProtos.ClosedGroupUpdate.SenderKey): org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey { +private fun ClosedGroupSenderKey.Companion.fromProto(proto: SignalServiceProtos.ClosedGroupUpdate.SenderKey): ClosedGroupSenderKey { return ClosedGroupSenderKey(chainKey = proto.chainKey.toByteArray(), keyIndex = proto.keyIndex, publicKey = proto.publicKey.toByteArray()) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt new file mode 100644 index 0000000000..3e1530f34e --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt @@ -0,0 +1,71 @@ +package org.session.libsession.messaging.messages.visible + +import android.util.Size +import android.webkit.MimeTypeMap +import org.session.libsignal.service.internal.push.SignalServiceProtos +import java.io.File +import java.net.URL +import kotlin.math.absoluteValue + +class Attachment : VisibleMessage() { + + var fileName: String? = null + var contentType: String? = null + var key: ByteArray? = null + var digest: ByteArray? = null + var kind: Kind? = null + var caption: String? = null + var size: Size? = null + var sizeInBytes: Int? = 0 + var url: String? = null + + companion object { + fun fromProto(proto: SignalServiceProtos.AttachmentPointer): Attachment? { + val result = Attachment() + result.fileName = proto.fileName + fun inferContentType(): String { + val fileName = result.fileName ?: return "application/octet-stream" //TODO find equivalent to OWSMimeTypeApplicationOctetStream + val fileExtension = File(fileName).extension + val mimeTypeMap = MimeTypeMap.getSingleton() + return mimeTypeMap.getMimeTypeFromExtension(fileExtension) ?: "application/octet-stream" //TODO check that it's correct + } + result.contentType = proto.contentType ?: inferContentType() + result.key = proto.key.toByteArray() + result.digest = proto.digest.toByteArray() + val kind: Kind + if (proto.hasFlags() && (proto.flags and SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) > 0) { + kind = Kind.VOICEMESSAGE + } else { + kind = Kind.GENERIC + } + result.kind = kind + result.caption = if (proto.hasCaption()) proto.caption else null + val size: Size + if (proto.hasWidth() && proto.width > 0 && proto.hasHeight() && proto.height > 0) { + size = Size(proto.width, proto.height) + } else { + size = Size(0,0) //TODO check that it's equivalent to swift: CGSize.zero + } + result.size = size + result.sizeInBytes = if (proto.size > 0) proto.size else null + result. url = proto.url + return result + } + } + + enum class Kind { + VOICEMESSAGE, + GENERIC + } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + // key and digest can be nil for open group attachments + return (contentType != null && kind != null && size != null && sizeInBytes != null && url != null) + } + + override fun toProto(transaction: String): SignalServiceProtos.AttachmentPointer? { + TODO("Not implemented") + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt deleted file mode 100644 index f50ffb6baa..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/attachments/Attachment.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.session.libsession.messaging.messages.visible.attachments - -import org.session.libsession.messaging.messages.visible.BaseVisibleMessage -import org.session.libsignal.service.internal.push.SignalServiceProtos - -internal class Attachment : BaseVisibleMessage() { - - companion object { - fun fromProto(proto: SignalServiceProtos.Content): Attachment? { - TODO("Not yet implemented") - } - } - - override fun toProto(): SignalServiceProtos.Content? { - TODO("Not yet implemented") - } -} \ No newline at end of file From a69916895692d440b50871443452e7e5aa8db59d Mon Sep 17 00:00:00 2001 From: Brice Date: Wed, 2 Dec 2020 16:21:38 +1100 Subject: [PATCH 11/24] code review, minor changes --- .../messaging/messages/Destination.kt | 3 +++ .../libsession/messaging/messages/Message.kt | 3 +-- .../messages/control/ClosedGroupUpdate.kt | 8 ++++---- .../messages/control/ExpirationTimerUpdate.kt | 2 +- .../messages/control/unused/SessionRequest.kt | 2 +- .../messaging/messages/visible/Attachment.kt | 2 +- .../messages/visible/BaseVisibleMessage.kt | 20 +++++++++++-------- .../messaging/messages/visible/LinkPreview.kt | 2 +- .../messaging/messages/visible/Quote.kt | 2 +- .../messages/visible/VisibleMessage.kt | 4 ++-- 10 files changed, 27 insertions(+), 21 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt index 8c255bdce5..a0573bd19c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt @@ -6,4 +6,7 @@ sealed class Destination { class ClosedGroup(val groupPublicKey: String) class OpenGroup(val channel: Long, val server: String) + companion object { + //TODO need to implement the equivalent to TSThread and then implement from(...) + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt index 3f2c5b0fee..3e92077689 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt @@ -16,10 +16,9 @@ abstract class Message { companion object { @JvmStatic val ttl = 2 * 24 * 60 * 60 * 1000 //TODO not sure about that declaration - - //TODO how to declare fromProto? } + // validation open fun isValid(): Boolean { sentTimestamp = if (sentTimestamp!! > 0) sentTimestamp else return false receivedTimestamp = if (receivedTimestamp!! > 0) receivedTimestamp else return false diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt index f72c8a1dce..75486b6557 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ClosedGroupUpdate.kt @@ -7,6 +7,8 @@ import org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSende class ClosedGroupUpdate() : ControlMessage() { + var kind: Kind? = null + // Kind enum sealed class Kind { class New(val groupPublicKey: ByteArray, val name: String, val groupPrivateKey: ByteArray, val senderKeys: Collection, val members: Collection, val admins: Collection) : Kind() @@ -15,15 +17,13 @@ class ClosedGroupUpdate() : ControlMessage() { class SenderKey(val groupPublicKey: ByteArray, val senderKey: org.session.libsignal.service.loki.protocol.closedgroups.ClosedGroupSenderKey) : Kind() } - var kind: Kind? = null - companion object { const val TAG = "ClosedGroupUpdate" fun fromProto(proto: SignalServiceProtos.Content): ClosedGroupUpdate? { val closedGroupUpdateProto = proto.dataMessage?.closedGroupUpdate ?: return null val groupPublicKey = closedGroupUpdateProto.groupPublicKey - var kind: Kind? = null + var kind: Kind when(closedGroupUpdateProto.type) { SignalServiceProtos.ClosedGroupUpdate.Type.NEW -> { val name = closedGroupUpdateProto.name ?: return null @@ -71,7 +71,7 @@ class ClosedGroupUpdate() : ControlMessage() { // validation override fun isValid(): Boolean { - if (!super.isValid() || kind == null) return false + if (!super.isValid()) return false val kind = kind ?: return false when(kind) { is Kind.New -> { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt index c392db9884..9f2a38c86c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt @@ -12,7 +12,7 @@ class ExpirationTimerUpdate() : ControlMessage() { fun fromProto(proto: SignalServiceProtos.Content): ExpirationTimerUpdate? { val dataMessageProto = proto.dataMessage ?: return null - val isExpirationTimerUpdate = (dataMessageProto.flags and SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0 + val isExpirationTimerUpdate = (dataMessageProto.flags and SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0 //TODO validate that 'and' operator equivalent to Swift '&' if (!isExpirationTimerUpdate) return null val duration = dataMessageProto.expireTimer return ExpirationTimerUpdate(duration) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt index 8a25b5dccc..b078ce5545 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt @@ -33,7 +33,7 @@ class SessionRequest() : ControlMessage() { null, //TODO preKeyBundleProto.signedKeyId, preKeyBundleProto.signature.toByteArray(), null, //TODO preKeyBundleProto.identityKey - ) ?: return null + ) return SessionRequest(preKeyBundle) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt index 3e1530f34e..7a37ef4849 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt @@ -33,7 +33,7 @@ class Attachment : VisibleMessage() { result.key = proto.key.toByteArray() result.digest = proto.digest.toByteArray() val kind: Kind - if (proto.hasFlags() && (proto.flags and SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) > 0) { + if (proto.hasFlags() && (proto.flags and SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) > 0) { //TODO validate that 'and' operator = swift '&' kind = Kind.VOICEMESSAGE } else { kind = Kind.GENERIC diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt index 1ea7768381..5e3fab2d1c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt @@ -21,14 +21,18 @@ class BaseVisibleMessage() : VisibleMessage() { result.text = dataMessage.body // Attachments are handled in MessageReceiver val quoteProto = dataMessage.quote - val quote = Quote.fromProto(quoteProto) - quote?.let { result.quote = quote } + quoteProto?.let { + val quote = Quote.fromProto(quoteProto) + quote?.let { result.quote = quote } + } val linkPreviewProto = dataMessage.previewList.first() - val linkPreview = LinkPreview.fromProto(linkPreviewProto) - linkPreview?.let { result.linkPreview = linkPreview } + linkPreviewProto?.let { + val linkPreview = LinkPreview.fromProto(linkPreviewProto) + linkPreview?.let { result.linkPreview = linkPreview } + } // TODO Contact val profile = Profile.fromProto(dataMessage) - if (profile != null) { result.profile = profile } + profile?.let { result.profile = profile } return result } } @@ -38,7 +42,7 @@ class BaseVisibleMessage() : VisibleMessage() { if (!super.isValid()) return false if (attachmentIDs.isNotEmpty()) return true val text = text?.trim() ?: return false - if (text.isEmpty()) return true + if (text.isNotEmpty()) return true return false } @@ -48,7 +52,7 @@ class BaseVisibleMessage() : VisibleMessage() { val dataMessage: SignalServiceProtos.DataMessage.Builder // Profile val profile = profile - val profileProto = profile?.toProto("") //TODO + val profileProto = profile?.toSSProto() if (profileProto != null) { dataMessage = profileProto.toBuilder() } else { @@ -84,7 +88,7 @@ class BaseVisibleMessage() : VisibleMessage() { // TODO I'm blocking on that one... //swift: let attachments = attachmentIDs.compactMap { TSAttachmentStream.fetch(uniqueId: $0, transaction: transaction) } - + // TODO Contact // Build try { proto.dataMessage = dataMessage.build() diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt index 90521b6e30..7806e9fbe0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt @@ -42,7 +42,7 @@ class LinkPreview() : VisibleMessage() } val linkPreviewProto = SignalServiceProtos.DataMessage.Preview.newBuilder() linkPreviewProto.url = url - title?. let { linkPreviewProto.title = title } + title?.let { linkPreviewProto.title = title } val attachmentID = attachmentID attachmentID?.let { //TODO database stuff diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt index 47a9e99626..0d07821e40 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt @@ -47,7 +47,7 @@ class Quote() : VisibleMessage() { quoteProto.id = timestamp quoteProto.author = publicKey text?.let { quoteProto.text = text } - //TODO addAttachmentsIfNeeded(quoteProto, transaction) + addAttachmentsIfNeeded(quoteProto, transaction) // Build try { return quoteProto.build() diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index bb60a51817..dd1068cc45 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -9,7 +9,7 @@ abstract class VisibleMessage : Me final override fun toProto(): SignalServiceProtos.Content? { //we don't need to implement this method in subclasses - //TODO it just needs an equivalent to swift: preconditionFailure("Use toProto(using:) instead.") - TODO("Not yet implemented") + //TODO it just needs an equivalent to swift: preconditionFailure("Use toProto(using:) if that exists... + TODO("Not implemented") } } \ No newline at end of file From 5789c146de6277c7d0589bfd21f8e7685d037920 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 2 Dec 2020 16:36:40 +1100 Subject: [PATCH 12/24] add dependencies --- libsession/build.gradle | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/libsession/build.gradle b/libsession/build.gradle index aea50a3003..f722488d23 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -33,7 +33,9 @@ android { } dependencies { - + // Local: + implementation project(":libsignal") + // Remote: implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' @@ -41,4 +43,22 @@ dependencies { testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + // from libsignal: + implementation "com.google.protobuf:protobuf-java:$protobufVersion" + implementation "com.googlecode.libphonenumber:libphonenumber:8.10.7" + implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonDatabindVersion" + + implementation "org.whispersystems:curve25519-java:$curve25519Version" + implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" + implementation "org.threeten:threetenbp:1.3.6" + + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" + implementation "nl.komponents.kovenant:kovenant:$kovenantVersion" + + testImplementation "junit:junit:3.8.2" + testImplementation "org.assertj:assertj-core:1.7.1" + testImplementation "org.conscrypt:conscrypt-openjdk-uber:2.0.0" } \ No newline at end of file From c1f84732adc7ecd91651f7f659531c1e8a2a48e0 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 2 Dec 2020 16:38:12 +1100 Subject: [PATCH 13/24] move and refactor files from libsignal to libsession --- .../messaging/fileserver/FileServerAPI.kt | 262 ++++++++++ .../messaging/opengroups/OpenGroup.kt | 37 ++ .../messaging/opengroups/OpenGroupAPI.kt | 381 ++++++++++++++ .../messaging/opengroups/OpenGroupInfo.kt | 7 + .../messaging/opengroups/OpenGroupMessage.kt | 242 +++++++++ .../sending_receiving/Notification.kt | 2 - .../notifications/Notification.kt | 2 + .../notifications/PushNotificationAPI.kt | 101 ++++ .../messaging/utilities/DotNetAPI.kt | 268 ++++++++++ .../messaging/utilities/MessageWrapper.kt | 85 ++++ .../utilities/UnidentifiedAccessUtil.java | 121 +++++ .../libsession/snode/OnionRequestAPI.kt | 464 ++++++++++++++++++ .../snode/OnionRequestEncryption.kt | 94 ++++ .../org/session/libsession/snode/Snode.kt | 34 ++ .../org/session/libsession/snode/SnodeAPI.kt | 370 ++++++++++++++ .../session/libsession/snode/SnodeMessage.kt | 23 + .../snode/utilities/OKHTTPUtilities.kt | 49 ++ .../libsession/snode/utilities/Random.kt | 18 + .../session/libsession/utilities/AESGCM.kt | 58 +++ 19 files changed, 2616 insertions(+), 2 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroup.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupInfo.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt delete mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/Notification.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Notification.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.java create mode 100644 libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt create mode 100644 libsession/src/main/java/org/session/libsession/snode/OnionRequestEncryption.kt create mode 100644 libsession/src/main/java/org/session/libsession/snode/Snode.kt create mode 100644 libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt create mode 100644 libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt create mode 100644 libsession/src/main/java/org/session/libsession/snode/utilities/OKHTTPUtilities.kt create mode 100644 libsession/src/main/java/org/session/libsession/snode/utilities/Random.kt create mode 100644 libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt diff --git a/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt new file mode 100644 index 0000000000..4cf728a106 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/fileserver/FileServerAPI.kt @@ -0,0 +1,262 @@ +package org.session.libsession.messaging.fileserver + +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map +import okhttp3.Request +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.libsignal.util.Hex +import org.session.libsignal.service.internal.util.Base64 +import org.session.libsignal.service.internal.util.JsonUtil +import org.session.libsignal.service.loki.api.SnodeAPI +import org.session.libsignal.service.loki.api.LokiDotNetAPI +import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI +import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol +import org.session.libsignal.service.loki.protocol.shelved.multidevice.DeviceLink +import org.session.libsignal.service.loki.utilities.* +import java.net.URL +import java.util.concurrent.ConcurrentHashMap +import kotlin.collections.set + +class FileServerAPI(public val server: String, userPublicKey: String, userPrivateKey: ByteArray, private val database: LokiAPIDatabaseProtocol) : LokiDotNetAPI(userPublicKey, userPrivateKey, database) { + + companion object { + // region Settings + /** + * Deprecated. + */ + private val deviceLinkType = "network.loki.messenger.devicemapping" + /** + * Deprecated. + */ + private val deviceLinkRequestCache = ConcurrentHashMap, Exception>>() + /** + * Deprecated. + */ + private val deviceLinkUpdateInterval = 60 * 1000 + private val lastDeviceLinkUpdate = ConcurrentHashMap() + + internal val fileServerPublicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C" + internal val maxRetryCount = 4 + + public val maxFileSize = 10_000_000 // 10 MB + /** + * The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes + * is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP + * request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also + * be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when + * uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only + * possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds. + */ + public val fileSizeORMultiplier = 2 // TODO: It should be possible to set this to 1.5? + public val fileStorageBucketURL = "https://file-static.lokinet.org" + // endregion + + // region Initialization + lateinit var shared: FileServerAPI + + /** + * Must be called before `LokiAPI` is used. + */ + fun configure(userPublicKey: String, userPrivateKey: ByteArray, database: LokiAPIDatabaseProtocol) { + if (Companion::shared.isInitialized) { return } + val server = "https://file.getsession.org" + shared = FileServerAPI(server, userPublicKey, userPrivateKey, database) + } + // endregion + } + + // region Device Link Update Result + sealed class DeviceLinkUpdateResult { + class Success(val publicKey: String, val deviceLinks: Set) : DeviceLinkUpdateResult() + class Failure(val publicKey: String, val error: Exception) : DeviceLinkUpdateResult() + } + // endregion + + // region API + public fun hasDeviceLinkCacheExpired(referenceTime: Long = System.currentTimeMillis(), publicKey: String): Boolean { + return !lastDeviceLinkUpdate.containsKey(publicKey) || (referenceTime - lastDeviceLinkUpdate[publicKey]!! > deviceLinkUpdateInterval) + } + + fun getDeviceLinks(publicKey: String, isForcedUpdate: Boolean = false): Promise, Exception> { + return Promise.of(setOf()) + /* + if (deviceLinkRequestCache.containsKey(publicKey) && !isForcedUpdate) { + val result = deviceLinkRequestCache[publicKey] + if (result != null) { return result } // A request was already pending + } + val promise = getDeviceLinks(setOf(publicKey), isForcedUpdate) + deviceLinkRequestCache[publicKey] = promise + promise.always { + deviceLinkRequestCache.remove(publicKey) + } + return promise + */ + } + + fun getDeviceLinks(publicKeys: Set, isForcedUpdate: Boolean = false): Promise, Exception> { + return Promise.of(setOf()) + /* + val validPublicKeys = publicKeys.filter { PublicKeyValidation.isValid(it) } + val now = System.currentTimeMillis() + // IMPORTANT: Don't fetch device links for the current user (i.e. don't remove the it != userHexEncodedPublicKey) check below + val updatees = validPublicKeys.filter { it != userPublicKey && (hasDeviceLinkCacheExpired(now, it) || isForcedUpdate) }.toSet() + val cachedDeviceLinks = validPublicKeys.minus(updatees).flatMap { database.getDeviceLinks(it) }.toSet() + if (updatees.isEmpty()) { + return Promise.of(cachedDeviceLinks) + } else { + return getUserProfiles(updatees, server, true).map(SnodeAPI.sharedContext) { data -> + data.map dataMap@ { node -> + val publicKey = node["username"] as String + val annotations = node["annotations"] as List> + val deviceLinksAnnotation = annotations.find { + annotation -> (annotation["type"] as String) == deviceLinkType + } ?: return@dataMap DeviceLinkUpdateResult.Success(publicKey, setOf()) + val value = deviceLinksAnnotation["value"] as Map<*, *> + val deviceLinksAsJSON = value["authorisations"] as List> + val deviceLinks = deviceLinksAsJSON.mapNotNull { deviceLinkAsJSON -> + try { + val masterPublicKey = deviceLinkAsJSON["primaryDevicePubKey"] as String + val slavePublicKey = deviceLinkAsJSON["secondaryDevicePubKey"] as String + var requestSignature: ByteArray? = null + var authorizationSignature: ByteArray? = null + if (deviceLinkAsJSON["requestSignature"] != null) { + val base64EncodedSignature = deviceLinkAsJSON["requestSignature"] as String + requestSignature = Base64.decode(base64EncodedSignature) + } + if (deviceLinkAsJSON["grantSignature"] != null) { + val base64EncodedSignature = deviceLinkAsJSON["grantSignature"] as String + authorizationSignature = Base64.decode(base64EncodedSignature) + } + val deviceLink = DeviceLink(masterPublicKey, slavePublicKey, requestSignature, authorizationSignature) + val isValid = deviceLink.verify() + if (!isValid) { + Log.d("Loki", "Ignoring invalid device link: $deviceLinkAsJSON.") + return@mapNotNull null + } + deviceLink + } catch (e: Exception) { + Log.d("Loki", "Failed to parse device links for $publicKey from $deviceLinkAsJSON due to error: $e.") + null + } + }.toSet() + DeviceLinkUpdateResult.Success(publicKey, deviceLinks) + } + }.recover { e -> + publicKeys.map { DeviceLinkUpdateResult.Failure(it, e) } + }.success { updateResults -> + for (updateResult in updateResults) { + if (updateResult is DeviceLinkUpdateResult.Success) { + database.clearDeviceLinks(updateResult.publicKey) + updateResult.deviceLinks.forEach { database.addDeviceLink(it) } + } else { + // Do nothing + } + } + }.map(SnodeAPI.sharedContext) { updateResults -> + val deviceLinks = mutableListOf() + for (updateResult in updateResults) { + when (updateResult) { + is DeviceLinkUpdateResult.Success -> { + lastDeviceLinkUpdate[updateResult.publicKey] = now + deviceLinks.addAll(updateResult.deviceLinks) + } + is DeviceLinkUpdateResult.Failure -> { + if (updateResult.error is SnodeAPI.Error.ParsingFailed) { + lastDeviceLinkUpdate[updateResult.publicKey] = now // Don't infinitely update in case of a parsing failure + } + deviceLinks.addAll(database.getDeviceLinks(updateResult.publicKey)) // Fall back on cached device links in case of a failure + } + } + } + // Updatees that didn't show up in the response provided by the file server are assumed to not have any device links + val excludedUpdatees = updatees.filter { updatee -> + updateResults.find { updateResult -> + when (updateResult) { + is DeviceLinkUpdateResult.Success -> updateResult.publicKey == updatee + is DeviceLinkUpdateResult.Failure -> updateResult.publicKey == updatee + } + } == null + } + excludedUpdatees.forEach { + lastDeviceLinkUpdate[it] = now + } + deviceLinks.union(cachedDeviceLinks) + }.recover { + publicKeys.flatMap { database.getDeviceLinks(it) }.toSet() + } + } + */ + } + + fun setDeviceLinks(deviceLinks: Set): Promise { + return Promise.of(Unit) + /* + val isMaster = deviceLinks.find { it.masterPublicKey == userPublicKey } != null + val deviceLinksAsJSON = deviceLinks.map { it.toJSON() } + val value = if (deviceLinks.isNotEmpty()) mapOf( "isPrimary" to isMaster, "authorisations" to deviceLinksAsJSON ) else null + val annotation = mapOf( "type" to deviceLinkType, "value" to value ) + val parameters = mapOf( "annotations" to listOf( annotation ) ) + return retryIfNeeded(maxRetryCount) { + execute(HTTPVerb.PATCH, server, "/users/me", parameters = parameters) + }.map { Unit } + */ + } + + fun addDeviceLink(deviceLink: DeviceLink): Promise { + return Promise.of(Unit) + /* + Log.d("Loki", "Updating device links.") + return getDeviceLinks(userPublicKey, true).bind { deviceLinks -> + val mutableDeviceLinks = deviceLinks.toMutableSet() + mutableDeviceLinks.add(deviceLink) + setDeviceLinks(mutableDeviceLinks) + }.success { + database.addDeviceLink(deviceLink) + }.map { Unit } + */ + } + + fun removeDeviceLink(deviceLink: DeviceLink): Promise { + return Promise.of(Unit) + /* + Log.d("Loki", "Updating device links.") + return getDeviceLinks(userPublicKey, true).bind { deviceLinks -> + val mutableDeviceLinks = deviceLinks.toMutableSet() + mutableDeviceLinks.remove(deviceLink) + setDeviceLinks(mutableDeviceLinks) + }.success { + database.removeDeviceLink(deviceLink) + }.map { Unit } + */ + } + // endregion + + // region Open Group Server Public Key + fun getPublicKeyForOpenGroupServer(openGroupServer: String): Promise { + val publicKey = database.getOpenGroupPublicKey(openGroupServer) + if (publicKey != null && PublicKeyValidation.isValid(publicKey, 64, false)) { + return Promise.of(publicKey) + } else { + val url = "$server/loki/v1/getOpenGroupKey/${URL(openGroupServer).host}" + val request = Request.Builder().url(url) + request.addHeader("Content-Type", "application/json") + request.addHeader("Authorization", "Bearer loki") // Tokenless request; use a dummy token + return OnionRequestAPI.sendOnionRequest(request.build(), server, fileServerPublicKey).map { json -> + try { + val bodyAsString = json["data"] as String + val body = JsonUtil.fromJson(bodyAsString) + val base64EncodedPublicKey = body.get("data").asText() + val prefixedPublicKey = Base64.decode(base64EncodedPublicKey) + val hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString() + val result = hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded() + database.setOpenGroupPublicKey(openGroupServer, result) + result + } catch (exception: Exception) { + Log.d("Loki", "Couldn't parse open group public key from: $json.") + throw exception + } + } + } + } +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroup.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroup.kt new file mode 100644 index 0000000000..868bb02fe4 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroup.kt @@ -0,0 +1,37 @@ +package org.session.libsession.messaging.opengroups + +import org.session.libsignal.service.internal.util.JsonUtil + +public data class OpenGroup( + public val channel: Long, + private val serverURL: String, + public val displayName: String, + public val isDeletable: Boolean +) { + public val server get() = serverURL.toLowerCase() + public val id get() = getId(channel, server) + + companion object { + + @JvmStatic fun getId(channel: Long, server: String): String { + return "$server.$channel" + } + + @JvmStatic fun fromJSON(jsonAsString: String): OpenGroup? { + try { + val json = JsonUtil.fromJson(jsonAsString) + val channel = json.get("channel").asLong() + val server = json.get("server").asText().toLowerCase() + val displayName = json.get("displayName").asText() + val isDeletable = json.get("isDeletable").asBoolean() + return OpenGroup(channel, server, displayName, isDeletable) + } catch (e: Exception) { + return null + } + } + } + + public fun toJSON(): Map { + return mapOf( "channel" to channel, "server" to server, "displayName" to displayName, "isDeletable" to isDeletable ) + } +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt new file mode 100644 index 0000000000..0cff14e41c --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupAPI.kt @@ -0,0 +1,381 @@ +package org.session.libsession.messaging.opengroups + +import nl.komponents.kovenant.Kovenant +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.deferred +import nl.komponents.kovenant.functional.map +import nl.komponents.kovenant.then +import org.session.libsession.messaging.Configuration + +import org.session.libsession.messaging.utilities.DotNetAPI +import org.session.libsession.messaging.fileserver.FileServerAPI +import org.session.libsession.snode.SnodeAPI + +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.util.Base64 +import org.session.libsignal.service.internal.util.Hex +import org.session.libsignal.service.internal.util.JsonUtil +import org.session.libsignal.service.loki.utilities.DownloadUtilities +import org.session.libsignal.service.loki.utilities.createContext +import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey +import org.session.libsignal.service.loki.utilities.retryIfNeeded +import java.io.ByteArrayOutputStream +import java.text.SimpleDateFormat +import java.util.* + +object OpenGroupAPI: DotNetAPI() { + + private val moderators: HashMap>> = hashMapOf() // Server URL to (channel ID to set of moderator IDs) + val sharedContext = Kovenant.createContext("LokiPublicChatAPISharedContext") + + // region Settings + private val fallbackBatchCount = 64 + private val maxRetryCount = 8 + // endregion + + // region Convenience + private val channelInfoType = "net.patter-app.settings" + private val attachmentType = "net.app.core.oembed" + @JvmStatic + public val openGroupMessageType = "network.loki.messenger.publicChat" + @JvmStatic + public val profilePictureType = "network.loki.messenger.avatar" + + fun getDefaultChats(): List { + return listOf() // Don't auto-join any open groups right now + } + + public fun isUserModerator(hexEncodedPublicKey: String, channel: Long, server: String): Boolean { + if (moderators[server] != null && moderators[server]!![channel] != null) { + return moderators[server]!![channel]!!.contains(hexEncodedPublicKey) + } + return false + } + // endregion + + // region Public API + public fun getMessages(channel: Long, server: String): Promise, Exception> { + Log.d("Loki", "Getting messages for open group with ID: $channel on server: $server.") + val storage = Configuration.shared.storage + val parameters = mutableMapOf( "include_annotations" to 1 ) + val lastMessageServerID = storage.getLastMessageServerID(channel, server) + if (lastMessageServerID != null) { + parameters["since_id"] = lastMessageServerID + } else { + parameters["count"] = fallbackBatchCount + parameters["include_deleted"] = 0 + } + return execute(HTTPVerb.GET, server, "channels/$channel/messages", parameters = parameters).then(sharedContext) { json -> + try { + val data = json["data"] as List> + val messages = data.mapNotNull { message -> + try { + val isDeleted = message["is_deleted"] as? Boolean ?: false + if (isDeleted) { return@mapNotNull null } + // Ignore messages without annotations + if (message["annotations"] == null) { return@mapNotNull null } + val annotation = (message["annotations"] as List>).find { + ((it["type"] as? String ?: "") == openGroupMessageType) && it["value"] != null + } ?: return@mapNotNull null + val value = annotation["value"] as Map<*, *> + val serverID = message["id"] as? Long ?: (message["id"] as? Int)?.toLong() ?: (message["id"] as String).toLong() + val user = message["user"] as Map<*, *> + val publicKey = user["username"] as String + val displayName = user["name"] as? String ?: "Anonymous" + var profilePicture: OpenGroupMessage.ProfilePicture? = null + if (user["annotations"] != null) { + val profilePictureAnnotation = (user["annotations"] as List>).find { + ((it["type"] as? String ?: "") == profilePictureType) && it["value"] != null + } + val profilePictureAnnotationValue = profilePictureAnnotation?.get("value") as? Map<*, *> + if (profilePictureAnnotationValue != null && profilePictureAnnotationValue["profileKey"] != null && profilePictureAnnotationValue["url"] != null) { + try { + val profileKey = Base64.decode(profilePictureAnnotationValue["profileKey"] as String) + val url = profilePictureAnnotationValue["url"] as String + profilePicture = OpenGroupMessage.ProfilePicture(profileKey, url) + } catch (e: Exception) {} + } + } + @Suppress("NAME_SHADOWING") val body = message["text"] as String + val timestamp = value["timestamp"] as? Long ?: (value["timestamp"] as? Int)?.toLong() ?: (value["timestamp"] as String).toLong() + var quote: OpenGroupMessage.Quote? = null + if (value["quote"] != null) { + val replyTo = message["reply_to"] as? Long ?: (message["reply_to"] as? Int)?.toLong() ?: (message["reply_to"] as String).toLong() + val quoteAnnotation = value["quote"] as? Map<*, *> + val quoteTimestamp = quoteAnnotation?.get("id") as? Long ?: (quoteAnnotation?.get("id") as? Int)?.toLong() ?: (quoteAnnotation?.get("id") as? String)?.toLong() ?: 0L + val author = quoteAnnotation?.get("author") as? String + val text = quoteAnnotation?.get("text") as? String + quote = if (quoteTimestamp > 0L && author != null && text != null) OpenGroupMessage.Quote(quoteTimestamp, author, text, replyTo) else null + } + val attachmentsAsJSON = (message["annotations"] as List>).filter { + ((it["type"] as? String ?: "") == attachmentType) && it["value"] != null + } + val attachments = attachmentsAsJSON.mapNotNull { it["value"] as? Map<*, *> }.mapNotNull { attachmentAsJSON -> + try { + val kindAsString = attachmentAsJSON["lokiType"] as String + val kind = OpenGroupMessage.Attachment.Kind.values().first { it.rawValue == kindAsString } + val id = attachmentAsJSON["id"] as? Long ?: (attachmentAsJSON["id"] as? Int)?.toLong() ?: (attachmentAsJSON["id"] as String).toLong() + val contentType = attachmentAsJSON["contentType"] as String + val size = attachmentAsJSON["size"] as? Int ?: (attachmentAsJSON["size"] as? Long)?.toInt() ?: (attachmentAsJSON["size"] as String).toInt() + val fileName = attachmentAsJSON["fileName"] as String + val flags = 0 + val url = attachmentAsJSON["url"] as String + val caption = attachmentAsJSON["caption"] as? String + val linkPreviewURL = attachmentAsJSON["linkPreviewUrl"] as? String + val linkPreviewTitle = attachmentAsJSON["linkPreviewTitle"] as? String + if (kind == OpenGroupMessage.Attachment.Kind.LinkPreview && (linkPreviewURL == null || linkPreviewTitle == null)) { + null + } else { + OpenGroupMessage.Attachment(kind, server, id, contentType, size, fileName, flags, 0, 0, caption, url, linkPreviewURL, linkPreviewTitle) + } + } catch (e: Exception) { + Log.d("Loki","Couldn't parse attachment due to error: $e.") + null + } + } + // Set the last message server ID here to avoid the situation where a message doesn't have a valid signature and this function is called over and over + @Suppress("NAME_SHADOWING") val lastMessageServerID = storage.getLastMessageServerID(channel, server) + if (serverID > lastMessageServerID ?: 0) { storage.setLastMessageServerID(channel, server, serverID) } + val hexEncodedSignature = value["sig"] as String + val signatureVersion = value["sigver"] as? Long ?: (value["sigver"] as? Int)?.toLong() ?: (value["sigver"] as String).toLong() + val signature = OpenGroupMessage.Signature(Hex.fromStringCondensed(hexEncodedSignature), signatureVersion) + val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) + format.timeZone = TimeZone.getTimeZone("GMT") + val dateAsString = message["created_at"] as String + val serverTimestamp = format.parse(dateAsString).time + // Verify the message + val groupMessage = OpenGroupMessage(serverID, publicKey, displayName, body, timestamp, openGroupMessageType, quote, attachments, profilePicture, signature, serverTimestamp) + if (groupMessage.hasValidSignature()) groupMessage else null + } catch (exception: Exception) { + Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server from: ${JsonUtil.toJson(message)}. Exception: ${exception.message}") + return@mapNotNull null + } + }.sortedBy { it.serverTimestamp } + messages + } catch (exception: Exception) { + Log.d("Loki", "Couldn't parse messages for open group with ID: $channel on server: $server.") + throw exception + } + } + } + + public fun getDeletedMessageServerIDs(channel: Long, server: String): Promise, Exception> { + Log.d("Loki", "Getting deleted messages for open group with ID: $channel on server: $server.") + val storage = Configuration.shared.storage + val parameters = mutableMapOf() + val lastDeletionServerID = storage.getLastDeletionServerID(channel, server) + if (lastDeletionServerID != null) { + parameters["since_id"] = lastDeletionServerID + } else { + parameters["count"] = fallbackBatchCount + } + return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/deletes", parameters = parameters).then(sharedContext) { json -> + try { + val deletedMessageServerIDs = (json["data"] as List>).mapNotNull { deletion -> + try { + val serverID = deletion["id"] as? Long ?: (deletion["id"] as? Int)?.toLong() ?: (deletion["id"] as String).toLong() + val messageServerID = deletion["message_id"] as? Long ?: (deletion["message_id"] as? Int)?.toLong() ?: (deletion["message_id"] as String).toLong() + @Suppress("NAME_SHADOWING") val lastDeletionServerID = storage.getLastDeletionServerID(channel, server) + if (serverID > (lastDeletionServerID ?: 0)) { storage.setLastDeletionServerID(channel, server, serverID) } + messageServerID + } catch (exception: Exception) { + Log.d("Loki", "Couldn't parse deleted message for open group with ID: $channel on server: $server. Exception: ${exception.message}") + return@mapNotNull null + } + } + deletedMessageServerIDs + } catch (exception: Exception) { + Log.d("Loki", "Couldn't parse deleted messages for open group with ID: $channel on server: $server.") + throw exception + } + } + } + + public fun sendMessage(message: OpenGroupMessage, channel: Long, server: String): Promise { + val deferred = deferred() + val storage = Configuration.shared.storage + val userKeyPair = storage.getUserKeyPair() ?: throw Error.Generic + val userDisplayName = storage.getUserDisplayName() ?: throw Error.Generic + Thread { + val signedMessage = message.sign(userKeyPair.privateKey.serialize()) + if (signedMessage == null) { + deferred.reject(Error.SigningFailed) + } else { + retryIfNeeded(maxRetryCount) { + Log.d("Loki", "Sending message to open group with ID: $channel on server: $server.") + val parameters = signedMessage.toJSON() + execute(HTTPVerb.POST, server, "channels/$channel/messages", parameters = parameters).then(sharedContext) { json -> + try { + val data = json["data"] as Map<*, *> + val serverID = (data["id"] as? Long) ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as String).toLong() + val text = data["text"] as String + val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) + format.timeZone = TimeZone.getTimeZone("GMT") + val dateAsString = data["created_at"] as String + val timestamp = format.parse(dateAsString).time + @Suppress("NAME_SHADOWING") val message = OpenGroupMessage(serverID, userKeyPair.hexEncodedPublicKey, userDisplayName, text, timestamp, openGroupMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp) + message + } catch (exception: Exception) { + Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server.") + throw exception + } + } + }.success { + deferred.resolve(it) + }.fail { + deferred.reject(it) + } + } + }.start() + return deferred.promise + } + + public fun deleteMessage(messageServerID: Long, channel: Long, server: String, isSentByUser: Boolean): Promise { + return retryIfNeeded(maxRetryCount) { + val isModerationRequest = !isSentByUser + Log.d("Loki", "Deleting message with ID: $messageServerID from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest).") + val endpoint = if (isSentByUser) "channels/$channel/messages/$messageServerID" else "loki/v1/moderation/message/$messageServerID" + execute(HTTPVerb.DELETE, server, endpoint, isJSONRequired = false).then { + Log.d("Loki", "Deleted message with ID: $messageServerID from open group with ID: $channel on server: $server.") + messageServerID + } + } + } + + public fun deleteMessages(messageServerIDs: List, channel: Long, server: String, isSentByUser: Boolean): Promise, Exception> { + return retryIfNeeded(maxRetryCount) { + val isModerationRequest = !isSentByUser + val parameters = mapOf( "ids" to messageServerIDs.joinToString(",") ) + Log.d("Loki", "Deleting messages with IDs: ${messageServerIDs.joinToString()} from open group with ID: $channel on server: $server (isModerationRequest = $isModerationRequest).") + val endpoint = if (isSentByUser) "loki/v1/messages" else "loki/v1/moderation/messages" + execute(HTTPVerb.DELETE, server, endpoint, parameters = parameters, isJSONRequired = false).then { json -> + Log.d("Loki", "Deleted messages with IDs: $messageServerIDs from open group with ID: $channel on server: $server.") + messageServerIDs + } + } + } + + public fun getModerators(channel: Long, server: String): Promise, Exception> { + return execute(HTTPVerb.GET, server, "loki/v1/channel/$channel/get_moderators").then(sharedContext) { json -> + try { + @Suppress("UNCHECKED_CAST") val moderators = json["moderators"] as? List + val moderatorsAsSet = moderators.orEmpty().toSet() + if (this.moderators[server] != null) { + this.moderators[server]!![channel] = moderatorsAsSet + } else { + this.moderators[server] = hashMapOf( channel to moderatorsAsSet ) + } + moderatorsAsSet + } catch (exception: Exception) { + Log.d("Loki", "Couldn't parse moderators for open group with ID: $channel on server: $server.") + throw exception + } + } + } + + public fun getChannelInfo(channel: Long, server: String): Promise { + return retryIfNeeded(maxRetryCount) { + val parameters = mapOf( "include_annotations" to 1 ) + execute(HTTPVerb.GET, server, "/channels/$channel", parameters = parameters).then(sharedContext) { json -> + try { + val data = json["data"] as Map<*, *> + val annotations = data["annotations"] as List> + val annotation = annotations.find { (it["type"] as? String ?: "") == channelInfoType } ?: throw Error.ParsingFailed + val info = annotation["value"] as Map<*, *> + val displayName = info["name"] as String + val countInfo = data["counts"] as Map<*, *> + val memberCount = countInfo["subscribers"] as? Int ?: (countInfo["subscribers"] as? Long)?.toInt() ?: (countInfo["subscribers"] as String).toInt() + val profilePictureURL = info["avatar"] as String + val publicChatInfo = OpenGroupInfo(displayName, profilePictureURL, memberCount) + Configuration.shared.storage.setUserCount(channel, server, memberCount) + publicChatInfo + } catch (exception: Exception) { + Log.d("Loki", "Couldn't parse info for open group with ID: $channel on server: $server.") + throw exception + } + } + } + } + + public fun updateProfileIfNeeded(channel: Long, server: String, groupID: String, info: OpenGroupInfo, isForcedUpdate: Boolean) { + val storage = Configuration.shared.storage + storage.setUserCount(channel, server, info.memberCount) + storage.updateTitle(groupID, info.displayName) + // Download and update profile picture if needed + val oldProfilePictureURL = storage.getOpenGroupProfilePictureURL(channel, server) + if (isForcedUpdate || oldProfilePictureURL != info.profilePictureURL) { + val profilePictureAsByteArray = downloadOpenGroupProfilePicture(server, info.profilePictureURL) ?: return + storage.updateProfilePicture(groupID, profilePictureAsByteArray) + storage.setOpenGroupProfilePictureURL(channel, server, info.profilePictureURL) + } + } + + public fun downloadOpenGroupProfilePicture(server: String, endpoint: String): ByteArray? { + val url = "${server.removeSuffix("/")}/${endpoint.removePrefix("/")}" + Log.d("Loki", "Downloading open group profile picture from \"$url\".") + val outputStream = ByteArrayOutputStream() + try { + DownloadUtilities.downloadFile(outputStream, url, FileServerAPI.maxFileSize, null) + Log.d("Loki", "Open group profile picture was successfully loaded from \"$url\"") + return outputStream.toByteArray() + } catch (e: Exception) { + Log.d("Loki", "Failed to download open group profile picture from \"$url\" due to error: $e.") + return null + } finally { + outputStream.close() + } + } + + public fun join(channel: Long, server: String): Promise { + return retryIfNeeded(maxRetryCount) { + execute(HTTPVerb.POST, server, "/channels/$channel/subscribe").then { + Log.d("Loki", "Joined channel with ID: $channel on server: $server.") + } + } + } + + public fun leave(channel: Long, server: String): Promise { + return retryIfNeeded(maxRetryCount) { + execute(HTTPVerb.DELETE, server, "/channels/$channel/subscribe").then { + Log.d("Loki", "Left channel with ID: $channel on server: $server.") + } + } + } + + public fun getDisplayNames(publicKeys: Set, server: String): Promise, Exception> { + return getUserProfiles(publicKeys, server, false).map(sharedContext) { json -> + val mapping = mutableMapOf() + for (user in json) { + if (user["username"] != null) { + val publicKey = user["username"] as String + val displayName = user["name"] as? String ?: "Anonymous" + mapping[publicKey] = displayName + } + } + mapping + } + } + + public fun setDisplayName(newDisplayName: String?, server: String): Promise { + Log.d("Loki", "Updating display name on server: $server.") + val parameters = mapOf( "name" to (newDisplayName ?: "") ) + return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters).map { Unit } + } + + public fun setProfilePicture(server: String, profileKey: ByteArray, url: String?): Promise { + return setProfilePicture(server, Base64.encodeBytes(profileKey), url) + } + + public fun setProfilePicture(server: String, profileKey: String, url: String?): Promise { + Log.d("Loki", "Updating profile picture on server: $server.") + val value = when (url) { + null -> null + else -> mapOf( "profileKey" to profileKey, "url" to url ) + } + // TODO: This may actually completely replace the annotations, have to double check it + return setSelfAnnotation(server, profilePictureType, value).map { Unit }.fail { + Log.d("Loki", "Failed to update profile picture due to error: $it.") + } + } + // endregion +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupInfo.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupInfo.kt new file mode 100644 index 0000000000..9cd1f18dea --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupInfo.kt @@ -0,0 +1,7 @@ +package org.session.libsession.messaging.opengroups + +public data class OpenGroupInfo ( + public val displayName: String, + public val profilePictureURL: String, + public val memberCount: Int +) diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt new file mode 100644 index 0000000000..51eb95b2f3 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt @@ -0,0 +1,242 @@ +package org.session.libsession.messaging.opengroups + +import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.util.Hex +import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded +import org.whispersystems.curve25519.Curve25519 + +public data class OpenGroupMessage( + public val serverID: Long?, + public val senderPublicKey: String, + public val displayName: String, + public val body: String, + public val timestamp: Long, + public val type: String, + public val quote: Quote?, + public val attachments: List, + public val profilePicture: ProfilePicture?, + public val signature: Signature?, + public val serverTimestamp: Long, +) { + + // region Settings + companion object { + fun from(message: VisibleMessage, server: String): OpenGroupMessage? { + val storage = Configuration.shared.storage + val userPublicKey = storage.getUserPublicKey() ?: return null + // Validation + if (!message.isValid) { return null } // Should be valid at this point + // Quote + val quote: OpenGroupMessage.Quote? = { + val quote = message.quote + if (quote != null && quote.isValid) { + val quotedMessageServerID = storage.getQuoteServerID(quote.id, quote.publicKey) + OpenGroupMessage.Quote(quote.timestamp, quote.publicKey, quote.text, quotedMessageServerID) + } else { + null + } + }() + // Message + val displayname = storage.getUserDisplayName() ?: "Anonymous" + val body = message.text ?: message.sentTimestamp.toString() // The back-end doesn't accept messages without a body so we use this as a workaround + val result = OpenGroupMessage(null, userPublicKey, displayname, body, message.sentTimestamp!!, OpenGroupAPI.openGroupMessageType, quote, mutableListOf(), null, null, 0) + // Link preview + val linkPreview = message.linkPreview + linkPreview?.let { + if (!linkPreview.isValid) { return@let } + val attachment = linkPreview.getImage() ?: return@let + val openGroupLinkPreview = OpenGroupMessage.Attachment( + OpenGroupMessage.Attachment.Kind.LinkPreview, + server, + attachment.getId(), + attachment.getContentType(), + attachment.getSize(), + attachment.getFileName(), + attachment.getFlags(), + attachment.getWidth(), + attachment.getHeight(), + attachment.getCaption(), + attachment.getUrl(), + linkPreview.getUrl(), + linkPreview.getTitle()) + result.attachments.add(openGroupLinkPreview) + } + // Attachments + val attachments = message.getAttachemnts().forEach { + val attachement = OpenGroupMessage.Attachment( + OpenGroupMessage.Attachment.Kind.Attachment, + server, + it.getId(), + it.getContentType(), + it.getSize(), + it.getFileName(), + it.getFlags(), + it.getWidth(), + it.getHeight(), + it.getCaption(), + it.getUrl(), + linkPreview.getUrl(), + linkPreview.getTitle()) + result.attachments.add(attachement) + } + // Return + return result + } + + private val curve = Curve25519.getInstance(Curve25519.BEST) + private val signatureVersion: Long = 1 + private val attachmentType = "net.app.core.oembed" + } + // endregion + + // region Types + public data class ProfilePicture( + public val profileKey: ByteArray, + public val url: String, + ) + + public data class Quote( + public val quotedMessageTimestamp: Long, + public val quoteePublicKey: String, + public val quotedMessageBody: String, + public val quotedMessageServerID: Long? = null, + ) + + public data class Signature( + public val data: ByteArray, + public val version: Long, + ) + + public data class Attachment( + public val kind: Kind, + public val server: String, + public val serverID: Long, + public val contentType: String, + public val size: Int, + public val fileName: String, + public val flags: Int, + public val width: Int, + public val height: Int, + public val caption: String?, + public val url: String, + /** + Guaranteed to be non-`nil` if `kind` is `LinkPreview`. + */ + public val linkPreviewURL: String?, + /** + Guaranteed to be non-`nil` if `kind` is `LinkPreview`. + */ + public val linkPreviewTitle: String?, + ) { + public val dotNetAPIType = when { + contentType.startsWith("image") -> "photo" + contentType.startsWith("video") -> "video" + contentType.startsWith("audio") -> "audio" + else -> "other" + } + + public enum class Kind(val rawValue: String) { + Attachment("attachment"), LinkPreview("preview") + } + } + // endregion + + // region Initialization + constructor(hexEncodedPublicKey: String, displayName: String, body: String, timestamp: Long, type: String, quote: Quote?, attachments: List) + : this(null, hexEncodedPublicKey, displayName, body, timestamp, type, quote, attachments, null, null, 0) + // endregion + + // region Crypto + internal fun sign(privateKey: ByteArray): OpenGroupMessage? { + val data = getValidationData(signatureVersion) + if (data == null) { + Log.d("Loki", "Failed to sign public chat message.") + return null + } + try { + val signatureData = curve.calculateSignature(privateKey, data) + val signature = Signature(signatureData, signatureVersion) + return copy(signature = signature) + } catch (e: Exception) { + Log.d("Loki", "Failed to sign public chat message due to error: ${e.message}.") + return null + } + } + + internal fun hasValidSignature(): Boolean { + if (signature == null) { return false } + val data = getValidationData(signature.version) ?: return false + val publicKey = Hex.fromStringCondensed(senderPublicKey.removing05PrefixIfNeeded()) + try { + return curve.verifySignature(publicKey, data, signature.data) + } catch (e: Exception) { + Log.d("Loki", "Failed to verify public chat message due to error: ${e.message}.") + return false + } + } + // endregion + + // region Parsing + internal fun toJSON(): Map { + val value = mutableMapOf("timestamp" to timestamp) + if (quote != null) { + value["quote"] = mapOf("id" to quote.quotedMessageTimestamp, "author" to quote.quoteePublicKey, "text" to quote.quotedMessageBody) + } + if (signature != null) { + value["sig"] = Hex.toStringCondensed(signature.data) + value["sigver"] = signature.version + } + val annotation = mapOf("type" to type, "value" to value) + val annotations = mutableListOf(annotation) + attachments.forEach { attachment -> + val attachmentValue = mutableMapOf( + // Fields required by the .NET API + "version" to 1, + "type" to attachment.dotNetAPIType, + // Custom fields + "lokiType" to attachment.kind.rawValue, + "server" to attachment.server, + "id" to attachment.serverID, + "contentType" to attachment.contentType, + "size" to attachment.size, + "fileName" to attachment.fileName, + "flags" to attachment.flags, + "width" to attachment.width, + "height" to attachment.height, + "url" to attachment.url + ) + if (attachment.caption != null) { attachmentValue["caption"] = attachment.caption } + if (attachment.linkPreviewURL != null) { attachmentValue["linkPreviewUrl"] = attachment.linkPreviewURL } + if (attachment.linkPreviewTitle != null) { attachmentValue["linkPreviewTitle"] = attachment.linkPreviewTitle } + val attachmentAnnotation = mapOf("type" to attachmentType, "value" to attachmentValue) + annotations.add(attachmentAnnotation) + } + val result = mutableMapOf("text" to body, "annotations" to annotations) + if (quote?.quotedMessageServerID != null) { + result["reply_to"] = quote.quotedMessageServerID + } + return result + } + // endregion + + // region Convenience + private fun getValidationData(signatureVersion: Long): ByteArray? { + var string = "${body.trim()}$timestamp" + if (quote != null) { + string += "${quote.quotedMessageTimestamp}${quote.quoteePublicKey}${quote.quotedMessageBody.trim()}" + if (quote.quotedMessageServerID != null) { + string += "${quote.quotedMessageServerID}" + } + } + string += attachments.sortedBy { it.serverID }.map { it.serverID }.joinToString("") + string += "$signatureVersion" + try { + return string.toByteArray(Charsets.UTF_8) + } catch (exception: Exception) { + return null + } + } + // endregion +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/Notification.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/Notification.kt deleted file mode 100644 index 72036e203d..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/Notification.kt +++ /dev/null @@ -1,2 +0,0 @@ -package org.session.messaging.sending_receiving - diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Notification.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Notification.kt new file mode 100644 index 0000000000..f8ae5a8be6 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/Notification.kt @@ -0,0 +1,2 @@ +package org.session.libsession.messaging.sending_receiving.notifications + diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt new file mode 100644 index 0000000000..80d925a081 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/notifications/PushNotificationAPI.kt @@ -0,0 +1,101 @@ +package org.session.libsession.messaging.sending_receiving.notifications + +import android.content.Context +import nl.komponents.kovenant.functional.map +import okhttp3.* +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.util.JsonUtil +import org.session.libsignal.service.loki.api.onionrequests.OnionRequestAPI +import org.session.libsignal.service.loki.utilities.retryIfNeeded +import java.io.IOException + +object PushNotificationAPI { + val server = "https://live.apns.getsession.org" + val serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" + private val maxRetryCount = 4 + private val tokenExpirationInterval = 12 * 60 * 60 * 1000 + + enum class ClosedGroupOperation { + Subscribe, Unsubscribe; + + val rawValue: String + get() { + return when (this) { + Subscribe -> "subscribe_closed_group" + Unsubscribe -> "unsubscribe_closed_group" + } + } + } + + fun unregister(token: String, context: Context) { + val parameters = mapOf( "token" to token ) + val url = "$server/unregister" + val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val request = Request.Builder().url(url).post(body) + retryIfNeeded(maxRetryCount) { + OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json -> + val code = json["code"] as? Int + if (code != null && code != 0) { + TextSecurePreferences.setIsUsingFCM(context, false) + } else { + Log.d("Loki", "Couldn't disable FCM due to error: ${json["message"] as? String ?: "null"}.") + } + }.fail { exception -> + Log.d("Loki", "Couldn't disable FCM due to error: ${exception}.") + } + } + // Unsubscribe from all closed groups + val allClosedGroupPublicKeys = DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys() + val userPublicKey = TextSecurePreferences.getLocalNumber(context) + allClosedGroupPublicKeys.forEach { closedGroup -> + performOperation(context, ClosedGroupOperation.Unsubscribe, closedGroup, userPublicKey) + } + } + + fun register(token: String, publicKey: String, context: Context, force: Boolean) { + val oldToken = TextSecurePreferences.getFCMToken(context) + val lastUploadDate = TextSecurePreferences.getLastFCMUploadTime(context) + if (!force && token == oldToken && System.currentTimeMillis() - lastUploadDate < tokenExpirationInterval) { return } + val parameters = mapOf( "token" to token, "pubKey" to publicKey ) + val url = "$server/register" + val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val request = Request.Builder().url(url).post(body) + retryIfNeeded(maxRetryCount) { + OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json -> + val code = json["code"] as? Int + if (code != null && code != 0) { + TextSecurePreferences.setIsUsingFCM(context, true) + TextSecurePreferences.setFCMToken(context, token) + TextSecurePreferences.setLastFCMUploadTime(context, System.currentTimeMillis()) + } else { + Log.d("Loki", "Couldn't register for FCM due to error: ${json["message"] as? String ?: "null"}.") + } + }.fail { exception -> + Log.d("Loki", "Couldn't register for FCM due to error: ${exception}.") + } + } + // Subscribe to all closed groups + val allClosedGroupPublicKeys = DatabaseFactory.getSSKDatabase(context).getAllClosedGroupPublicKeys() + allClosedGroupPublicKeys.forEach { closedGroup -> + performOperation(context, ClosedGroupOperation.Subscribe, closedGroup, publicKey) + } + } + + fun performOperation(context: Context, operation: ClosedGroupOperation, closedGroupPublicKey: String, publicKey: String) { + if (!TextSecurePreferences.isUsingFCM(context)) { return } + val parameters = mapOf( "closedGroupPublicKey" to closedGroupPublicKey, "pubKey" to publicKey ) + val url = "$server/${operation.rawValue}" + val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val request = Request.Builder().url(url).post(body) + retryIfNeeded(maxRetryCount) { + OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json -> + val code = json["code"] as? Int + if (code == null || code == 0) { + Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.") + } + }.fail { exception -> + Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${exception}.") + } + } + } +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt new file mode 100644 index 0000000000..8f881e8ad1 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/DotNetAPI.kt @@ -0,0 +1,268 @@ +package org.session.libsession.messaging.utilities + +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map +import nl.komponents.kovenant.then +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.Request +import okhttp3.RequestBody + +import org.session.libsession.messaging.Configuration +import org.session.libsession.snode.OnionRequestAPI +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.messaging.fileserver.FileServerAPI + +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.libsignal.loki.DiffieHellman +import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream +import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException +import org.session.libsignal.service.api.push.exceptions.PushNetworkException +import org.session.libsignal.service.api.util.StreamDetails +import org.session.libsignal.service.internal.push.ProfileAvatarData +import org.session.libsignal.service.internal.push.PushAttachmentData +import org.session.libsignal.service.internal.push.http.DigestingRequestBody +import org.session.libsignal.service.internal.push.http.ProfileCipherOutputStreamFactory +import org.session.libsignal.service.internal.util.Base64 +import org.session.libsignal.service.internal.util.Hex +import org.session.libsignal.service.internal.util.JsonUtil +import org.session.libsignal.service.loki.api.utilities.HTTP +import org.session.libsignal.service.loki.utilities.* +import java.util.* + +/** + * Base class that provides utilities for .NET based APIs. + */ +open class DotNetAPI { + + internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH } + + // Error + internal sealed class Error(val description: String) : Exception() { + object Generic : Error("An error occurred.") + object InvalidURL : Error("Invalid URL.") + object ParsingFailed : Error("Invalid file server response.") + object SigningFailed : Error("Couldn't sign message.") + object EncryptionFailed : Error("Couldn't encrypt file.") + object DecryptionFailed : Error("Couldn't decrypt file.") + object MaxFileSizeExceeded : Error("Maximum file size exceeded.") + object TokenExpired: Error("Token expired.") // Session Android + } + + companion object { + private val authTokenRequestCache = hashMapOf>() + } + + public data class UploadResult(val id: Long, val url: String, val digest: ByteArray?) + + public fun getAuthToken(server: String): Promise { + val storage = Configuration.shared.storage + val token = storage.getAuthToken(server) + if (token != null) { return Promise.of(token) } + // Avoid multiple token requests to the server by caching + var promise = authTokenRequestCache[server] + if (promise == null) { + promise = requestNewAuthToken(server).bind { submitAuthToken(it, server) }.then { newToken -> + storage.setAuthToken(server, newToken) + newToken + }.always { + authTokenRequestCache.remove(server) + } + authTokenRequestCache[server] = promise + } + return promise + } + + private fun requestNewAuthToken(server: String): Promise { + Log.d("Loki", "Requesting auth token for server: $server.") + val userKeyPair = Configuration.shared.storage.getUserKeyPair() ?: throw Error.Generic + val parameters: Map = mapOf( "pubKey" to userKeyPair.hexEncodedPublicKey ) + return execute(HTTPVerb.GET, server, "loki/v1/get_challenge", false, parameters).map(SnodeAPI.sharedContext) { json -> + try { + val base64EncodedChallenge = json["cipherText64"] as String + val challenge = Base64.decode(base64EncodedChallenge) + val base64EncodedServerPublicKey = json["serverPubKey64"] as String + var serverPublicKey = Base64.decode(base64EncodedServerPublicKey) + // Discard the "05" prefix if needed + if (serverPublicKey.count() == 33) { + val hexEncodedServerPublicKey = Hex.toStringCondensed(serverPublicKey) + serverPublicKey = Hex.fromStringCondensed(hexEncodedServerPublicKey.removing05PrefixIfNeeded()) + } + // The challenge is prefixed by the 16 bit IV + val tokenAsData = DiffieHellman.decrypt(challenge, serverPublicKey, userKeyPair.privateKey.serialize()) + val token = tokenAsData.toString(Charsets.UTF_8) + token + } catch (exception: Exception) { + Log.d("Loki", "Couldn't parse auth token for server: $server.") + throw exception + } + } + } + + private fun submitAuthToken(token: String, server: String): Promise { + Log.d("Loki", "Submitting auth token for server: $server.") + val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.Generic + val parameters = mapOf( "pubKey" to userPublicKey, "token" to token ) + return execute(HTTPVerb.POST, server, "loki/v1/submit_challenge", false, parameters, isJSONRequired = false).map { token } + } + + internal fun execute(verb: HTTPVerb, server: String, endpoint: String, isAuthRequired: Boolean = true, parameters: Map = mapOf(), isJSONRequired: Boolean = true): Promise, Exception> { + fun execute(token: String?): Promise, Exception> { + val sanitizedEndpoint = endpoint.removePrefix("/") + var url = "$server/$sanitizedEndpoint" + if (verb == HTTPVerb.GET || verb == HTTPVerb.DELETE) { + val queryParameters = parameters.map { "${it.key}=${it.value}" }.joinToString("&") + if (queryParameters.isNotEmpty()) { url += "?$queryParameters" } + } + var request = Request.Builder().url(url) + if (isAuthRequired) { + if (token == null) { throw IllegalStateException() } + request = request.header("Authorization", "Bearer $token") + } + when (verb) { + HTTPVerb.GET -> request = request.get() + HTTPVerb.DELETE -> request = request.delete() + else -> { + val parametersAsJSON = JsonUtil.toJson(parameters) + val body = RequestBody.create(MediaType.get("application/json"), parametersAsJSON) + when (verb) { + HTTPVerb.PUT -> request = request.put(body) + HTTPVerb.POST -> request = request.post(body) + HTTPVerb.PATCH -> request = request.patch(body) + else -> throw IllegalStateException() + } + } + } + val serverPublicKeyPromise = if (server == FileServerAPI.shared.server) Promise.of(FileServerAPI.fileServerPublicKey) + else FileServerAPI.shared.getPublicKeyForOpenGroupServer(server) + return serverPublicKeyPromise.bind { serverPublicKey -> + OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, isJSONRequired = isJSONRequired).recover { exception -> + if (exception is HTTP.HTTPRequestFailedException) { + val statusCode = exception.statusCode + if (statusCode == 401 || statusCode == 403) { + Configuration.shared.storage.setAuthToken(server, null) + throw Error.TokenExpired + } + } + throw exception + } + } + } + return if (isAuthRequired) { + getAuthToken(server).bind { execute(it) } + } else { + execute(null) + } + } + + internal fun getUserProfiles(publicKeys: Set, server: String, includeAnnotations: Boolean): Promise>, Exception> { + val parameters = mapOf( "include_user_annotations" to includeAnnotations.toInt(), "ids" to publicKeys.joinToString { "@$it" } ) + return execute(HTTPVerb.GET, server, "users", parameters = parameters).map { json -> + val data = json["data"] as? List> + if (data == null) { + Log.d("Loki", "Couldn't parse user profiles for: $publicKeys from: $json.") + throw Error.ParsingFailed + } + data!! // For some reason the compiler can't infer that this can't be null at this point + } + } + + internal fun setSelfAnnotation(server: String, type: String, newValue: Any?): Promise, Exception> { + val annotation = mutableMapOf( "type" to type ) + if (newValue != null) { annotation["value"] = newValue } + val parameters = mapOf( "annotations" to listOf( annotation ) ) + return execute(HTTPVerb.PATCH, server, "users/me", parameters = parameters) + } + + @Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class) + fun uploadAttachment(server: String, attachment: PushAttachmentData): UploadResult { + // This function mimics what Signal does in PushServiceSocket + val contentType = "application/octet-stream" + val file = DigestingRequestBody(attachment.data, attachment.outputStreamFactory, contentType, attachment.dataSize, attachment.listener) + Log.d("Loki", "File size: ${attachment.dataSize} bytes.") + val body = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("type", "network.loki") + .addFormDataPart("Content-Type", contentType) + .addFormDataPart("content", UUID.randomUUID().toString(), file) + .build() + val request = Request.Builder().url("$server/files").post(body) + return upload(server, request) { json -> // Retrying is handled by AttachmentUploadJob + val data = json["data"] as? Map<*, *> + if (data == null) { + Log.d("Loki", "Couldn't parse attachment from: $json.") + throw Error.ParsingFailed + } + val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong() + val url = data["url"] as? String + if (id == null || url == null || url.isEmpty()) { + Log.d("Loki", "Couldn't parse upload from: $json.") + throw Error.ParsingFailed + } + UploadResult(id, url, file.transmittedDigest) + }.get() + } + + @Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class) + fun uploadProfilePicture(server: String, key: ByteArray, profilePicture: StreamDetails, setLastProfilePictureUpload: () -> Unit): UploadResult { + val profilePictureUploadData = ProfileAvatarData(profilePicture.stream, ProfileCipherOutputStream.getCiphertextLength(profilePicture.length), profilePicture.contentType, ProfileCipherOutputStreamFactory(key)) + val file = DigestingRequestBody(profilePictureUploadData.data, profilePictureUploadData.outputStreamFactory, + profilePictureUploadData.contentType, profilePictureUploadData.dataLength, null) + val body = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("type", "network.loki") + .addFormDataPart("Content-Type", "application/octet-stream") + .addFormDataPart("content", UUID.randomUUID().toString(), file) + .build() + val request = Request.Builder().url("$server/files").post(body) + return retryIfNeeded(4) { + upload(server, request) { json -> + val data = json["data"] as? Map<*, *> + if (data == null) { + Log.d("Loki", "Couldn't parse profile picture from: $json.") + throw Error.ParsingFailed + } + val id = data["id"] as? Long ?: (data["id"] as? Int)?.toLong() ?: (data["id"] as? String)?.toLong() + val url = data["url"] as? String + if (id == null || url == null || url.isEmpty()) { + Log.d("Loki", "Couldn't parse profile picture from: $json.") + throw Error.ParsingFailed + } + setLastProfilePictureUpload() + UploadResult(id, url, file.transmittedDigest) + } + }.get() + } + + @Throws(PushNetworkException::class, NonSuccessfulResponseCodeException::class) + private fun upload(server: String, request: Request.Builder, parse: (Map<*, *>) -> UploadResult): Promise { + val promise: Promise, Exception> + if (server == FileServerAPI.shared.server) { + request.addHeader("Authorization", "Bearer loki") + // Uploads to the Loki File Server shouldn't include any personally identifiable information, so use a dummy auth token + promise = OnionRequestAPI.sendOnionRequest(request.build(), FileServerAPI.shared.server, FileServerAPI.fileServerPublicKey) + } else { + promise = FileServerAPI.shared.getPublicKeyForOpenGroupServer(server).bind { openGroupServerPublicKey -> + getAuthToken(server).bind { token -> + request.addHeader("Authorization", "Bearer $token") + OnionRequestAPI.sendOnionRequest(request.build(), server, openGroupServerPublicKey) + } + } + } + return promise.map { json -> + parse(json) + }.recover { exception -> + if (exception is HTTP.HTTPRequestFailedException) { + val statusCode = exception.statusCode + if (statusCode == 401 || statusCode == 403) { + Configuration.shared.storage.setAuthToken(server, null) + } + throw NonSuccessfulResponseCodeException("Request returned with status code ${exception.statusCode}.") + } + throw PushNetworkException(exception) + } + } +} + +private fun Boolean.toInt(): Int { return if (this) 1 else 0 } diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt new file mode 100644 index 0000000000..8af6ddf5bc --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt @@ -0,0 +1,85 @@ +package org.session.libsession.messaging.utilities + +import com.google.protobuf.ByteString +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope +import org.session.libsignal.service.internal.util.Base64 +import org.session.libsignal.service.internal.websocket.WebSocketProtos.WebSocketMessage +import org.session.libsignal.service.internal.websocket.WebSocketProtos.WebSocketRequestMessage +import java.security.SecureRandom + +object MessageWrapper { + + // region Types + sealed class Error(val description: String) : Exception() { + object FailedToWrapData : Error("Failed to wrap data.") + object FailedToWrapMessageInEnvelope : Error("Failed to wrap message in envelope.") + object FailedToWrapEnvelopeInWebSocketMessage : Error("Failed to wrap envelope in web socket message.") + object FailedToUnwrapData : Error("Failed to unwrap data.") + } + // endregion + + // region Wrapping + /** + * Wraps `message` in a `SignalServiceProtos.Envelope` and then a `WebSocketProtos.WebSocketMessage` to match the desktop application. + */ + fun wrap(type: Envelope.Type, timestamp: Long, senderPublicKey: String, content: ByteArray): ByteArray { + try { + val envelope = createEnvelope(type, timestamp, senderPublicKey, content) + val webSocketMessage = createWebSocketMessage(envelope) + return webSocketMessage.toByteArray() + } catch (e: Exception) { + throw if (e is Error) { e } else { Error.FailedToWrapData } + } + } + + private fun createEnvelope(type: Envelope.Type, timestamp: Long, senderPublicKey: String, content: ByteArray): Envelope { + try { + val builder = Envelope.newBuilder() + builder.type = type + builder.timestamp = timestamp + builder.source = senderPublicKey + builder.sourceDevice = 1 + builder.content = ByteString.copyFrom(content) + return builder.build() + } catch (e: Exception) { + Log.d("Loki", "Failed to wrap message in envelope: ${e.message}.") + throw Error.FailedToWrapMessageInEnvelope + } + } + + private fun createWebSocketMessage(envelope: Envelope): WebSocketMessage { + try { + val requestBuilder = WebSocketRequestMessage.newBuilder() + requestBuilder.verb = "PUT" + requestBuilder.path = "/api/v1/message" + requestBuilder.id = SecureRandom.getInstance("SHA1PRNG").nextLong() + requestBuilder.body = envelope.toByteString() + val messageBuilder = WebSocketMessage.newBuilder() + messageBuilder.request = requestBuilder.build() + messageBuilder.type = WebSocketMessage.Type.REQUEST + return messageBuilder.build() + } catch (e: Exception) { + Log.d("Loki", "Failed to wrap envelope in web socket message: ${e.message}.") + throw Error.FailedToWrapEnvelopeInWebSocketMessage + } + } + // endregion + + // region Unwrapping + /** + * `data` shouldn't be base 64 encoded. + */ + fun unwrap(data: ByteArray): Envelope { + try { + val webSocketMessage = WebSocketMessage.parseFrom(data) + val envelopeAsData = webSocketMessage.request.body + return Envelope.parseFrom(envelopeAsData) + } catch (e: Exception) { + Log.d("Loki", "Failed to unwrap data: ${e.message}.") + throw Error.FailedToUnwrapData + } + } + // endregion +} diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.java b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.java new file mode 100644 index 0000000000..c66a9b4954 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.java @@ -0,0 +1,121 @@ +package org.session.libsession.messaging.utilities; + + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import org.session.libsignal.libsignal.util.guava.Optional; +import org.session.libsignal.metadata.SignalProtos; +import org.session.libsignal.metadata.certificate.CertificateValidator; +import org.session.libsignal.metadata.certificate.InvalidCertificateException; +import org.session.libsignal.service.api.crypto.UnidentifiedAccess; +import org.session.libsignal.service.api.crypto.UnidentifiedAccessPair; +import org.session.libsignal.service.api.push.SignalServiceAddress; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; + +public class UnidentifiedAccessUtil { + + private static final String TAG = UnidentifiedAccessUtil.class.getSimpleName(); + + public static CertificateValidator getCertificateValidator() { + return new CertificateValidator(); + } + + @WorkerThread + public static Optional getAccessFor(@NonNull Context context, + @NonNull Recipient recipient) + { + if (!TextSecurePreferences.isUnidentifiedDeliveryEnabled(context)) { + Log.i(TAG, "Unidentified delivery is disabled. [other]"); + return Optional.absent(); + } + + try { + byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); + byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context); + byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context); + + if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { + ourUnidentifiedAccessKey = Util.getSecretBytes(16); + } + + Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) + + " | Our access key present? " + (ourUnidentifiedAccessKey != null) + + " | Our certificate present? " + (ourUnidentifiedAccessCertificate != null)); + + if (theirUnidentifiedAccessKey != null && + ourUnidentifiedAccessKey != null && + ourUnidentifiedAccessCertificate != null) + { + return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey, + ourUnidentifiedAccessCertificate), + new UnidentifiedAccess(ourUnidentifiedAccessKey, + ourUnidentifiedAccessCertificate))); + } + + return Optional.absent(); + } catch (InvalidCertificateException e) { + Log.w(TAG, e); + return Optional.absent(); + } + } + + public static Optional getAccessForSync(@NonNull Context context) { + if (!TextSecurePreferences.isUnidentifiedDeliveryEnabled(context)) { + Log.i(TAG, "Unidentified delivery is disabled. [self]"); + return Optional.absent(); + } + + try { + byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context); + byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context); + + if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { + ourUnidentifiedAccessKey = Util.getSecretBytes(16); + } + + if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) { + return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(ourUnidentifiedAccessKey, + ourUnidentifiedAccessCertificate), + new UnidentifiedAccess(ourUnidentifiedAccessKey, + ourUnidentifiedAccessCertificate))); + } + + return Optional.absent(); + } catch (InvalidCertificateException e) { + Log.w(TAG, e); + return Optional.absent(); + } + } + + public static @NonNull byte[] getSelfUnidentifiedAccessKey(@NonNull Context context) { + return UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getProfileKey(context)); + } + + private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) { + byte[] theirProfileKey = recipient.resolve().getProfileKey(); + + if (theirProfileKey == null) return Util.getSecretBytes(16); + else return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); + + } + + private static @Nullable byte[] getUnidentifiedAccessCertificate(Context context) { + String ourNumber = TextSecurePreferences.getLocalNumber(context); + if (ourNumber != null) { + SignalProtos.SenderCertificate certificate = SignalProtos.SenderCertificate.newBuilder() + .setSender(ourNumber) + .setSenderDevice(SignalServiceAddress.DEFAULT_DEVICE_ID) + .build(); + return certificate.toByteArray(); + } + + return null; + } +} diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt new file mode 100644 index 0000000000..4d12bde107 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -0,0 +1,464 @@ +package org.session.libsession.snode + +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.all +import nl.komponents.kovenant.deferred +import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map +import okhttp3.Request +import org.session.libsession.utilities.AESGCM +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.util.Base64 +import org.session.libsignal.service.internal.util.JsonUtil +import org.session.libsignal.service.loki.api.* +import org.session.libsignal.service.loki.api.fileserver.FileServerAPI +import org.session.libsignal.service.loki.api.utilities.* +import org.session.libsession.utilities.AESGCM.EncryptionResult +import org.session.libsession.utilities.getBodyForOnionRequest +import org.session.libsession.utilities.getHeadersForOnionRequest +import org.session.libsignal.service.loki.utilities.* + +private typealias Path = List + +/** + * See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information. + */ +public object OnionRequestAPI { + private val pathFailureCount = mutableMapOf() + private val snodeFailureCount = mutableMapOf() + public var guardSnodes = setOf() + public var paths: List // Not a set to ensure we consistently show the same path to the user + get() = SnodeAPI.database.getOnionRequestPaths() + set(newValue) { + if (newValue.isEmpty()) { + SnodeAPI.database.clearOnionRequestPaths() + } else { + SnodeAPI.database.setOnionRequestPaths(newValue) + } + } + + // region Settings + /** + * The number of snodes (including the guard snode) in a path. + */ + private val pathSize = 3 + /** + * The number of times a path can fail before it's replaced. + */ + private val pathFailureThreshold = 2 + /** + * The number of times a snode can fail before it's replaced. + */ + private val snodeFailureThreshold = 2 + /** + * The number of paths to maintain. + */ + public val targetPathCount = 2 // A main path and a backup path for the case where the target snode is in the main path + + /** + * The number of guard snodes required to maintain `targetPathCount` paths. + */ + private val targetGuardSnodeCount + get() = targetPathCount // One per path + // endregion + + class HTTPRequestFailedAtDestinationException(val statusCode: Int, val json: Map<*, *>) + : Exception("HTTP request failed at destination with status code $statusCode.") + class InsufficientSnodesException : Exception("Couldn't find enough snodes to build a path.") + + private data class OnionBuildingResult( + internal val guardSnode: Snode, + internal val finalEncryptionResult: EncryptionResult, + internal val destinationSymmetricKey: ByteArray + ) + + internal sealed class Destination { + class Snode(val snode: org.session.libsession.snode.Snode) : Destination() + class Server(val host: String, val target: String, val x25519PublicKey: String) : Destination() + } + + // region Private API + /** + * Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise. + */ + private fun testSnode(snode: Snode): Promise { + val deferred = deferred() + Thread { // No need to block the shared context for this + val url = "${snode.address}:${snode.port}/get_stats/v1" + try { + val json = HTTP.execute(HTTP.Verb.GET, url) + val version = json["version"] as? String + if (version == null) { deferred.reject(Exception("Missing snode version.")); return@Thread } + if (version >= "2.0.7") { + deferred.resolve(Unit) + } else { + val message = "Unsupported snode version: $version." + Log.d("Loki", message) + deferred.reject(Exception(message)) + } + } catch (exception: Exception) { + deferred.reject(exception) + } + }.start() + return deferred.promise + } + + /** + * Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out if not + * enough (reliable) snodes are available. + */ + private fun getGuardSnodes(reusableGuardSnodes: List): Promise, Exception> { + if (guardSnodes.count() >= targetGuardSnodeCount) { + return Promise.of(guardSnodes) + } else { + Log.d("Loki", "Populating guard snode cache.") + return SnodeAPI.getRandomSnode().bind(SnodeAPI.sharedContext) { // Just used to populate the snode pool + var unusedSnodes = SnodeAPI.snodePool.minus(reusableGuardSnodes) + val reusableGuardSnodeCount = reusableGuardSnodes.count() + if (unusedSnodes.count() < (targetGuardSnodeCount - reusableGuardSnodeCount)) { throw InsufficientSnodesException() } + fun getGuardSnode(): Promise { + val candidate = unusedSnodes.getRandomElementOrNull() + ?: return Promise.ofFail(InsufficientSnodesException()) + unusedSnodes = unusedSnodes.minus(candidate) + Log.d("Loki", "Testing guard snode: $candidate.") + // Loop until a reliable guard snode is found + val deferred = deferred() + testSnode(candidate).success { + deferred.resolve(candidate) + }.fail { + getGuardSnode().success { + deferred.resolve(candidate) + }.fail { exception -> + if (exception is InsufficientSnodesException) { + deferred.reject(exception) + } + } + } + return deferred.promise + } + val promises = (0 until (targetGuardSnodeCount - reusableGuardSnodeCount)).map { getGuardSnode() } + all(promises).map(SnodeAPI.sharedContext) { guardSnodes -> + val guardSnodesAsSet = (guardSnodes + reusableGuardSnodes).toSet() + OnionRequestAPI.guardSnodes = guardSnodesAsSet + guardSnodesAsSet + } + } + } + } + + /** + * Builds and returns `targetPathCount` paths. The returned promise errors out if not + * enough (reliable) snodes are available. + */ + private fun buildPaths(reusablePaths: List): Promise, Exception> { + Log.d("Loki", "Building onion request paths.") + SnodeAPI.broadcaster.broadcast("buildingPaths") + return SnodeAPI.getRandomSnode().bind(SnodeAPI.sharedContext) { // Just used to populate the snode pool + val reusableGuardSnodes = reusablePaths.map { it[0] } + getGuardSnodes(reusableGuardSnodes).map(SnodeAPI.sharedContext) { guardSnodes -> + var unusedSnodes = SnodeAPI.snodePool.minus(guardSnodes).minus(reusablePaths.flatten()) + val reusableGuardSnodeCount = reusableGuardSnodes.count() + val pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount) + if (unusedSnodes.count() < pathSnodeCount) { throw InsufficientSnodesException() } + // Don't test path snodes as this would reveal the user's IP to them + guardSnodes.minus(reusableGuardSnodes).map { guardSnode -> + val result = listOf( guardSnode ) + (0 until (pathSize - 1)).map { + val pathSnode = unusedSnodes.getRandomElement() + unusedSnodes = unusedSnodes.minus(pathSnode) + pathSnode + } + Log.d("Loki", "Built new onion request path: $result.") + result + } + }.map { paths -> + OnionRequestAPI.paths = paths + reusablePaths + SnodeAPI.broadcaster.broadcast("pathsBuilt") + paths + } + } + } + + /** + * Returns a `Path` to be used for building an onion request. Builds new paths as needed. + */ + private fun getPath(snodeToExclude: Snode?): Promise { + if (pathSize < 1) { throw Exception("Can't build path of size zero.") } + val paths = this.paths + val guardSnodes = mutableSetOf() + if (paths.isNotEmpty()) { + guardSnodes.add(paths[0][0]) + if (paths.count() >= 2) { + guardSnodes.add(paths[1][0]) + } + } + OnionRequestAPI.guardSnodes = guardSnodes + fun getPath(paths: List): Path { + if (snodeToExclude != null) { + return paths.filter { !it.contains(snodeToExclude) }.getRandomElement() + } else { + return paths.getRandomElement() + } + } + if (paths.count() >= targetPathCount) { + return Promise.of(getPath(paths)) + } else if (paths.isNotEmpty()) { + if (paths.any { !it.contains(snodeToExclude) }) { + buildPaths(paths) // Re-build paths in the background + return Promise.of(getPath(paths)) + } else { + return buildPaths(paths).map(SnodeAPI.sharedContext) { newPaths -> + getPath(newPaths) + } + } + } else { + return buildPaths(listOf()).map(SnodeAPI.sharedContext) { newPaths -> + getPath(newPaths) + } + } + } + + private fun dropGuardSnode(snode: Snode) { + guardSnodes = guardSnodes.filter { it != snode }.toSet() + } + + private fun dropSnode(snode: Snode) { + // We repair the path here because we can do it sync. In the case where we drop a whole + // path we leave the re-building up to getPath() because re-building the path in that case + // is async. + snodeFailureCount[snode] = 0 + val oldPaths = paths.toMutableList() + val pathIndex = oldPaths.indexOfFirst { it.contains(snode) } + if (pathIndex == -1) { return } + val path = oldPaths[pathIndex].toMutableList() + val snodeIndex = path.indexOf(snode) + if (snodeIndex == -1) { return } + path.removeAt(snodeIndex) + val unusedSnodes = SnodeAPI.snodePool.minus(oldPaths.flatten()) + if (unusedSnodes.isEmpty()) { throw InsufficientSnodesException() } + path.add(unusedSnodes.getRandomElement()) + // Don't test the new snode as this would reveal the user's IP + oldPaths.removeAt(pathIndex) + val newPaths = oldPaths + listOf( path ) + paths = newPaths + } + + private fun dropPath(path: Path) { + pathFailureCount[path] = 0 + val paths = OnionRequestAPI.paths.toMutableList() + val pathIndex = paths.indexOf(path) + if (pathIndex == -1) { return } + paths.removeAt(pathIndex) + OnionRequestAPI.paths = paths + } + + /** + * Builds an onion around `payload` and returns the result. + */ + private fun buildOnionForDestination(payload: Map<*, *>, destination: Destination): Promise { + lateinit var guardSnode: Snode + lateinit var destinationSymmetricKey: ByteArray // Needed by LokiAPI to decrypt the response sent back by the destination + lateinit var encryptionResult: EncryptionResult + val snodeToExclude = when (destination) { + is Destination.Snode -> destination.snode + is Destination.Server -> null + } + return getPath(snodeToExclude).bind(SnodeAPI.sharedContext) { path -> + guardSnode = path.first() + // Encrypt in reverse order, i.e. the destination first + OnionRequestEncryption.encryptPayloadForDestination(payload, destination).bind(SnodeAPI.sharedContext) { r -> + destinationSymmetricKey = r.symmetricKey + // Recursively encrypt the layers of the onion (again in reverse order) + encryptionResult = r + @Suppress("NAME_SHADOWING") var path = path + var rhs = destination + fun addLayer(): Promise { + if (path.isEmpty()) { + return Promise.of(encryptionResult) + } else { + val lhs = Destination.Snode(path.last()) + path = path.dropLast(1) + return OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind(SnodeAPI.sharedContext) { r -> + encryptionResult = r + rhs = lhs + addLayer() + } + } + } + addLayer() + } + }.map(SnodeAPI.sharedContext) { OnionBuildingResult(guardSnode, encryptionResult, destinationSymmetricKey) } + } + + /** + * Sends an onion request to `destination`. Builds new paths as needed. + */ + private fun sendOnionRequest(destination: Destination, payload: Map<*, *>, isJSONRequired: Boolean = true): Promise, Exception> { + val deferred = deferred, Exception>() + lateinit var guardSnode: Snode + buildOnionForDestination(payload, destination).success { result -> + guardSnode = result.guardSnode + val url = "${guardSnode.address}:${guardSnode.port}/onion_req/v2" + val finalEncryptionResult = result.finalEncryptionResult + val onion = finalEncryptionResult.ciphertext + if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerAPI.maxFileSize.toDouble()) { + Log.d("Loki", "Approaching request size limit: ~${onion.count()} bytes.") + } + @Suppress("NAME_SHADOWING") val parameters = mapOf( + "ephemeral_key" to finalEncryptionResult.ephemeralPublicKey.toHexString() + ) + val body: ByteArray + try { + body = OnionRequestEncryption.encode(onion, parameters) + } catch (exception: Exception) { + return@success deferred.reject(exception) + } + val destinationSymmetricKey = result.destinationSymmetricKey + Thread { + try { + val json = HTTP.execute(HTTP.Verb.POST, url, body) + val base64EncodedIVAndCiphertext = json["result"] as? String ?: return@Thread deferred.reject(Exception("Invalid JSON")) + val ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext) + try { + val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey) + try { + @Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java) + val statusCode = json["status"] as Int + if (statusCode == 406) { + @Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." ) + val exception = HTTPRequestFailedAtDestinationException(statusCode, body) + return@Thread deferred.reject(exception) + } else if (json["body"] != null) { + @Suppress("NAME_SHADOWING") val body: Map<*, *> + if (json["body"] is Map<*, *>) { + body = json["body"] as Map<*, *> + } else { + val bodyAsString = json["body"] as String + if (!isJSONRequired) { + body = mapOf( "result" to bodyAsString ) + } else { + body = JsonUtil.fromJson(bodyAsString, Map::class.java) + } + } + if (statusCode != 200) { + val exception = HTTPRequestFailedAtDestinationException(statusCode, body) + return@Thread deferred.reject(exception) + } + deferred.resolve(body) + } else { + if (statusCode != 200) { + val exception = HTTPRequestFailedAtDestinationException(statusCode, json) + return@Thread deferred.reject(exception) + } + deferred.resolve(json) + } + } catch (exception: Exception) { + deferred.reject(Exception("Invalid JSON: ${plaintext.toString(Charsets.UTF_8)}.")) + } + } catch (exception: Exception) { + deferred.reject(exception) + } + } catch (exception: Exception) { + deferred.reject(exception) + } + }.start() + }.fail { exception -> + deferred.reject(exception) + } + val promise = deferred.promise + promise.fail { exception -> + val path = paths.firstOrNull { it.contains(guardSnode) } + if (exception is HTTP.HTTPRequestFailedException) { + fun handleUnspecificError() { + if (path == null) { return } + var pathFailureCount = OnionRequestAPI.pathFailureCount[path] ?: 0 + pathFailureCount += 1 + if (pathFailureCount >= pathFailureThreshold) { + dropGuardSnode(guardSnode) + path.forEach { snode -> + @Suppress("ThrowableNotThrown") + SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, null) // Intentionally don't throw + } + dropPath(path) + } else { + OnionRequestAPI.pathFailureCount[path] = pathFailureCount + } + } + val json = exception.json + val message = json?.get("result") as? String + val prefix = "Next node not found: " + if (message != null && message.startsWith(prefix)) { + val ed25519PublicKey = message.substringAfter(prefix) + val snode = path?.firstOrNull { it.publicKeySet!!.ed25519Key == ed25519PublicKey } + if (snode != null) { + var snodeFailureCount = OnionRequestAPI.snodeFailureCount[snode] ?: 0 + snodeFailureCount += 1 + if (snodeFailureCount >= snodeFailureThreshold) { + @Suppress("ThrowableNotThrown") + SnodeAPI.handleSnodeError(exception.statusCode, json, snode, null) // Intentionally don't throw + try { + dropSnode(snode) + } catch (exception: Exception) { + handleUnspecificError() + } + } else { + OnionRequestAPI.snodeFailureCount[snode] = snodeFailureCount + } + } else { + handleUnspecificError() + } + } else if (message == "Loki Server error") { + // Do nothing + } else { + handleUnspecificError() + } + } + } + return promise + } + // endregion + + // region Internal API + /** + * Sends an onion request to `snode`. Builds new paths as needed. + */ + internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, publicKey: String): Promise, Exception> { + val payload = mapOf( "method" to method.rawValue, "params" to parameters ) + return sendOnionRequest(Destination.Snode(snode), payload).recover { exception -> + val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException + if (httpRequestFailedException != null) { + val error = SnodeAPI.handleSnodeError(httpRequestFailedException.statusCode, httpRequestFailedException.json, snode, publicKey) + if (error != null) { throw error } + } + throw exception + } + } + + /** + * Sends an onion request to `server`. Builds new paths as needed. + * + * `publicKey` is the hex encoded public key of the user the call is associated with. This is needed for swarm cache maintenance. + */ + public fun sendOnionRequest(request: Request, server: String, x25519PublicKey: String, target: String = "/loki/v3/lsrpc", isJSONRequired: Boolean = true): Promise, Exception> { + val headers = request.getHeadersForOnionRequest() + val url = request.url() + val urlAsString = url.toString() + val host = url.host() + val endpoint = when { + server.count() < urlAsString.count() -> urlAsString.substringAfter("$server/") + else -> "" + } + val body = request.getBodyForOnionRequest() ?: "null" + val payload = mapOf( + "body" to body, + "endpoint" to endpoint, + "method" to request.method(), + "headers" to headers + ) + val destination = Destination.Server(host, target, x25519PublicKey) + return sendOnionRequest(destination, payload, isJSONRequired).recover { exception -> + Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.") + throw exception + } + } + // endregion +} diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestEncryption.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestEncryption.kt new file mode 100644 index 0000000000..ed907e5b08 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestEncryption.kt @@ -0,0 +1,94 @@ +package org.session.libsession.snode + +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.deferred +import org.session.libsignal.service.internal.util.JsonUtil +import org.session.libsession.utilities.AESGCM.EncryptionResult +import org.session.libsession.utilities.AESGCM +import org.session.libsignal.service.loki.utilities.toHexString +import java.nio.Buffer +import java.nio.ByteBuffer +import java.nio.ByteOrder + +object OnionRequestEncryption { + + internal fun encode(ciphertext: ByteArray, json: Map<*, *>): ByteArray { + // The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 | + val jsonAsData = JsonUtil.toJson(json).toByteArray() + val ciphertextSize = ciphertext.size + val buffer = ByteBuffer.allocate(Int.SIZE_BYTES) + buffer.order(ByteOrder.LITTLE_ENDIAN) + buffer.putInt(ciphertextSize) + val ciphertextSizeAsData = ByteArray(buffer.capacity()) + // Casting here avoids an issue where this gets compiled down to incorrect byte code. See + // https://github.com/eclipse/jetty.project/issues/3244 for more info + (buffer as Buffer).position(0) + buffer.get(ciphertextSizeAsData) + return ciphertextSizeAsData + ciphertext + jsonAsData + } + + /** + * Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request. + */ + internal fun encryptPayloadForDestination(payload: Map<*, *>, destination: OnionRequestAPI.Destination): Promise { + val deferred = deferred() + Thread { + try { + // Wrapping isn't needed for file server or open group onion requests + when (destination) { + is OnionRequestAPI.Destination.Snode -> { + val snodeX25519PublicKey = destination.snode.publicKeySet!!.x25519Key + val payloadAsData = JsonUtil.toJson(payload).toByteArray() + val plaintext = encode(payloadAsData, mapOf( "headers" to "" )) + val result = AESGCM.encrypt(plaintext, snodeX25519PublicKey) + deferred.resolve(result) + } + is OnionRequestAPI.Destination.Server -> { + val plaintext = JsonUtil.toJson(payload).toByteArray() + val result = AESGCM.encrypt(plaintext, destination.x25519PublicKey) + deferred.resolve(result) + } + } + } catch (exception: Exception) { + deferred.reject(exception) + } + }.start() + return deferred.promise + } + + /** + * Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request. + */ + internal fun encryptHop(lhs: OnionRequestAPI.Destination, rhs: OnionRequestAPI.Destination, previousEncryptionResult: EncryptionResult): Promise { + val deferred = deferred() + Thread { + try { + val payload: MutableMap + when (rhs) { + is OnionRequestAPI.Destination.Snode -> { + payload = mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key ) + } + is OnionRequestAPI.Destination.Server -> { + payload = mutableMapOf( "host" to rhs.host, "target" to rhs.target, "method" to "POST" ) + } + } + payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() + val x25519PublicKey: String + when (lhs) { + is OnionRequestAPI.Destination.Snode -> { + x25519PublicKey = lhs.snode.publicKeySet!!.x25519Key + } + is OnionRequestAPI.Destination.Server -> { + x25519PublicKey = lhs.x25519PublicKey + } + } + val plaintext = encode(previousEncryptionResult.ciphertext, payload) + val result = AESGCM.encrypt(plaintext, x25519PublicKey) + deferred.resolve(result) + } catch (exception: Exception) { + deferred.reject(exception) + } + }.start() + return deferred.promise + } +} diff --git a/libsession/src/main/java/org/session/libsession/snode/Snode.kt b/libsession/src/main/java/org/session/libsession/snode/Snode.kt new file mode 100644 index 0000000000..8fd05c5f01 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/Snode.kt @@ -0,0 +1,34 @@ +package org.session.libsession.snode + +public class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) { + + val ip: String get() = address.removePrefix("https://") + + internal enum class Method(val rawValue: String) { + /** + * Only supported by snode targets. + */ + GetSwarm("get_snodes_for_pubkey"), + /** + * Only supported by snode targets. + */ + GetMessages("retrieve"), + SendMessage("store") + } + + data class KeySet(val ed25519Key: String, val x25519Key: String) + + override fun equals(other: Any?): Boolean { + return if (other is Snode) { + address == other.address && port == other.port + } else { + false + } + } + + override fun hashCode(): Int { + return address.hashCode() xor port.hashCode() + } + + override fun toString(): String { return "$address:$port" } +} diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt new file mode 100644 index 0000000000..0b550936be --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -0,0 +1,370 @@ +@file:Suppress("NAME_SHADOWING") + +package org.session.libsession.snode + +import nl.komponents.kovenant.Kovenant +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.deferred +import nl.komponents.kovenant.functional.bind +import nl.komponents.kovenant.functional.map +import nl.komponents.kovenant.task + +import org.session.libsession.snode.utilities.getRandomElement + +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.internal.util.Base64 +import org.session.libsignal.service.loki.api.MessageWrapper +import org.session.libsignal.service.loki.api.utilities.HTTP +import org.session.libsignal.service.loki.utilities.createContext +import org.session.libsignal.service.loki.utilities.prettifiedDescription +import org.session.libsignal.service.loki.utilities.retryIfNeeded +import org.session.libsignal.service.internal.push.SignalServiceProtos.Envelope + +import java.security.SecureRandom + +object SnodeAPI { + val database = Configuration.shared.storage + val broadcaster = Configuration.shared.broadcaster + val sharedContext = Kovenant.createContext("LokiAPISharedContext") + val messageSendingContext = Kovenant.createContext("LokiAPIMessageSendingContext") + val messagePollingContext = Kovenant.createContext("LokiAPIMessagePollingContext") + + internal var snodeFailureCount: MutableMap = mutableMapOf() + internal var snodePool: Set + get() = database.getSnodePool() + set(newValue) { database.setSnodePool(newValue) } + + // Settings + private val maxRetryCount = 6 + private val minimumSnodePoolCount = 64 + private val minimumSwarmSnodeCount = 2 + private val seedNodePool: Set = setOf( "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ) + internal val snodeFailureThreshold = 4 + private val targetSwarmSnodeCount = 2 + + private val useOnionRequests = true + + internal var powDifficulty = 1 + + // Error + internal sealed class Error(val description: String) : Exception() { + object Generic : Error("An error occurred.") + object ClockOutOfSync : Error("The user's clock is out of sync with the service node network.") + object RandomSnodePoolUpdatingFailed : Error("Failed to update random service node pool.") + } + + // Internal API + internal fun invoke(method: Snode.Method, snode: Snode, publicKey: String, parameters: Map): RawResponsePromise { + val url = "${snode.address}:${snode.port}/storage_rpc/v1" + if (useOnionRequests) { + return OnionRequestAPI.sendOnionRequest(method, parameters, snode, publicKey) + } else { + val deferred = deferred, Exception>() + Thread { + val payload = mapOf( "method" to method.rawValue, "params" to parameters ) + try { + val json = HTTP.execute(HTTP.Verb.POST, url, payload) + deferred.resolve(json) + } catch (exception: Exception) { + val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException + if (httpRequestFailedException != null) { + val error = handleSnodeError(httpRequestFailedException.statusCode, httpRequestFailedException.json, snode, publicKey) + if (error != null) { return@Thread deferred.reject(exception) } + } + Log.d("Loki", "Unhandled exception: $exception.") + deferred.reject(exception) + } + }.start() + return deferred.promise + } + } + + internal fun getRandomSnode(): Promise { + val snodePool = this.snodePool + if (snodePool.count() < minimumSnodePoolCount) { + val target = seedNodePool.random() + val url = "$target/json_rpc" + Log.d("Loki", "Populating snode pool using: $target.") + val parameters = mapOf( + "method" to "get_n_service_nodes", + "params" to mapOf( + "active_only" to true, + "fields" to mapOf( "public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true ) + ) + ) + val deferred = deferred() + deferred(SnodeAPI.sharedContext) + Thread { + try { + val json = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true) + val intermediate = json["result"] as? Map<*, *> + val rawSnodes = intermediate?.get("service_node_states") as? List<*> + if (rawSnodes != null) { + val snodePool = rawSnodes.mapNotNull { rawSnode -> + val rawSnodeAsJSON = rawSnode as? Map<*, *> + val address = rawSnodeAsJSON?.get("public_ip") as? String + val port = rawSnodeAsJSON?.get("storage_port") as? Int + val ed25519Key = rawSnodeAsJSON?.get("pubkey_ed25519") as? String + val x25519Key = rawSnodeAsJSON?.get("pubkey_x25519") as? String + if (address != null && port != null && ed25519Key != null && x25519Key != null && address != "0.0.0.0") { + Snode("https://$address", port, Snode.KeySet(ed25519Key, x25519Key)) + } else { + Log.d("Loki", "Failed to parse: ${rawSnode?.prettifiedDescription()}.") + null + } + }.toMutableSet() + Log.d("Loki", "Persisting snode pool to database.") + this.snodePool = snodePool + try { + deferred.resolve(snodePool.getRandomElement()) + } catch (exception: Exception) { + Log.d("Loki", "Got an empty snode pool from: $target.") + deferred.reject(SnodeAPI.Error.Generic) + } + } else { + Log.d("Loki", "Failed to update snode pool from: ${(rawSnodes as List<*>?)?.prettifiedDescription()}.") + deferred.reject(SnodeAPI.Error.Generic) + } + } catch (exception: Exception) { + deferred.reject(exception) + } + }.start() + return deferred.promise + } else { + return Promise.of(snodePool.getRandomElement()) + } + } + + internal fun dropSnodeFromSwarmIfNeeded(snode: Snode, publicKey: String) { + val swarm = database.getSwarm(publicKey)?.toMutableSet() + if (swarm != null && swarm.contains(snode)) { + swarm.remove(snode) + database.setSwarm(publicKey, swarm) + } + } + + internal fun getSingleTargetSnode(publicKey: String): Promise { + // SecureRandom() should be cryptographically secure + return getSwarm(publicKey).map { it.shuffled(SecureRandom()).random() } + } + + // Public API + fun getTargetSnodes(publicKey: String): Promise, Exception> { + // SecureRandom() should be cryptographically secure + return getSwarm(publicKey).map { it.shuffled(SecureRandom()).take(targetSwarmSnodeCount) } + } + + fun getSwarm(publicKey: String): Promise, Exception> { + val cachedSwarm = database.getSwarm(publicKey) + if (cachedSwarm != null && cachedSwarm.size >= minimumSwarmSnodeCount) { + val cachedSwarmCopy = mutableSetOf() // Workaround for a Kotlin compiler issue + cachedSwarmCopy.addAll(cachedSwarm) + return task { cachedSwarmCopy } + } else { + val parameters = mapOf( "pubKey" to publicKey ) + return getRandomSnode().bind { + invoke(Snode.Method.GetSwarm, it, publicKey, parameters) + }.map(SnodeAPI.sharedContext) { + parseSnodes(it).toSet() + }.success { + database.setSwarm(publicKey, it) + } + } + } + + fun getRawMessages(snode: Snode, publicKey: String): RawResponsePromise { + val lastHashValue = database.getLastMessageHashValue(snode, publicKey) ?: "" + val parameters = mapOf( "pubKey" to publicKey, "lastHash" to lastHashValue ) + return invoke(Snode.Method.GetMessages, snode, publicKey, parameters) + } + + fun getMessages(publicKey: String): MessageListPromise { + return retryIfNeeded(maxRetryCount) { + getSingleTargetSnode(publicKey).bind(messagePollingContext) { snode -> + getRawMessages(snode, publicKey).map(messagePollingContext) { parseRawMessagesResponse(it, snode, publicKey) } + } + } + } + + fun sendMessage(message: SnodeMessage): Promise, Exception> { + val destination = message.recipient + fun broadcast(event: String) { + val dayInMs: Long = 86400000 + if (message.ttl != dayInMs && message.ttl != 4 * dayInMs) { return } + broadcaster.broadcast(event, message.timestamp) + } + broadcast("calculatingPoW") + return retryIfNeeded(maxRetryCount) { + getTargetSnodes(destination).map(messageSendingContext) { swarm -> + swarm.map { snode -> + broadcast("sendingMessage") + val parameters = message.toJSON() + retryIfNeeded(maxRetryCount) { + invoke(Snode.Method.SendMessage, snode, destination, parameters).map(messageSendingContext) { rawResponse -> + val json = rawResponse as? Map<*, *> + val powDifficulty = json?.get("difficulty") as? Int + if (powDifficulty != null) { + if (powDifficulty != SnodeAPI.powDifficulty && powDifficulty < 100) { + Log.d("Loki", "Setting proof of work difficulty to $powDifficulty (snode: $snode).") + SnodeAPI.powDifficulty = powDifficulty + } + } else { + Log.d("Loki", "Failed to update proof of work difficulty from: ${rawResponse.prettifiedDescription()}.") + } + rawResponse + } + } + }.toSet() + } + } + } + + // Parsing + private fun parseSnodes(rawResponse: Any): List { + val json = rawResponse as? Map<*, *> + val rawSnodes = json?.get("snodes") as? List<*> + if (rawSnodes != null) { + return rawSnodes.mapNotNull { rawSnode -> + val rawSnodeAsJSON = rawSnode as? Map<*, *> + val address = rawSnodeAsJSON?.get("ip") as? String + val portAsString = rawSnodeAsJSON?.get("port") as? String + val port = portAsString?.toInt() + val ed25519Key = rawSnodeAsJSON?.get("pubkey_ed25519") as? String + val x25519Key = rawSnodeAsJSON?.get("pubkey_x25519") as? String + if (address != null && port != null && ed25519Key != null && x25519Key != null && address != "0.0.0.0") { + Snode("https://$address", port, Snode.KeySet(ed25519Key, x25519Key)) + } else { + Log.d("Loki", "Failed to parse snode from: ${rawSnode?.prettifiedDescription()}.") + null + } + } + } else { + Log.d("Loki", "Failed to parse snodes from: ${rawResponse.prettifiedDescription()}.") + return listOf() + } + } + + private fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String): List { + val messages = rawResponse["messages"] as? List<*> + return if (messages != null) { + updateLastMessageHashValueIfPossible(snode, publicKey, messages) + val newRawMessages = removeDuplicates(publicKey, messages) + parseEnvelopes(newRawMessages) + } else { + listOf() + } + } + + private fun updateLastMessageHashValueIfPossible(snode: Snode, publicKey: String, rawMessages: List<*>) { + val lastMessageAsJSON = rawMessages.lastOrNull() as? Map<*, *> + val hashValue = lastMessageAsJSON?.get("hash") as? String + val expiration = lastMessageAsJSON?.get("expiration") as? Int + if (hashValue != null) { + database.setLastMessageHashValue(snode, publicKey, hashValue) + } else if (rawMessages.isNotEmpty()) { + Log.d("Loki", "Failed to update last message hash value from: ${rawMessages.prettifiedDescription()}.") + } + } + + private fun removeDuplicates(publicKey: String, rawMessages: List<*>): List<*> { + val receivedMessageHashValues = database.getReceivedMessageHashValues(publicKey)?.toMutableSet() ?: mutableSetOf() + return rawMessages.filter { rawMessage -> + val rawMessageAsJSON = rawMessage as? Map<*, *> + val hashValue = rawMessageAsJSON?.get("hash") as? String + if (hashValue != null) { + val isDuplicate = receivedMessageHashValues.contains(hashValue) + receivedMessageHashValues.add(hashValue) + database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues) + !isDuplicate + } else { + Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.") + false + } + } + } + + private fun parseEnvelopes(rawMessages: List<*>): List { + return rawMessages.mapNotNull { rawMessage -> + val rawMessageAsJSON = rawMessage as? Map<*, *> + val base64EncodedData = rawMessageAsJSON?.get("data") as? String + val data = base64EncodedData?.let { Base64.decode(it) } + if (data != null) { + try { + MessageWrapper.unwrap(data) + } catch (e: Exception) { + Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") + null + } + } else { + Log.d("Loki", "Failed to decode data for message: ${rawMessage?.prettifiedDescription()}.") + null + } + } + } + + // Error Handling + internal fun handleSnodeError(statusCode: Int, json: Map<*, *>?, snode: Snode, publicKey: String? = null): Exception? { + fun handleBadSnode() { + val oldFailureCount = snodeFailureCount[snode] ?: 0 + val newFailureCount = oldFailureCount + 1 + snodeFailureCount[snode] = newFailureCount + Log.d("Loki", "Couldn't reach snode at $snode; setting failure count to $newFailureCount.") + if (newFailureCount >= snodeFailureThreshold) { + Log.d("Loki", "Failure threshold reached for: $snode; dropping it.") + if (publicKey != null) { + dropSnodeFromSwarmIfNeeded(snode, publicKey) + } + snodePool = snodePool.toMutableSet().minus(snode).toSet() + Log.d("Loki", "Snode pool count: ${snodePool.count()}.") + snodeFailureCount[snode] = 0 + } + } + when (statusCode) { + 400, 500, 503 -> { // Usually indicates that the snode isn't up to date + handleBadSnode() + } + 406 -> { + Log.d("Loki", "The user's clock is out of sync with the service node network.") + broadcaster.broadcast("clockOutOfSync") + return Error.ClockOutOfSync + } + 421 -> { + // The snode isn't associated with the given public key anymore + if (publicKey != null) { + Log.d("Loki", "Invalidating swarm for: $publicKey.") + dropSnodeFromSwarmIfNeeded(snode, publicKey) + } else { + Log.d("Loki", "Got a 421 without an associated public key.") + } + } + 432 -> { + // The PoW difficulty is too low + val powDifficulty = json?.get("difficulty") as? Int + if (powDifficulty != null) { + if (powDifficulty < 100) { + Log.d("Loki", "Setting proof of work difficulty to $powDifficulty (snode: $snode).") + SnodeAPI.powDifficulty = powDifficulty + } else { + handleBadSnode() + } + } else { + Log.d("Loki", "Failed to update proof of work difficulty.") + } + } + else -> { + handleBadSnode() + Log.d("Loki", "Unhandled response code: ${statusCode}.") + return Error.Generic + } + } + return null + } + + +} + +// Type Aliases +typealias RawResponse = Map<*, *> +typealias MessageListPromise = Promise, Exception> +typealias RawResponsePromise = Promise diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt new file mode 100644 index 0000000000..558447c549 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeMessage.kt @@ -0,0 +1,23 @@ +package org.session.libsession.snode + +data class SnodeMessage( + // The hex encoded public key of the recipient. + val recipient: String, + // The content of the message. + val data: String, + // The time to live for the message in milliseconds. + val ttl: Long, + // When the proof of work was calculated. + val timestamp: Long, + // The base 64 encoded proof of work. + val nonce: String +) { + internal fun toJSON(): Map { + return mutableMapOf( + "pubKey" to recipient, + "data" to data, + "ttl" to ttl.toString(), + "timestamp" to timestamp.toString(), + "nonce" to nonce) + } +} diff --git a/libsession/src/main/java/org/session/libsession/snode/utilities/OKHTTPUtilities.kt b/libsession/src/main/java/org/session/libsession/snode/utilities/OKHTTPUtilities.kt new file mode 100644 index 0000000000..ba3a5dcb79 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/utilities/OKHTTPUtilities.kt @@ -0,0 +1,49 @@ +package org.session.libsession.utilities + +import okhttp3.MultipartBody +import okhttp3.Request +import okio.Buffer +import org.session.libsignal.service.internal.util.Base64 +import java.io.IOException +import java.util.* + +internal fun Request.getHeadersForOnionRequest(): Map { + val result = mutableMapOf() + val contentType = body()?.contentType() + if (contentType != null) { + result["content-type"] = contentType.toString() + } + val headers = headers() + for (name in headers.names()) { + val value = headers.get(name) + if (value != null) { + if (value.toLowerCase(Locale.US) == "true" || value.toLowerCase(Locale.US) == "false") { + result[name] = value.toBoolean() + } else if (value.toIntOrNull() != null) { + result[name] = value.toInt() + } else { + result[name] = value + } + } + } + return result +} + +internal fun Request.getBodyForOnionRequest(): Any? { + try { + val copyOfThis = newBuilder().build() + val buffer = Buffer() + val body = copyOfThis.body() ?: return null + body.writeTo(buffer) + val bodyAsData = buffer.readByteArray() + if (body is MultipartBody) { + val base64EncodedBody: String = Base64.encodeBytes(bodyAsData) + return mapOf( "fileUpload" to base64EncodedBody ) + } else { + val charset = body.contentType()?.charset() ?: Charsets.UTF_8 + return bodyAsData?.toString(charset) + } + } catch (e: IOException) { + return null + } +} diff --git a/libsession/src/main/java/org/session/libsession/snode/utilities/Random.kt b/libsession/src/main/java/org/session/libsession/snode/utilities/Random.kt new file mode 100644 index 0000000000..2ec42cdf5b --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/utilities/Random.kt @@ -0,0 +1,18 @@ +package org.session.libsession.snode.utilities + +import java.security.SecureRandom + +/** + * Uses `SecureRandom` to pick an element from this collection. + */ +fun Collection.getRandomElementOrNull(): T? { + val index = SecureRandom().nextInt(size) // SecureRandom() should be cryptographically secure + return elementAtOrNull(index) +} + +/** + * Uses `SecureRandom` to pick an element from this collection. + */ +fun Collection.getRandomElement(): T { + return getRandomElementOrNull()!! +} diff --git a/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt b/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt new file mode 100644 index 0000000000..7d2c616c23 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt @@ -0,0 +1,58 @@ +package org.session.libsession.utilities + +import org.whispersystems.curve25519.Curve25519 +import org.session.libsignal.libsignal.util.ByteUtil +import org.session.libsignal.libsignal.util.Hex +import org.session.libsignal.service.internal.util.Util +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.SecretKeySpec + +internal object AESGCM { + + internal data class EncryptionResult( + internal val ciphertext: ByteArray, + internal val symmetricKey: ByteArray, + internal val ephemeralPublicKey: ByteArray + ) + + internal val gcmTagSize = 128 + internal val ivSize = 12 + + /** + * Sync. Don't call from the main thread. + */ + internal fun decrypt(ivAndCiphertext: ByteArray, symmetricKey: ByteArray): ByteArray { + val iv = ivAndCiphertext.sliceArray(0 until ivSize) + val ciphertext = ivAndCiphertext.sliceArray(ivSize until ivAndCiphertext.count()) + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv)) + return cipher.doFinal(ciphertext) + } + + /** + * Sync. Don't call from the main thread. + */ + internal fun encrypt(plaintext: ByteArray, symmetricKey: ByteArray): ByteArray { + val iv = Util.getSecretBytes(ivSize) + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(symmetricKey, "AES"), GCMParameterSpec(gcmTagSize, iv)) + return ByteUtil.combine(iv, cipher.doFinal(plaintext)) + } + + /** + * Sync. Don't call from the main thread. + */ + internal fun encrypt(plaintext: ByteArray, hexEncodedX25519PublicKey: String): EncryptionResult { + val x25519PublicKey = Hex.fromStringCondensed(hexEncodedX25519PublicKey) + val ephemeralKeyPair = Curve25519.getInstance(Curve25519.BEST).generateKeyPair() + val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(x25519PublicKey, ephemeralKeyPair.privateKey) + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256")) + val symmetricKey = mac.doFinal(ephemeralSharedSecret) + val ciphertext = encrypt(plaintext, symmetricKey) + return EncryptionResult(ciphertext, symmetricKey, ephemeralKeyPair.publicKey) + } + +} \ No newline at end of file From da71fdfe4421931ad2bae052eec66ddb1dec3a1b Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 2 Dec 2020 16:38:30 +1100 Subject: [PATCH 14/24] add configuration and storage --- .../libsession/messaging/Configuration.kt | 22 ++++++ .../libsession/messaging/StorageProtocol.kt | 71 +++++++++++++++++++ .../session/libsession/snode/Configuration.kt | 14 ++++ .../libsession/snode/StorageProtocol.kt | 15 ++++ 4 files changed, 122 insertions(+) create mode 100644 libsession/src/main/java/org/session/libsession/messaging/Configuration.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt create mode 100644 libsession/src/main/java/org/session/libsession/snode/Configuration.kt create mode 100644 libsession/src/main/java/org/session/libsession/snode/StorageProtocol.kt diff --git a/libsession/src/main/java/org/session/libsession/messaging/Configuration.kt b/libsession/src/main/java/org/session/libsession/messaging/Configuration.kt new file mode 100644 index 0000000000..e95bcebfd4 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/Configuration.kt @@ -0,0 +1,22 @@ +package org.session.libsession.messaging + +import org.session.libsignal.libsignal.loki.SessionResetProtocol +import org.session.libsignal.libsignal.state.* +import org.session.libsignal.metadata.certificate.CertificateValidator +import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysDatabaseProtocol + +class Configuration(val storage: StorageProtocol, val signalStorage: SignalProtocolStore, val sskDatabase: SharedSenderKeysDatabaseProtocol, val sessionResetImp: SessionResetProtocol, val certificateValidator: CertificateValidator) { + companion object { + lateinit var shared: Configuration + + fun configure(storage: StorageProtocol, + signalStorage: SignalProtocolStore, + sskDatabase: SharedSenderKeysDatabaseProtocol, + sessionResetImp: SessionResetProtocol, + certificateValidator: CertificateValidator + ) { + if (Companion::shared.isInitialized) { return } + shared = Configuration(storage, signalStorage, sskDatabase, sessionResetImp, certificateValidator) + } + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt new file mode 100644 index 0000000000..a2ac740bbf --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -0,0 +1,71 @@ +package org.session.libsession.messaging + +import org.session.libsession.messaging.jobs.AttachmentUploadJob +import org.session.libsession.messaging.jobs.Job +import org.session.libsession.messaging.jobs.MessageSendJob +import org.session.libsession.messaging.opengroups.OpenGroup + +import org.session.libsignal.libsignal.ecc.ECKeyPair +import org.session.libsignal.libsignal.ecc.ECPrivateKey + +interface StorageProtocol { + + // General + fun getUserPublicKey(): String? + fun getUserKeyPair(): ECKeyPair? + fun getUserDisplayName(): String? + fun getUserProfileKey(): ByteArray? + fun getUserProfilePictureURL(): String? + + // Shared Sender Keys + fun getClosedGroupPrivateKey(publicKey: String): ECPrivateKey? + fun isClosedGroup(publicKey: String): Boolean + + // Jobs + fun persist(job: Job) + fun markJobAsSucceeded(job: Job) + fun markJobAsFailed(job: Job) + fun getAllPendingJobs(type: String): List + fun getAttachmentUploadJob(attachmentID: String): AttachmentUploadJob? + fun getMessageSendJob(messageSendJobID: String): MessageSendJob? + fun resumeMessageSendJobIfNeeded(messageSendJobID: String) + fun isJobCanceled(job: Job): Boolean + + // Authorization + fun getAuthToken(server: String): String? + fun setAuthToken(server: String, newValue: String?) + fun removeAuthToken(server: String) + + // Open Groups + fun getOpenGroup(threadID: String): OpenGroup? + fun getThreadID(openGroupID: String): String? + + // Open Group Public Keys + fun getOpenGroupPublicKey(server: String): String? + fun setOpenGroupPublicKey(server: String, newValue: String) + + // Last Message Server ID + fun getLastMessageServerID(group: Long, server: String): Long? + fun setLastMessageServerID(group: Long, server: String, newValue: Long) + fun removeLastMessageServerID(group: Long, server: String) + + // Last Deletion Server ID + fun getLastDeletionServerID(group: Long, server: String): Long? + fun setLastDeletionServerID(group: Long, server: String, newValue: Long) + fun removeLastDeletionServerID(group: Long, server: String) + + // Open Group Metadata + fun setUserCount(group: Long, server: String, newValue: Int) + fun setOpenGroupProfilePictureURL(group: Long, server: String, newValue: String) + fun getOpenGroupProfilePictureURL(group: Long, server: String): String? + fun updateTitle(groupID: String, newValue: String) + fun updateProfilePicture(groupID: String, newValue: ByteArray) + + + + + fun getSessionRequestSentTimestamp(publicKey: String): Long? + fun setSessionRequestSentTimestamp(publicKey: String, newValue: Long) + fun getSessionRequestProcessedTimestamp(publicKey: String): Long? + fun setSessionRequestProcessedTimestamp(publicKey: String, newValue: Long) +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/snode/Configuration.kt b/libsession/src/main/java/org/session/libsession/snode/Configuration.kt new file mode 100644 index 0000000000..756351d15a --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/Configuration.kt @@ -0,0 +1,14 @@ +package org.session.libsession.snode + +import org.session.libsignal.service.loki.utilities.Broadcaster + +class Configuration(val storage: SnodeStorageProtocol, val broadcaster: Broadcaster) { + companion object { + lateinit var shared: Configuration + + fun configure(storage: SnodeStorageProtocol, broadcaster: Broadcaster) { + if (Companion::shared.isInitialized) { return } + shared = Configuration(storage, broadcaster) + } + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/snode/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/snode/StorageProtocol.kt new file mode 100644 index 0000000000..ec79d1ccaf --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/StorageProtocol.kt @@ -0,0 +1,15 @@ +package org.session.libsession.snode + +interface SnodeStorageProtocol { + fun getSnodePool(): Set + fun setSnodePool(newValue: Set) + fun getOnionRequestPaths(): List> + fun clearOnionRequestPaths() + fun setOnionRequestPaths(newValue: List>) + fun getSwarm(publicKey: String): Set? + fun setSwarm(publicKey: String, newValue: Set) + fun getLastMessageHashValue(snode: Snode, publicKey: String): String? + fun setLastMessageHashValue(snode: Snode, publicKey: String, newValue: String) + fun getReceivedMessageHashValues(publicKey: String): Set? + fun setReceivedMessageHashValues(publicKey: String, newValue: Set) +} \ No newline at end of file From bfb16c581a4cbd5360ed46b64dd64eb5f1191402 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 2 Dec 2020 16:39:02 +1100 Subject: [PATCH 15/24] WIP: refactor jobs (basic) --- .../messaging/jobs/AttachmentDownloadJob.kt | 15 +++- .../messaging/jobs/AttachmentUploadJob.kt | 15 +++- .../session/libsession/messaging/jobs/Job.kt | 9 +- .../libsession/messaging/jobs/JobDelegate.kt | 5 +- .../libsession/messaging/jobs/JobQueue.kt | 87 ++++++++++++++++++- .../messaging/jobs/MessageReceiveJob.kt | 15 +++- .../messaging/jobs/MessageSendJob.kt | 15 +++- .../messaging/jobs/NotifyPNServerJob.kt | 57 +++++++++++- 8 files changed, 209 insertions(+), 9 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index 036502604b..4c06c85547 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -1,4 +1,17 @@ -package org.session.messaging.jobs +package org.session.libsession.messaging.jobs class AttachmentDownloadJob: Job { + override var delegate: JobDelegate? = null + override var id: String? = null + override var failureCount: Int = 0 + + // Settings + override val maxFailureCount: Int = 100 + companion object { + val collection: String = "AttachmentDownloadJobCollection" + } + + override fun execute() { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index 8cd49dff8c..d0fc7ce7e7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -1,4 +1,17 @@ -package org.session.messaging.jobs +package org.session.libsession.messaging.jobs class AttachmentUploadJob : Job { + override var delegate: JobDelegate? = null + override var id: String? = null + override var failureCount: Int = 0 + + // Settings + override val maxFailureCount: Int = 20 + companion object { + val collection: String = "AttachmentUploadJobCollection" + } + + override fun execute() { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt index 2913063b16..c3108835cf 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/Job.kt @@ -1,4 +1,11 @@ -package org.session.messaging.jobs +package org.session.libsession.messaging.jobs interface Job { + var delegate: JobDelegate? + var id: String? + var failureCount: Int + + val maxFailureCount: Int + + fun execute() } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobDelegate.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobDelegate.kt index ba4f2c9afc..0efe78fbda 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobDelegate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobDelegate.kt @@ -1,4 +1,7 @@ -package org.session.messaging.jobs +package org.session.libsession.messaging.jobs interface JobDelegate { + fun handleJobSucceeded(job: Job) + fun handleJobFailed(job: Job, error: Exception) + fun handleJobFailedPermanently(job: Job, error: Exception) } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt index 5e3fa3990e..0054a77b1d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/JobQueue.kt @@ -1,4 +1,89 @@ -package org.session.messaging.jobs +package org.session.libsession.messaging.jobs + +import kotlin.math.min +import kotlin.math.pow +import java.util.Timer + +import org.session.libsession.messaging.Configuration + +import org.session.libsignal.libsignal.logging.Log +import kotlin.concurrent.schedule +import kotlin.math.roundToLong + class JobQueue : JobDelegate { + private var hasResumedPendingJobs = false // Just for debugging + + companion object { + val shared: JobQueue by lazy { JobQueue() } + } + + fun add(job: Job) { + addWithoutExecuting(job) + job.execute() + } + + fun addWithoutExecuting(job: Job) { + job.id = System.currentTimeMillis().toString() + Configuration.shared.storage.persist(job) + job.delegate = this + } + + fun resumePendingJobs() { + if (hasResumedPendingJobs) { + Log.d("Loki", "resumePendingJobs() should only be called once.") + return + } + hasResumedPendingJobs = true + val allJobTypes = listOf(AttachmentDownloadJob.collection, AttachmentDownloadJob.collection, MessageReceiveJob.collection, MessageSendJob.collection, NotifyPNServerJob.collection) + allJobTypes.forEach { type -> + val allPendingJobs = Configuration.shared.storage.getAllPendingJobs(type) + allPendingJobs.sortedBy { it.id }.forEach { job -> + Log.i("Jobs", "Resuming pending job of type: ${job::class.simpleName}.") + job.delegate = this + job.execute() + } + } + } + + override fun handleJobSucceeded(job: Job) { + Configuration.shared.storage.markJobAsSucceeded(job) + } + + override fun handleJobFailed(job: Job, error: Exception) { + job.failureCount += 1 + val storage = Configuration.shared.storage + if (storage.isJobCanceled(job)) { return Log.i("Jobs", "${job::class.simpleName} canceled.")} + storage.persist(job) + if (job.failureCount == job.maxFailureCount) { + storage.markJobAsFailed(job) + } else { + val retryInterval = getRetryInterval(job) + Log.i("Jobs", "${job::class.simpleName} failed; scheduling retry (failure count is ${job.failureCount}).") + Timer().schedule(delay = retryInterval) { + Log.i("Jobs", "Retrying ${job::class.simpleName}.") + job.execute() + } + } + } + + override fun handleJobFailedPermanently(job: Job, error: Exception) { + job.failureCount += 1 + val storage = Configuration.shared.storage + storage.persist(job) + storage.markJobAsFailed(job) + } + + private fun getRetryInterval(job: Job): Long { + // Arbitrary backoff factor... + // try 1 delay: 0ms + // try 2 delay: 190ms + // ... + // try 5 delay: 1300ms + // ... + // try 11 delay: 61310ms + val backoffFactor = 1.9 + val maxBackoff = (60 * 60 * 1000).toDouble() + return (100 * min(maxBackoff, backoffFactor.pow(job.failureCount))).roundToLong() + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt index cc12a399bf..fb5e3d34b5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageReceiveJob.kt @@ -1,4 +1,17 @@ -package org.session.messaging.jobs +package org.session.libsession.messaging.jobs class MessageReceiveJob : Job { + override var delegate: JobDelegate? = null + override var id: String? = null + override var failureCount: Int = 0 + + // Settings + override val maxFailureCount: Int = 10 + companion object { + val collection: String = "MessageReceiveJobCollection" + } + + override fun execute() { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index a9c44b77b1..9e32623fde 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -1,4 +1,17 @@ -package org.session.messaging.jobs +package org.session.libsession.messaging.jobs class MessageSendJob : Job { + override var delegate: JobDelegate? = null + override var id: String? = null + override var failureCount: Int = 0 + + // Settings + override val maxFailureCount: Int = 10 + companion object { + val collection: String = "MessageSendJobCollection" + } + + override fun execute() { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt index 134ddb7c45..b9221efcf5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/NotifyPNServerJob.kt @@ -1,4 +1,57 @@ -package org.session.messaging.jobs +package org.session.libsession.messaging.jobs -class NotifyPNServerJob : Job { +import nl.komponents.kovenant.functional.map +import okhttp3.MediaType +import okhttp3.Request +import okhttp3.RequestBody + +import org.session.libsession.messaging.sending_receiving.notifications.PushNotificationAPI +import org.session.libsession.snode.SnodeMessage +import org.session.libsession.snode.OnionRequestAPI + +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.util.JsonUtil +import org.session.libsignal.service.loki.utilities.retryIfNeeded + +class NotifyPNServerJob(val message: SnodeMessage) : Job { + override var delegate: JobDelegate? = null + override var id: String? = null + override var failureCount: Int = 0 + + // Settings + override val maxFailureCount: Int = 20 + companion object { + val collection: String = "NotifyPNServerJobCollection" + } + + // Running + override fun execute() { + val server = PushNotificationAPI.server + val parameters = mapOf( "data" to message.data, "send_to" to message.recipient ) + val url = "${server}/notify" + val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters)) + val request = Request.Builder().url(url).post(body) + retryIfNeeded(4) { + OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, "/loki/v2/lsrpc").map { json -> + val code = json["code"] as? Int + if (code == null || code == 0) { + Log.d("Loki", "[Loki] Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.") + } + }.fail { exception -> + Log.d("Loki", "[Loki] Couldn't notify PN server due to error: $exception.") + } + }.success { + handleSuccess() + }. fail { + handleFailure(it) + } + } + + private fun handleSuccess() { + delegate?.handleJobSucceeded(this) + } + + private fun handleFailure(error: Exception) { + delegate?.handleJobFailed(this, error) + } } \ No newline at end of file From 6cc20b81bdcbffdb86019d9fd4fbf1287ec9c68f Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 2 Dec 2020 16:39:21 +1100 Subject: [PATCH 16/24] decryption and encrytion --- .../MessageReceiverDecryption.kt | 54 ++++- .../sending_receiving/MessageSender.kt | 196 +++++++++++++++++- .../MessageSenderEncryption.kt | 47 ++++- 3 files changed, 294 insertions(+), 3 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt index 6b0ee22072..6ca2757f59 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt @@ -1,4 +1,56 @@ -package org.session.messaging.sending_receiving +package org.session.libsession.messaging.sending_receiving + +import org.session.libsession.messaging.Configuration +import org.session.libsignal.service.api.push.SignalServiceAddress +import org.session.libsignal.service.loki.crypto.LokiServiceCipher +import org.session.libsession.messaging.sending_receiving.MessageReceiver.Error +import org.session.libsession.utilities.AESGCM +import org.whispersystems.curve25519.Curve25519 +import org.session.libsignal.libsignal.loki.ClosedGroupCiphertextMessage +import org.session.libsignal.libsignal.util.Pair +import org.session.libsignal.service.api.messages.SignalServiceEnvelope +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation +import org.session.libsignal.service.loki.utilities.toHexString +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec object MessageReceiverDecryption { + + internal fun decryptWithSignalProtocol(envelope: SignalServiceEnvelope): Pair { + val storage = Configuration.shared.signalStorage + val certificateValidator = Configuration.shared.certificateValidator + val data = envelope.content + if (data.count() == 0) { throw Error.NoData } + val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey + val cipher = LokiServiceCipher(SignalServiceAddress(userPublicKey), storage, sskDatabase, Configuration.shared.sessionResetImp, certificateValidator) + val result = cipher.decrypt(envelope) + } + + internal fun decryptWithSharedSenderKeys(envelope: SignalServiceEnvelope): Pair { + // 1. ) Check preconditions + val groupPublicKey = envelope.source + if (!Configuration.shared.storage.isClosedGroup(groupPublicKey)) { throw Error.InvalidGroupPublicKey } + val data = envelope.content + if (data.count() == 0) { throw Error.NoData } + val groupPrivateKey = Configuration.shared.storage.getClosedGroupPrivateKey(groupPublicKey) ?: throw Error.NoGroupPrivateKey + // 2. ) Parse the wrapper + val wrapper = SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.parseFrom(data) + val ivAndCiphertext = wrapper.ciphertext.toByteArray() + val ephemeralPublicKey = wrapper.ephemeralPublicKey.toByteArray() + // 3. ) Decrypt the data inside + val ephemeralSharedSecret = Curve25519.getInstance(Curve25519.BEST).calculateAgreement(ephemeralPublicKey, groupPrivateKey.serialize()) + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec("LOKI".toByteArray(), "HmacSHA256")) + val symmetricKey = mac.doFinal(ephemeralSharedSecret) + val closedGroupCiphertextMessageAsData = AESGCM.decrypt(ivAndCiphertext, symmetricKey) + // 4. ) Parse the closed group ciphertext message + val closedGroupCiphertextMessage = ClosedGroupCiphertextMessage.from(closedGroupCiphertextMessageAsData) ?: throw Error.ParsingFailed + val senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString() + if (senderPublicKey == Configuration.shared.storage.getUserPublicKey()) { throw Error.SelfSend } + // 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content + val plaintext = SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, groupPublicKey, senderPublicKey, closedGroupCiphertextMessage.keyIndex) + // 6. ) Return + return Pair(plaintext, senderPublicKey) + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index 87941f7aea..f4f1048ca1 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -1,4 +1,198 @@ -package org.session.messaging.sending_receiving +package org.session.libsession.messaging.sending_receiving + +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.deferred + +import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.jobs.JobQueue +import org.session.libsession.messaging.messages.Destination +import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.jobs.NotifyPNServerJob +import org.session.libsession.messaging.opengroups.OpenGroupAPI +import org.session.libsession.messaging.opengroups.OpenGroupMessage +import org.session.libsession.messaging.utilities.MessageWrapper +import org.session.libsession.snode.RawResponsePromise +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.snode.SnodeMessage + +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.internal.util.Base64 +import org.session.libsignal.service.loki.api.crypto.ProofOfWork + object MessageSender { + + // Error + internal sealed class Error(val description: String) : Exception() { + object InvalidMessage : Error("Invalid message.") + object ProtoConversionFailed : Error("Couldn't convert message to proto.") + object ProofOfWorkCalculationFailed : Error("Proof of work calculation failed.") + object NoUserPublicKey : Error("Couldn't find user key pair.") + + // Closed groups + object NoThread : Error("Couldn't find a thread associated with the given group public key.") + object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.") + object InvalidClosedGroupUpdate : Error("Invalid group update.") + + internal val isRetryable: Boolean = when (this) { + is InvalidMessage -> false + is ProtoConversionFailed -> false + is ProofOfWorkCalculationFailed -> false + is InvalidClosedGroupUpdate -> false + else -> true + } + } + + // Convenience + fun send(message: Message, destination: Destination): Promise { + if (destination is Destination.OpenGroup) { + return sendToOpenGroupDestination(destination, message) + } + return sendToSnodeDestination(destination, message) + } + + // One-on-One Chats & Closed Groups + fun sendToSnodeDestination(destination: Destination, message: Message): Promise { + val deferred = deferred() + val promise = deferred.promise + val storage = Configuration.shared.storage + val preconditionFailure = Exception("Destination should not be open groups!") + var snodeMessage: SnodeMessage? = null + message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */ + message.sender = storage.getUserPublicKey() + try { + when (destination) { + is Destination.Contact -> message.recipient = destination.publicKey + is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey + is Destination.OpenGroup -> throw preconditionFailure + } + // Validate the message + message.isValid ?: throw Error.InvalidMessage + // Convert it to protobuf + val proto = message.toProto() ?: throw Error.ProtoConversionFailed + // Serialize the protobuf + val plaintext = proto.toByteArray() + // Encrypt the serialized protobuf + val ciphertext: ByteArray + when (destination) { + is Destination.Contact -> ciphertext = MessageSenderEncryption.encryptWithSignalProtocol(plaintext, message, destination.publicKey) + is Destination.ClosedGroup -> ciphertext = MessageSenderEncryption.encryptWithSharedSenderKeys(plaintext, destination.groupPublicKey) + is Destination.OpenGroup -> throw preconditionFailure + } + // Wrap the result + val kind: SignalServiceProtos.Envelope.Type + val senderPublicKey: String + when (destination) { + is Destination.Contact -> { + kind = SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER + senderPublicKey = "" + } + is Destination.ClosedGroup -> { + kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT + senderPublicKey = destination.groupPublicKey + } + is Destination.OpenGroup -> throw preconditionFailure + } + val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext) + // Calculate proof of work + val recipient = message.recipient!! + val base64EncodedData = Base64.encodeBytes(wrappedMessage) + val timestamp = System.currentTimeMillis() + val nonce = ProofOfWork.calculate(base64EncodedData, recipient, timestamp, message.ttl.toInt()) ?: throw Error.ProofOfWorkCalculationFailed + // Send the result + snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, timestamp, nonce) + SnodeAPI.sendMessage(snodeMessage).success { promises: Set -> + var isSuccess = false + val promiseCount = promises.size + var errorCount = 0 + promises.forEach { promise: RawResponsePromise -> + promise.success { + if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds + isSuccess = true + deferred.resolve(Unit) + } + promise.fail { + errorCount += 1 + if (errorCount != promiseCount) { return@fail } // Only error out if all promises failed + deferred.reject(it) + } + } + }.fail { + Log.d("Loki", "Couldn't send message due to error: $it.") + deferred.reject(it) + } + } catch (exception: Exception) { + deferred.reject(exception) + } + // Handle completion + promise.success { + handleSuccessfulMessageSend(message) + if (message is VisibleMessage && snodeMessage != null) { + val notifyPNServerJob = NotifyPNServerJob(snodeMessage) + JobQueue.shared.add(notifyPNServerJob) + } + } + promise.fail { + handleFailedMessageSend(message, it) + } + + return promise + } + + // Open Groups + fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise { + val deferred = deferred() + val promise = deferred.promise + val storage = Configuration.shared.storage + val preconditionFailure = Exception("Destination should not be contacts or closed groups!") + message.sentTimestamp = System.currentTimeMillis() + message.sender = storage.getUserPublicKey() + try { + val server: String + val channel: Long + when (destination) { + is Destination.Contact -> throw preconditionFailure + is Destination.ClosedGroup -> throw preconditionFailure + is Destination.OpenGroup -> { + message.recipient = "${destination.server}.${destination.channel}" + server = destination.server + channel = destination.channel + } + } + // Validate the message + if (message !is VisibleMessage || !message.isValid) { + throw Error.InvalidMessage + } + // Convert the message to an open group message + val openGroupMessage = OpenGroupMessage.from(message, server) ?: throw Error.InvalidMessage + // Send the result + OpenGroupAPI.sendMessage(openGroupMessage, channel, server).success { + message.openGroupServerMessageID = it.serverID + deferred.resolve(Unit) + }.fail { + deferred.reject(it) + } + } catch (exception: Exception) { + deferred.reject(exception) + } + // Handle completion + promise.success { + handleSuccessfulMessageSend(message) + } + promise.fail { + handleFailedMessageSend(message, it) + } + return deferred.promise + } + + // Result Handling + fun handleSuccessfulMessageSend(message: Message) { + + } + + fun handleFailedMessageSend(message: Message, error: Exception) { + + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt index 65e9828fbb..ef1a5ba8b9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt @@ -1,4 +1,49 @@ -package org.session.messaging.sending_receiving +package org.session.libsession.messaging.sending_receiving + +import com.google.protobuf.ByteString +import org.session.libsession.messaging.Configuration +import org.session.libsession.messaging.messages.Message +import org.session.libsession.messaging.sending_receiving.MessageSender.Error +import org.session.libsession.messaging.utilities.UnidentifiedAccessUtil +import org.session.libsession.utilities.AESGCM +import org.session.libsignal.libsignal.SignalProtocolAddress +import org.session.libsignal.libsignal.loki.ClosedGroupCiphertextMessage +import org.session.libsignal.libsignal.util.Hex +import org.session.libsignal.service.api.crypto.SignalServiceCipher +import org.session.libsignal.service.api.push.SignalServiceAddress +import org.session.libsignal.service.internal.push.SignalServiceProtos +import org.session.libsignal.service.internal.util.Base64 +import org.session.libsignal.service.loki.protocol.closedgroups.SharedSenderKeysImplementation +import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded object MessageSenderEncryption { + + internal fun encryptWithSignalProtocol(plaintext: ByteArray, message: Message, recipientPublicKey: String): ByteArray{ + val storage = Configuration.shared.signalStorage + val sskDatabase = Configuration.shared.sskDatabase + val sessionResetImp = Configuration.shared.sessionResetImp + val localAddress = SignalServiceAddress(recipientPublicKey) + val certificateValidator = Configuration.shared.certificateValidator + val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator) + val signalProtocolAddress = SignalProtocolAddress(recipientPublicKey, 1) + val unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) + val encryptedMessage = cipher.encrypt(signalProtocolAddress, unidentifiedAccess,plaintext) + return Base64.decode(encryptedMessage.content) + } + + internal fun encryptWithSharedSenderKeys(plaintext: ByteArray, groupPublicKey: String): ByteArray { + // 1. ) Encrypt the data with the user's sender key + val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey + val ciphertextAndKeyIndex = SharedSenderKeysImplementation.shared.encrypt(plaintext, groupPublicKey, userPublicKey) + val ivAndCiphertext = ciphertextAndKeyIndex.first + val keyIndex = ciphertextAndKeyIndex.second + val encryptedMessage = ClosedGroupCiphertextMessage(ivAndCiphertext, Hex.fromStringCondensed(userPublicKey), keyIndex); + // 2. ) Encrypt the result for the group's public key to hide the sender public key and key index + val intermediate = AESGCM.encrypt(encryptedMessage.serialize(), groupPublicKey.removing05PrefixIfNeeded()) + // 3. ) Wrap the result + return SignalServiceProtos.ClosedGroupCiphertextMessageWrapper.newBuilder() + .setCiphertext(ByteString.copyFrom(intermediate.ciphertext)) + .setEphemeralPublicKey(ByteString.copyFrom(intermediate.ephemeralPublicKey)) + .build().toByteArray() + } } \ No newline at end of file From 0a437042693528ba4d27c6d8888404155d8d32e8 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 2 Dec 2020 16:39:33 +1100 Subject: [PATCH 17/24] message sending and receiving --- .../sending_receiving/MessageReceiver.kt | 27 ++++++++++++++++++- .../MessageReceiverDelegate.kt | 2 +- .../MessageSenderDelegate.kt | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index 5764d952b9..2b50dfd62d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -1,4 +1,29 @@ -package org.session.messaging.sending_receiving +package org.session.libsession.messaging.sending_receiving object MessageReceiver { + internal sealed class Error(val description: String) : Exception() { + object InvalidMessage: Error("Invalid message.") + object UnknownMessage: Error("Unknown message type.") + object UnknownEnvelopeType: Error("Unknown envelope type.") + object NoUserPublicKey: Error("Couldn't find user key pair.") + object NoData: Error("Received an empty envelope.") + object SenderBlocked: Error("Received a message from a blocked user.") + object NoThread: Error("Couldn't find thread for message.") + object SelfSend: Error("Message addressed at self.") + object ParsingFailed : Error("Couldn't parse ciphertext message.") + // Shared sender keys + object InvalidGroupPublicKey: Error("Invalid group public key.") + object NoGroupPrivateKey: Error("Missing group private key.") + object SharedSecretGenerationFailed: Error("Couldn't generate a shared secret.") + + internal val isRetryable: Boolean = when (this) { + is InvalidMessage -> false + is UnknownMessage -> false + is UnknownEnvelopeType -> false + is NoData -> false + is SenderBlocked -> false + is SelfSend -> false + else -> true + } + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDelegate.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDelegate.kt index d3f37719d3..8312f2c674 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDelegate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDelegate.kt @@ -1,4 +1,4 @@ -package org.session.messaging.sending_receiving +package org.session.libsession.messaging.sending_receiving interface MessageReceiverDelegate { } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderDelegate.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderDelegate.kt index cee5622c47..2cdcb4d206 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderDelegate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderDelegate.kt @@ -1,4 +1,4 @@ -package org.session.messaging.sending_receiving +package org.session.libsession.messaging.sending_receiving interface MessageSenderDelegate { } \ No newline at end of file From bd96342a160ba9809f265e34962935358ea909f9 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 2 Dec 2020 16:43:19 +1100 Subject: [PATCH 18/24] minor refactoring on receiving decryption --- .../messaging/sending_receiving/MessageReceiverDecryption.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt index 6ca2757f59..529d7d5272 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt @@ -20,10 +20,12 @@ object MessageReceiverDecryption { internal fun decryptWithSignalProtocol(envelope: SignalServiceEnvelope): Pair { val storage = Configuration.shared.signalStorage val certificateValidator = Configuration.shared.certificateValidator + val sskDatabase = Configuration.shared.sskDatabase + val sessionResetImp = Configuration.shared.sessionResetImp val data = envelope.content if (data.count() == 0) { throw Error.NoData } val userPublicKey = Configuration.shared.storage.getUserPublicKey() ?: throw Error.NoUserPublicKey - val cipher = LokiServiceCipher(SignalServiceAddress(userPublicKey), storage, sskDatabase, Configuration.shared.sessionResetImp, certificateValidator) + val cipher = LokiServiceCipher(SignalServiceAddress(userPublicKey), storage, sskDatabase, sessionResetImp, certificateValidator) val result = cipher.decrypt(envelope) } From 69ba55138f61ed84148dc5454187ebcd8a324b65 Mon Sep 17 00:00:00 2001 From: Brice Date: Wed, 2 Dec 2020 16:46:48 +1100 Subject: [PATCH 19/24] test commit --- .../libsession/messaging/messages/control/unused/NullMessage.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt index b46bbc0231..a6ee19d18b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt @@ -9,7 +9,6 @@ import java.security.SecureRandom class NullMessage() : ControlMessage() { - companion object { const val TAG = "NullMessage" From 295eb3b52fd815792d59fef0298247085490963a Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 2 Dec 2020 17:06:28 +1100 Subject: [PATCH 20/24] rename and minor refactor to match the api --- .../messaging/messages/Destination.kt | 6 +- .../libsession/messaging/messages/Message.kt | 6 +- .../messaging/messages/visible/Attachment.kt | 4 +- .../messages/visible/BaseVisibleMessage.kt | 102 ------------------ .../messaging/messages/visible/Contact.kt | 2 +- .../messaging/messages/visible/LinkPreview.kt | 3 +- .../messaging/messages/visible/Profile.kt | 2 +- .../messaging/messages/visible/Quote.kt | 2 +- .../messages/visible/VisibleMessage.kt | 101 +++++++++++++++-- .../messages/visible/VisibleMessageProto.kt | 15 +++ .../messaging/opengroups/OpenGroupMessage.kt | 6 +- .../sending_receiving/MessageSender.kt | 4 +- 12 files changed, 123 insertions(+), 130 deletions(-) delete mode 100644 libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessageProto.kt diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt index a0573bd19c..07c2e726af 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Destination.kt @@ -2,9 +2,9 @@ package org.session.libsession.messaging.messages sealed class Destination { - class Contact(val publicKey: String) - class ClosedGroup(val groupPublicKey: String) - class OpenGroup(val channel: Long, val server: String) + class Contact(val publicKey: String) : Destination() + class ClosedGroup(val groupPublicKey: String) : Destination() + class OpenGroup(val channel: Long, val server: String) : Destination() companion object { //TODO need to implement the equivalent to TSThread and then implement from(...) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt index 3e92077689..338b8e0855 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/Message.kt @@ -12,11 +12,7 @@ abstract class Message { var sender: String? = null var groupPublicKey: String? = null var openGroupServerMessageID: Long? = null - - companion object { - @JvmStatic - val ttl = 2 * 24 * 60 * 60 * 1000 //TODO not sure about that declaration - } + val ttl: Long = 2 * 24 * 60 * 60 * 1000 // validation open fun isValid(): Boolean { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt index 7a37ef4849..90a3ebbaca 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Attachment.kt @@ -4,10 +4,8 @@ import android.util.Size import android.webkit.MimeTypeMap import org.session.libsignal.service.internal.push.SignalServiceProtos import java.io.File -import java.net.URL -import kotlin.math.absoluteValue -class Attachment : VisibleMessage() { +class Attachment : VisibleMessageProto() { var fileName: String? = null var contentType: String? = null diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt deleted file mode 100644 index 5e3fab2d1c..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/BaseVisibleMessage.kt +++ /dev/null @@ -1,102 +0,0 @@ -package org.session.libsession.messaging.messages.visible - -import org.session.libsignal.libsignal.logging.Log -import org.session.libsignal.service.internal.push.SignalServiceProtos - -class BaseVisibleMessage() : VisibleMessage() { - - var text: String? = null - var attachmentIDs = ArrayList() - var quote: Quote? = null - var linkPreview: LinkPreview? = null - var contact: Contact? = null - var profile: Profile? = null - - companion object { - const val TAG = "BaseVisibleMessage" - - fun fromProto(proto: SignalServiceProtos.Content): BaseVisibleMessage? { - val dataMessage = proto.dataMessage ?: return null - val result = BaseVisibleMessage() - result.text = dataMessage.body - // Attachments are handled in MessageReceiver - val quoteProto = dataMessage.quote - quoteProto?.let { - val quote = Quote.fromProto(quoteProto) - quote?.let { result.quote = quote } - } - val linkPreviewProto = dataMessage.previewList.first() - linkPreviewProto?.let { - val linkPreview = LinkPreview.fromProto(linkPreviewProto) - linkPreview?.let { result.linkPreview = linkPreview } - } - // TODO Contact - val profile = Profile.fromProto(dataMessage) - profile?.let { result.profile = profile } - return result - } - } - - // validation - override fun isValid(): Boolean { - if (!super.isValid()) return false - if (attachmentIDs.isNotEmpty()) return true - val text = text?.trim() ?: return false - if (text.isNotEmpty()) return true - return false - } - - override fun toProto(transaction: String): SignalServiceProtos.Content? { - val proto = SignalServiceProtos.Content.newBuilder() - var attachmentIDs = this.attachmentIDs - val dataMessage: SignalServiceProtos.DataMessage.Builder - // Profile - val profile = profile - val profileProto = profile?.toSSProto() - if (profileProto != null) { - dataMessage = profileProto.toBuilder() - } else { - dataMessage = SignalServiceProtos.DataMessage.newBuilder() - } - // Text - text?.let { dataMessage.body = text } - // Quote - val quotedAttachmentID = quote?.attachmentID - quotedAttachmentID?.let { - val index = attachmentIDs.indexOf(quotedAttachmentID) - if (index >= 0) { attachmentIDs.removeAt(index) } - } - val quote = quote - quote?.let { - val quoteProto = quote.toProto(transaction) - if (quoteProto != null) dataMessage.quote = quoteProto - } - //Link preview - val linkPreviewAttachmentID = linkPreview?.attachmentID - linkPreviewAttachmentID?.let { - val index = attachmentIDs.indexOf(quotedAttachmentID) - if (index >= 0) { attachmentIDs.removeAt(index) } - } - val linkPreview = linkPreview - linkPreview?.let { - val linkPreviewProto = linkPreview.toProto(transaction) - linkPreviewProto?.let { - dataMessage.addAllPreview(listOf(linkPreviewProto)) - } - } - //Attachments - // TODO I'm blocking on that one... - //swift: let attachments = attachmentIDs.compactMap { TSAttachmentStream.fetch(uniqueId: $0, transaction: transaction) } - - // TODO Contact - // Build - try { - proto.dataMessage = dataMessage.build() - return proto.build() - } catch (e: Exception) { - Log.w(TAG, "Couldn't construct visible message proto from: $this") - return null - } - } - -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt index cbca11cd2d..96af6e8bbd 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Contact.kt @@ -2,7 +2,7 @@ package org.session.libsession.messaging.messages.visible import org.session.libsignal.service.internal.push.SignalServiceProtos -class Contact : VisibleMessage() { +class Contact : VisibleMessageProto() { companion object { fun fromProto(proto: SignalServiceProtos.Content): Contact? { diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt index 7806e9fbe0..87779f26e3 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/LinkPreview.kt @@ -1,10 +1,9 @@ package org.session.libsession.messaging.messages.visible -import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos -class LinkPreview() : VisibleMessage(){ +class LinkPreview() : VisibleMessageProto(){ var title: String? = null var url: String? = null diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt index 5a2590b56d..10bc6ba350 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Profile.kt @@ -4,7 +4,7 @@ import com.google.protobuf.ByteString import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos -class Profile() : VisibleMessage() { +class Profile() : VisibleMessageProto() { var displayName: String? = null var profileKey: ByteArray? = null diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt index 0d07821e40..4fb54f70a6 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Quote.kt @@ -3,7 +3,7 @@ package org.session.libsession.messaging.messages.visible import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos -class Quote() : VisibleMessage() { +class Quote() : VisibleMessageProto() { var timestamp: Long? = 0 var publicKey: String? = null diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index dd1068cc45..89486f7481 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -1,15 +1,102 @@ package org.session.libsession.messaging.messages.visible -import org.session.libsession.messaging.messages.Message +import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos -abstract class VisibleMessage : Message() { +class VisibleMessage() : VisibleMessageProto() { - abstract fun toProto(transaction: String): T + var text: String? = null + var attachmentIDs = ArrayList() + var quote: Quote? = null + var linkPreview: LinkPreview? = null + var contact: Contact? = null + var profile: Profile? = null - final override fun toProto(): SignalServiceProtos.Content? { - //we don't need to implement this method in subclasses - //TODO it just needs an equivalent to swift: preconditionFailure("Use toProto(using:) if that exists... - TODO("Not implemented") + companion object { + const val TAG = "BaseVisibleMessage" + + fun fromProto(proto: SignalServiceProtos.Content): VisibleMessage? { + val dataMessage = proto.dataMessage ?: return null + val result = VisibleMessage() + result.text = dataMessage.body + // Attachments are handled in MessageReceiver + val quoteProto = dataMessage.quote + quoteProto?.let { + val quote = Quote.fromProto(quoteProto) + quote?.let { result.quote = quote } + } + val linkPreviewProto = dataMessage.previewList.first() + linkPreviewProto?.let { + val linkPreview = LinkPreview.fromProto(linkPreviewProto) + linkPreview?.let { result.linkPreview = linkPreview } + } + // TODO Contact + val profile = Profile.fromProto(dataMessage) + profile?.let { result.profile = profile } + return result + } } + + // validation + override fun isValid(): Boolean { + if (!super.isValid()) return false + if (attachmentIDs.isNotEmpty()) return true + val text = text?.trim() ?: return false + if (text.isNotEmpty()) return true + return false + } + + override fun toProto(transaction: String): SignalServiceProtos.Content? { + val proto = SignalServiceProtos.Content.newBuilder() + var attachmentIDs = this.attachmentIDs + val dataMessage: SignalServiceProtos.DataMessage.Builder + // Profile + val profile = profile + val profileProto = profile?.toSSProto() + if (profileProto != null) { + dataMessage = profileProto.toBuilder() + } else { + dataMessage = SignalServiceProtos.DataMessage.newBuilder() + } + // Text + text?.let { dataMessage.body = text } + // Quote + val quotedAttachmentID = quote?.attachmentID + quotedAttachmentID?.let { + val index = attachmentIDs.indexOf(quotedAttachmentID) + if (index >= 0) { attachmentIDs.removeAt(index) } + } + val quote = quote + quote?.let { + val quoteProto = quote.toProto(transaction) + if (quoteProto != null) dataMessage.quote = quoteProto + } + //Link preview + val linkPreviewAttachmentID = linkPreview?.attachmentID + linkPreviewAttachmentID?.let { + val index = attachmentIDs.indexOf(quotedAttachmentID) + if (index >= 0) { attachmentIDs.removeAt(index) } + } + val linkPreview = linkPreview + linkPreview?.let { + val linkPreviewProto = linkPreview.toProto(transaction) + linkPreviewProto?.let { + dataMessage.addAllPreview(listOf(linkPreviewProto)) + } + } + //Attachments + // TODO I'm blocking on that one... + //swift: let attachments = attachmentIDs.compactMap { TSAttachmentStream.fetch(uniqueId: $0, transaction: transaction) } + + // TODO Contact + // Build + try { + proto.dataMessage = dataMessage.build() + return proto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct visible message proto from: $this") + return null + } + } + } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessageProto.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessageProto.kt new file mode 100644 index 0000000000..d8dcd7090f --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessageProto.kt @@ -0,0 +1,15 @@ +package org.session.libsession.messaging.messages.visible + +import org.session.libsession.messaging.messages.Message +import org.session.libsignal.service.internal.push.SignalServiceProtos + +abstract class VisibleMessageProto : Message() { + + abstract fun toProto(transaction: String): T + + final override fun toProto(): SignalServiceProtos.Content? { + //we don't need to implement this method in subclasses + //TODO it just needs an equivalent to swift: preconditionFailure("Use toProto(using:) if that exists... + TODO("Not implemented") + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt index 51eb95b2f3..1647997a0e 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt @@ -27,11 +27,11 @@ public data class OpenGroupMessage( val storage = Configuration.shared.storage val userPublicKey = storage.getUserPublicKey() ?: return null // Validation - if (!message.isValid) { return null } // Should be valid at this point + if (!message.isValid()) { return null } // Should be valid at this point // Quote val quote: OpenGroupMessage.Quote? = { val quote = message.quote - if (quote != null && quote.isValid) { + if (quote != null && quote.isValid()) { val quotedMessageServerID = storage.getQuoteServerID(quote.id, quote.publicKey) OpenGroupMessage.Quote(quote.timestamp, quote.publicKey, quote.text, quotedMessageServerID) } else { @@ -45,7 +45,7 @@ public data class OpenGroupMessage( // Link preview val linkPreview = message.linkPreview linkPreview?.let { - if (!linkPreview.isValid) { return@let } + if (!linkPreview.isValid()) { return@let } val attachment = linkPreview.getImage() ?: return@let val openGroupLinkPreview = OpenGroupMessage.Attachment( OpenGroupMessage.Attachment.Kind.LinkPreview, diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index f4f1048ca1..773cb50dfc 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -69,7 +69,7 @@ object MessageSender { is Destination.OpenGroup -> throw preconditionFailure } // Validate the message - message.isValid ?: throw Error.InvalidMessage + if (!message.isValid()) { throw Error.InvalidMessage } // Convert it to protobuf val proto = message.toProto() ?: throw Error.ProtoConversionFailed // Serialize the protobuf @@ -162,7 +162,7 @@ object MessageSender { } } // Validate the message - if (message !is VisibleMessage || !message.isValid) { + if (message !is VisibleMessage || !message.isValid()) { throw Error.InvalidMessage } // Convert the message to an open group message From c758619f1399f429d071a46b66a799182cdf08b5 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Wed, 2 Dec 2020 17:10:45 +1100 Subject: [PATCH 21/24] clean --- .../messaging/opengroups/OpenGroupMessage.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt index 1647997a0e..6472a9f1b1 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/opengroups/OpenGroupMessage.kt @@ -29,11 +29,11 @@ public data class OpenGroupMessage( // Validation if (!message.isValid()) { return null } // Should be valid at this point // Quote - val quote: OpenGroupMessage.Quote? = { + val quote: Quote? = { val quote = message.quote if (quote != null && quote.isValid()) { val quotedMessageServerID = storage.getQuoteServerID(quote.id, quote.publicKey) - OpenGroupMessage.Quote(quote.timestamp, quote.publicKey, quote.text, quotedMessageServerID) + Quote(quote.timestamp!!, quote.publicKey!!, quote.text!!, quotedMessageServerID) } else { null } @@ -47,8 +47,8 @@ public data class OpenGroupMessage( linkPreview?.let { if (!linkPreview.isValid()) { return@let } val attachment = linkPreview.getImage() ?: return@let - val openGroupLinkPreview = OpenGroupMessage.Attachment( - OpenGroupMessage.Attachment.Kind.LinkPreview, + val openGroupLinkPreview = Attachment( + Attachment.Kind.LinkPreview, server, attachment.getId(), attachment.getContentType(), @@ -59,14 +59,14 @@ public data class OpenGroupMessage( attachment.getHeight(), attachment.getCaption(), attachment.getUrl(), - linkPreview.getUrl(), - linkPreview.getTitle()) + linkPreview.url, + linkPreview.title) result.attachments.add(openGroupLinkPreview) } // Attachments val attachments = message.getAttachemnts().forEach { - val attachement = OpenGroupMessage.Attachment( - OpenGroupMessage.Attachment.Kind.Attachment, + val attachement = Attachment( + Attachment.Kind.Attachment, server, it.getId(), it.getContentType(), From 5924d90b127006e143f33ffc9846846c60ccc044 Mon Sep 17 00:00:00 2001 From: Ryan ZHAO Date: Thu, 3 Dec 2020 14:20:49 +1100 Subject: [PATCH 22/24] refactor unidentified access (sealed sender) --- libsession/build.gradle | 1 + .../libsession/messaging/StorageProtocol.kt | 2 + .../MessageSenderEncryption.kt | 7 +- .../utilities/UnidentifiedAccessUtil.java | 121 ------------------ .../utilities/UnidentifiedAccessUtil.kt | 60 +++++++++ 5 files changed, 68 insertions(+), 123 deletions(-) delete mode 100644 libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.java create mode 100644 libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt diff --git a/libsession/build.gradle b/libsession/build.gradle index 2da1d4bc60..0ae0a9ca03 100644 --- a/libsession/build.gradle +++ b/libsession/build.gradle @@ -36,6 +36,7 @@ dependencies { // Local: implementation project(":libsignal") // Remote: + implementation "com.goterl.lazycode:lazysodium-android:4.2.0@aar" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt index a2ac740bbf..ded75c0799 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -17,6 +17,8 @@ interface StorageProtocol { fun getUserProfileKey(): ByteArray? fun getUserProfilePictureURL(): String? + fun getProfileKeyForRecipient(recipientPublicKey: String): ByteArray? + // Shared Sender Keys fun getClosedGroupPrivateKey(publicKey: String): ECPrivateKey? fun isClosedGroup(publicKey: String): Boolean diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt index ef1a5ba8b9..f1c9dd2c88 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt @@ -6,9 +6,11 @@ import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.sending_receiving.MessageSender.Error import org.session.libsession.messaging.utilities.UnidentifiedAccessUtil import org.session.libsession.utilities.AESGCM + import org.session.libsignal.libsignal.SignalProtocolAddress import org.session.libsignal.libsignal.loki.ClosedGroupCiphertextMessage import org.session.libsignal.libsignal.util.Hex +import org.session.libsignal.libsignal.util.guava.Optional import org.session.libsignal.service.api.crypto.SignalServiceCipher import org.session.libsignal.service.api.push.SignalServiceAddress import org.session.libsignal.service.internal.push.SignalServiceProtos @@ -26,8 +28,9 @@ object MessageSenderEncryption { val certificateValidator = Configuration.shared.certificateValidator val cipher = SignalServiceCipher(localAddress, storage, sskDatabase, sessionResetImp, certificateValidator) val signalProtocolAddress = SignalProtocolAddress(recipientPublicKey, 1) - val unidentifiedAccess = UnidentifiedAccessUtil.getAccessFor(context, recipient) - val encryptedMessage = cipher.encrypt(signalProtocolAddress, unidentifiedAccess,plaintext) + val unidentifiedAccessPair = UnidentifiedAccessUtil.getAccessFor(recipientPublicKey) + val unidentifiedAccess = if (unidentifiedAccessPair != null) unidentifiedAccessPair.targetUnidentifiedAccess else Optional.absent() + val encryptedMessage = cipher.encrypt(signalProtocolAddress, unidentifiedAccess, plaintext) return Base64.decode(encryptedMessage.content) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.java b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.java deleted file mode 100644 index c66a9b4954..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.java +++ /dev/null @@ -1,121 +0,0 @@ -package org.session.libsession.messaging.utilities; - - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; - -import org.session.libsignal.libsignal.util.guava.Optional; -import org.session.libsignal.metadata.SignalProtos; -import org.session.libsignal.metadata.certificate.CertificateValidator; -import org.session.libsignal.metadata.certificate.InvalidCertificateException; -import org.session.libsignal.service.api.crypto.UnidentifiedAccess; -import org.session.libsignal.service.api.crypto.UnidentifiedAccessPair; -import org.session.libsignal.service.api.push.SignalServiceAddress; -import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.Util; - -public class UnidentifiedAccessUtil { - - private static final String TAG = UnidentifiedAccessUtil.class.getSimpleName(); - - public static CertificateValidator getCertificateValidator() { - return new CertificateValidator(); - } - - @WorkerThread - public static Optional getAccessFor(@NonNull Context context, - @NonNull Recipient recipient) - { - if (!TextSecurePreferences.isUnidentifiedDeliveryEnabled(context)) { - Log.i(TAG, "Unidentified delivery is disabled. [other]"); - return Optional.absent(); - } - - try { - byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); - byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context); - byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context); - - if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { - ourUnidentifiedAccessKey = Util.getSecretBytes(16); - } - - Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) + - " | Our access key present? " + (ourUnidentifiedAccessKey != null) + - " | Our certificate present? " + (ourUnidentifiedAccessCertificate != null)); - - if (theirUnidentifiedAccessKey != null && - ourUnidentifiedAccessKey != null && - ourUnidentifiedAccessCertificate != null) - { - return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey, - ourUnidentifiedAccessCertificate), - new UnidentifiedAccess(ourUnidentifiedAccessKey, - ourUnidentifiedAccessCertificate))); - } - - return Optional.absent(); - } catch (InvalidCertificateException e) { - Log.w(TAG, e); - return Optional.absent(); - } - } - - public static Optional getAccessForSync(@NonNull Context context) { - if (!TextSecurePreferences.isUnidentifiedDeliveryEnabled(context)) { - Log.i(TAG, "Unidentified delivery is disabled. [self]"); - return Optional.absent(); - } - - try { - byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context); - byte[] ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate(context); - - if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) { - ourUnidentifiedAccessKey = Util.getSecretBytes(16); - } - - if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) { - return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(ourUnidentifiedAccessKey, - ourUnidentifiedAccessCertificate), - new UnidentifiedAccess(ourUnidentifiedAccessKey, - ourUnidentifiedAccessCertificate))); - } - - return Optional.absent(); - } catch (InvalidCertificateException e) { - Log.w(TAG, e); - return Optional.absent(); - } - } - - public static @NonNull byte[] getSelfUnidentifiedAccessKey(@NonNull Context context) { - return UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getProfileKey(context)); - } - - private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) { - byte[] theirProfileKey = recipient.resolve().getProfileKey(); - - if (theirProfileKey == null) return Util.getSecretBytes(16); - else return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey); - - } - - private static @Nullable byte[] getUnidentifiedAccessCertificate(Context context) { - String ourNumber = TextSecurePreferences.getLocalNumber(context); - if (ourNumber != null) { - SignalProtos.SenderCertificate certificate = SignalProtos.SenderCertificate.newBuilder() - .setSender(ourNumber) - .setSenderDevice(SignalServiceAddress.DEFAULT_DEVICE_ID) - .build(); - return certificate.toByteArray(); - } - - return null; - } -} diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt new file mode 100644 index 0000000000..de4e2db801 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/UnidentifiedAccessUtil.kt @@ -0,0 +1,60 @@ +package org.session.libsession.messaging.utilities + +import com.goterl.lazycode.lazysodium.LazySodiumAndroid +import com.goterl.lazycode.lazysodium.SodiumAndroid + +import org.session.libsession.messaging.Configuration + +import org.session.libsignal.libsignal.logging.Log +import org.session.libsignal.metadata.SignalProtos +import org.session.libsignal.metadata.certificate.InvalidCertificateException +import org.session.libsignal.service.api.crypto.UnidentifiedAccess +import org.session.libsignal.service.api.crypto.UnidentifiedAccessPair + +object UnidentifiedAccessUtil { + private val TAG = UnidentifiedAccessUtil::class.simpleName + private val sodium = LazySodiumAndroid(SodiumAndroid()) + + fun getAccessFor(recipientPublicKey: String): UnidentifiedAccessPair? { + try { + val theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipientPublicKey) + val ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey() + val ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate() + + Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) + + " | Our access key present? " + (ourUnidentifiedAccessKey != null) + + " | Our certificate present? " + (ourUnidentifiedAccessCertificate != null)) + + if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) { + return UnidentifiedAccessPair(UnidentifiedAccess(theirUnidentifiedAccessKey, ourUnidentifiedAccessCertificate), + UnidentifiedAccess(ourUnidentifiedAccessKey, ourUnidentifiedAccessCertificate)) + } + return null + } catch (e: InvalidCertificateException) { + Log.w(TAG, e) + return null + } + } + + private fun getTargetUnidentifiedAccessKey(recipientPublicKey: String): ByteArray? { + val theirProfileKey = Configuration.shared.storage.getProfileKeyForRecipient(recipientPublicKey) ?: return sodium.randomBytesBuf(16) + return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey) + } + + private fun getSelfUnidentifiedAccessKey(): ByteArray? { + val userPublicKey = Configuration.shared.storage.getUserPublicKey() + if (userPublicKey != null) { + return sodium.randomBytesBuf(16) + } + return null + } + + private fun getUnidentifiedAccessCertificate(): ByteArray? { + val userPublicKey = Configuration.shared.storage.getUserPublicKey() + if (userPublicKey != null) { + val certificate = SignalProtos.SenderCertificate.newBuilder().setSender(userPublicKey).setSenderDevice(1).build() + return certificate.toByteArray() + } + return null + } +} \ No newline at end of file From 3d87de4b56c848cf47a04c9509e93a4ef96f6042 Mon Sep 17 00:00:00 2001 From: Brice Date: Thu, 3 Dec 2020 14:29:50 +1100 Subject: [PATCH 23/24] SessionRequest implementation completed --- .../libsession/messaging/StorageProtocol.kt | 4 ++++ .../messages/control/unused/SessionRequest.kt | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt index a2ac740bbf..75702b6191 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -17,6 +17,10 @@ interface StorageProtocol { fun getUserProfileKey(): ByteArray? fun getUserProfilePictureURL(): String? + // Signal Protocol + + fun getOrGenerateRegistrationID(): Int //TODO needs impl + // Shared Sender Keys fun getClosedGroupPrivateKey(publicKey: String): ECPrivateKey? fun isClosedGroup(publicKey: String): Boolean diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt index b078ce5545..07bed84988 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/SessionRequest.kt @@ -1,9 +1,10 @@ package org.session.libsession.messaging.messages.control.unused import com.google.protobuf.ByteString +import org.session.libsession.messaging.Configuration import org.session.libsession.messaging.messages.control.ControlMessage -import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate -import org.session.libsession.messaging.messages.control.TypingIndicator +import org.session.libsignal.libsignal.IdentityKey +import org.session.libsignal.libsignal.ecc.DjbECPublicKey import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.libsignal.state.PreKeyBundle import org.session.libsignal.service.internal.push.SignalServiceProtos @@ -19,8 +20,9 @@ class SessionRequest() : ControlMessage() { fun fromProto(proto: SignalServiceProtos.Content): SessionRequest? { if (proto.nullMessage == null) return null val preKeyBundleProto = proto.preKeyBundleMessage ?: return null - val registrationID: Int = 0 - //TODO looks like database stuff here + var registrationID: Int = 0 + registrationID = Configuration.shared.storage.getOrGenerateRegistrationID() //TODO no implementation for getOrGenerateRegistrationID yet + //TODO just confirm if the above code does the equivalent to swift below: /*iOS code: Configuration.shared.storage.with { transaction in registrationID = Configuration.shared.storage.getOrGenerateRegistrationID(using: transaction) }*/ @@ -28,11 +30,11 @@ class SessionRequest() : ControlMessage() { registrationID, 1, preKeyBundleProto.preKeyId, - null, //TODO preKeyBundleProto.preKey, - 0, //TODO preKeyBundleProto.signedKey, - null, //TODO preKeyBundleProto.signedKeyId, + DjbECPublicKey(preKeyBundleProto.preKey.toByteArray()), + preKeyBundleProto.signedKeyId, + DjbECPublicKey(preKeyBundleProto.signedKey.toByteArray()), preKeyBundleProto.signature.toByteArray(), - null, //TODO preKeyBundleProto.identityKey + IdentityKey(DjbECPublicKey(preKeyBundleProto.identityKey.toByteArray())) ) return SessionRequest(preKeyBundle) } @@ -61,12 +63,12 @@ class SessionRequest() : ControlMessage() { val padding = ByteArray(paddingSize) nullMessageProto.padding = ByteString.copyFrom(padding) val preKeyBundleProto = SignalServiceProtos.PreKeyBundleMessage.newBuilder() - //TODO preKeyBundleProto.identityKey = preKeyBundle.identityKey + preKeyBundleProto.identityKey = ByteString.copyFrom(preKeyBundle.identityKey.publicKey.serialize()) preKeyBundleProto.deviceId = preKeyBundle.deviceId preKeyBundleProto.preKeyId = preKeyBundle.preKeyId - //TODO preKeyBundleProto.preKey = preKeyBundle.preKeyPublic + preKeyBundleProto.preKey = ByteString.copyFrom(preKeyBundle.preKey.serialize()) preKeyBundleProto.signedKeyId = preKeyBundle.signedPreKeyId - //TODO preKeyBundleProto.signedKey = preKeyBundle.signedPreKeyPublic + preKeyBundleProto.signedKey = ByteString.copyFrom(preKeyBundle.signedPreKey.serialize()) preKeyBundleProto.signature = ByteString.copyFrom(preKeyBundle.signedPreKeySignature) val contentProto = SignalServiceProtos.Content.newBuilder() try { From 2b1655d6880e6705627838035eb8197b21997e53 Mon Sep 17 00:00:00 2001 From: Brice Date: Thu, 3 Dec 2020 15:12:50 +1100 Subject: [PATCH 24/24] test commit --- .../libsession/messaging/messages/control/unused/NullMessage.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt index a6ee19d18b..59e8f2444a 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/unused/NullMessage.kt @@ -2,7 +2,6 @@ package org.session.libsession.messaging.messages.control.unused import com.google.protobuf.ByteString import org.session.libsession.messaging.messages.control.ControlMessage -import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsignal.libsignal.logging.Log import org.session.libsignal.service.internal.push.SignalServiceProtos import java.security.SecureRandom