Merge branch 'dev' of https://github.com/loki-project/session-android into data-extraction

This commit is contained in:
Brice-W
2021-03-23 10:42:30 +11:00
152 changed files with 1286 additions and 5495 deletions

View File

@@ -1,10 +1,8 @@
package org.session.libsession.database
import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentState
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentPointer
import org.session.libsession.messaging.sending_receiving.attachments.SessionServiceAttachmentStream
import org.session.libsession.messaging.sending_receiving.attachments.*
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
import java.io.InputStream
@@ -14,10 +12,13 @@ interface MessageDataProvider {
fun getMessageID(serverID: Long): Long?
fun deleteMessage(messageID: Long)
fun getDatabaseAttachment(attachmentId: Long): DatabaseAttachment?
fun getAttachmentStream(attachmentId: Long): SessionServiceAttachmentStream?
fun getAttachmentPointer(attachmentId: Long): SessionServiceAttachmentPointer?
fun getSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
fun getScaledSignalAttachmentStream(attachmentId: Long): SignalServiceAttachmentStream?
fun getSignalAttachmentPointer(attachmentId: Long): SignalServiceAttachmentPointer?
fun setAttachmentState(attachmentState: AttachmentState, attachmentId: Long, messageID: Long)
@@ -26,12 +27,15 @@ interface MessageDataProvider {
fun isOutgoingMessage(timestamp: Long): Boolean
@Throws(Exception::class)
fun uploadAttachment(attachmentId: Long)
fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult)
fun updateAttachmentAfterUploadFailed(attachmentId: Long)
// Quotes
fun getMessageForQuote(timestamp: Long, author: Address): Long?
fun getAttachmentsAndLinkPreviewFor(messageID: Long): List<Attachment>
fun getMessageBodyFor(messageID: Long): String
fun getAttachmentIDsFor(messageID: Long): List<Long>
fun getLinkPreviewAttachmentIDFor(messageID: Long): Long?
}

View File

@@ -0,0 +1,10 @@
package org.session.libsession.database.documents;
import java.util.List;
public interface Document<T> {
public int size();
public List<T> getList();
}

View File

@@ -0,0 +1,88 @@
package org.session.libsession.database.documents;
import org.session.libsignal.utilities.logging.Log;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.session.libsession.messaging.threads.Address;
import org.session.libsignal.utilities.Base64;
import org.session.libsignal.libsignal.IdentityKey;
import org.session.libsignal.libsignal.InvalidKeyException;
import java.io.IOException;
public class IdentityKeyMismatch {
private static final String TAG = IdentityKeyMismatch.class.getSimpleName();
@JsonProperty(value = "a")
private String address;
@JsonProperty(value = "k")
@JsonSerialize(using = IdentityKeySerializer.class)
@JsonDeserialize(using = IdentityKeyDeserializer.class)
private IdentityKey identityKey;
public IdentityKeyMismatch() {}
public IdentityKeyMismatch(Address address, IdentityKey identityKey) {
this.address = address.serialize();
this.identityKey = identityKey;
}
@JsonIgnore
public Address getAddress() {
return Address.fromSerialized(address);
}
public IdentityKey getIdentityKey() {
return identityKey;
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof IdentityKeyMismatch)) {
return false;
}
IdentityKeyMismatch that = (IdentityKeyMismatch)other;
return that.address.equals(this.address) && that.identityKey.equals(this.identityKey);
}
@Override
public int hashCode() {
return address.hashCode() ^ identityKey.hashCode();
}
private static class IdentityKeySerializer extends JsonSerializer<IdentityKey> {
@Override
public void serialize(IdentityKey value, JsonGenerator jsonGenerator, SerializerProvider serializers)
throws IOException
{
jsonGenerator.writeString(Base64.encodeBytes(value.serialize()));
}
}
private static class IdentityKeyDeserializer extends JsonDeserializer<IdentityKey> {
@Override
public IdentityKey deserialize(JsonParser jsonParser, DeserializationContext ctxt)
throws IOException
{
try {
return new IdentityKey(Base64.decode(jsonParser.getValueAsString()), 0);
} catch (InvalidKeyException e) {
Log.w(TAG, e);
throw new IOException(e);
}
}
}
}

View File

@@ -0,0 +1,33 @@
package org.session.libsession.database.documents;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.LinkedList;
import java.util.List;
public class IdentityKeyMismatchList implements Document<IdentityKeyMismatch> {
@JsonProperty(value = "m")
private List<IdentityKeyMismatch> mismatches;
public IdentityKeyMismatchList() {
this.mismatches = new LinkedList<>();
}
public IdentityKeyMismatchList(List<IdentityKeyMismatch> mismatches) {
this.mismatches = mismatches;
}
@Override
public int size() {
if (mismatches == null) return 0;
else return mismatches.size();
}
@Override
@JsonIgnore
public List<IdentityKeyMismatch> getList() {
return mismatches;
}
}

View File

@@ -0,0 +1,36 @@
package org.session.libsession.database.documents;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.session.libsession.messaging.threads.Address;
public class NetworkFailure {
@JsonProperty(value = "a")
private String address;
public NetworkFailure(Address address) {
this.address = address.serialize();
}
public NetworkFailure() {}
@JsonIgnore
public Address getAddress() {
return Address.fromSerialized(address);
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof NetworkFailure)) return false;
NetworkFailure that = (NetworkFailure)other;
return this.address.equals(that.address);
}
@Override
public int hashCode() {
return address.hashCode();
}
}

View File

@@ -0,0 +1,33 @@
package org.session.libsession.database.documents;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.LinkedList;
import java.util.List;
public class NetworkFailureList implements Document<NetworkFailure> {
@JsonProperty(value = "l")
private List<NetworkFailure> failures;
public NetworkFailureList() {
this.failures = new LinkedList<>();
}
public NetworkFailureList(List<NetworkFailure> failures) {
this.failures = failures;
}
@Override
public int size() {
if (failures == null) return 0;
else return failures.size();
}
@Override
@JsonIgnore
public List<NetworkFailure> getList() {
return failures;
}
}

View File

@@ -59,6 +59,8 @@ interface StorageProtocol {
fun getThreadID(openGroupID: String): String?
fun getAllOpenGroups(): Map<Long, PublicChat>
fun addOpenGroup(server: String, channel: Long)
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
fun getQuoteServerID(quoteID: Long, publicKey: String): Long?
// Open Group Public Keys
fun getOpenGroupPublicKey(server: String): String?
@@ -94,10 +96,9 @@ interface StorageProtocol {
fun persistAttachments(messageId: Long, attachments: List<Attachment>): List<Long>
fun getMessageIdInDatabase(timestamp: Long, author: String): Long?
fun setOpenGroupServerMessageID(messageID: Long, serverID: Long)
fun markAsSent(messageID: Long)
fun markUnidentified(messageID: Long)
fun setErrorMessage(messageID: Long, error: Exception)
fun markAsSent(timestamp: Long, author: String)
fun markUnidentified(timestamp: Long, author: String)
fun setErrorMessage(timestamp: Long, author: String, error: Exception)
// Closed Groups
fun getGroup(groupID: String): GroupRecord?
@@ -112,9 +113,9 @@ interface StorageProtocol {
fun addClosedGroupEncryptionKeyPair(encryptionKeyPair: ECKeyPair, groupPublicKey: String)
fun removeAllClosedGroupEncryptionKeyPairs(groupPublicKey: String)
fun insertIncomingInfoMessage(context: Context, senderPublicKey: String, groupID: String, type0: SignalServiceProtos.GroupContext.Type, type1: SignalServiceGroup.Type,
name: String, members: Collection<String>, admins: Collection<String>)
name: String, members: Collection<String>, admins: Collection<String>, sentTimestamp: Long)
fun insertOutgoingInfoMessage(context: Context, groupID: String, type: SignalServiceProtos.GroupContext.Type, name: String,
members: Collection<String>, admins: Collection<String>, threadID: Long)
members: Collection<String>, admins: Collection<String>, threadID: Long, sentTimestamp: Long)
fun isClosedGroup(publicKey: String): Boolean
fun getClosedGroupEncryptionKeyPairs(groupPublicKey: String): MutableList<ECKeyPair>
fun getLatestClosedGroupEncryptionKeyPair(groupPublicKey: String): ECKeyPair?

View File

@@ -1,40 +0,0 @@
package org.session.libsession.messaging.avatars;
import androidx.annotation.NonNull;
import org.session.libsession.utilities.color.MaterialColor;
/**
* Used for migrating legacy colors to modern colors. For normal color generation, use
* {@link ContactColors}.
*/
public class ContactColorsLegacy {
private static final String[] LEGACY_PALETTE = new String[] {
"red",
"pink",
"purple",
"deep_purple",
"indigo",
"blue",
"light_blue",
"cyan",
"teal",
"green",
"light_green",
"orange",
"deep_orange",
"amber",
"blue_grey"
};
public static MaterialColor generateFor(@NonNull String name) {
String serialized = LEGACY_PALETTE[Math.abs(name.hashCode()) % LEGACY_PALETTE.length];
try {
return MaterialColor.fromSerialized(serialized);
} catch (MaterialColor.UnknownColorException e) {
return ContactColors.generateFor(name);
}
}
}

View File

@@ -29,7 +29,6 @@ class FileServerAPI(public val server: String, userPublicKey: String, userPrivat
* 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?
val server = "https://file.getsession.org"
public val fileStorageBucketURL = "https://file-static.lokinet.org"
// endregion

View File

@@ -17,7 +17,7 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long)
private val MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024
// Error
internal sealed class Error(val description: String) : Exception() {
internal sealed class Error(val description: String) : Exception(description) {
object NoAttachment : Error("No such attachment.")
}

View File

@@ -8,11 +8,15 @@ import org.session.libsession.messaging.fileserver.FileServerAPI
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.DotNetAPI
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream
import org.session.libsignal.service.api.messages.SignalServiceAttachmentStream
import org.session.libsignal.service.internal.crypto.PaddingInputStream
import org.session.libsignal.service.internal.push.PushAttachmentData
import org.session.libsignal.service.internal.push.http.AttachmentCipherOutputStreamFactory
import org.session.libsignal.service.internal.util.Util
import org.session.libsignal.service.loki.utilities.PlaintextOutputStreamFactory
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.logging.Log
class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job {
@@ -21,14 +25,14 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
override var failureCount: Int = 0
// Error
internal sealed class Error(val description: String) : Exception() {
internal sealed class Error(val description: String) : Exception(description) {
object NoAttachment : Error("No such attachment.")
}
// Settings
override val maxFailureCount: Int = 20
companion object {
val TAG = AttachmentUploadJob::class.qualifiedName
val TAG = AttachmentUploadJob::class.simpleName
val KEY: String = "AttachmentUploadJob"
val maxFailureCount: Int = 20
@@ -41,46 +45,54 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
}
override fun execute() {
try {
val attachmentStream = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
?: return handleFailure(Error.NoAttachment)
ThreadUtils.queue {
try {
val attachment = MessagingConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID)
?: return@queue handleFailure(Error.NoAttachment)
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
val server = openGroup?.server ?: FileServerAPI.server
var server = FileServerAPI.shared.server
var shouldEncrypt = true
val usePadding = false
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)
openGroup?.let {
server = it.server
shouldEncrypt = false
}
//TODO add some encryption stuff here
val isEncryptionRequired = false
//val isEncryptionRequired = (server == FileServerAPI.server)
val attachmentKey = Util.getSecretBytes(64)
val paddedLength = if (usePadding) PaddingInputStream.getPaddedSize(attachment.length) else attachment.length
val dataStream = if (usePadding) PaddingInputStream(attachment.inputStream, attachment.length) else attachment.inputStream
val ciphertextLength = if (shouldEncrypt) AttachmentCipherOutputStream.getCiphertextLength(paddedLength) else attachment.length
val attachmentKey = Util.getSecretBytes(64)
val outputStreamFactory = if (isEncryptionRequired) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
val ciphertextLength = attachmentStream.length
val outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory()
val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener)
val attachmentData = PushAttachmentData(attachmentStream.contentType, attachmentStream.inputStream, ciphertextLength, outputStreamFactory, attachmentStream.listener)
val uploadResult = FileServerAPI.shared.uploadAttachment(server, attachmentData)
handleSuccess(attachment, attachmentKey, uploadResult)
FileServerAPI.shared.uploadAttachment(server, attachmentData)
} catch (e: java.lang.Exception) {
if (e is Error && e == Error.NoAttachment) {
this.handlePermanentFailure(e)
} else if (e is DotNetAPI.Error && !e.isRetryable) {
this.handlePermanentFailure(e)
} else {
this.handleFailure(e)
} catch (e: java.lang.Exception) {
if (e is Error && e == Error.NoAttachment) {
this.handlePermanentFailure(e)
} else if (e is DotNetAPI.Error && !e.isRetryable) {
this.handlePermanentFailure(e)
} else {
this.handleFailure(e)
}
}
}
}
private fun handleSuccess() {
private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) {
Log.w(TAG, "Attachment uploaded successfully.")
delegate?.handleJobSucceeded(this)
MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult)
MessagingConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID)
//TODO interaction stuff, not sure how to deal with that
}
private fun handlePermanentFailure(e: Exception) {
Log.w(TAG, "Attachment upload failed permanently due to error: $this.")
delegate?.handleJobFailedPermanently(this, e)
MessagingConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID)
failAssociatedMessageSendJob(e)
}
@@ -95,7 +107,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
private fun failAssociatedMessageSendJob(e: Exception) {
val storage = MessagingConfiguration.shared.storage
val messageSendJob = storage.getMessageSendJob(messageSendJobID)
MessageSender.handleFailedMessageSend(this.message!!, e)
MessageSender.handleFailedMessageSend(this.message, e)
if (messageSendJob != null) {
storage.markJobAsFailed(messageSendJob)
}
@@ -119,7 +131,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
}
override fun getFactoryKey(): String {
return AttachmentDownloadJob.KEY
return KEY
}
class Factory: Job.Factory<AttachmentUploadJob> {

View File

@@ -15,7 +15,8 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
// Settings
override val maxFailureCount: Int = 10
companion object {
val TAG = MessageReceiveJob::class.qualifiedName
val TAG = MessageReceiveJob::class.simpleName
val KEY: String = "MessageReceiveJob"
//keys used for database storage purpose
@@ -75,7 +76,7 @@ class MessageReceiveJob(val data: ByteArray, val isBackgroundPoll: Boolean, val
}
override fun getFactoryKey(): String {
return AttachmentDownloadJob.KEY
return KEY
}
class Factory: Job.Factory<MessageReceiveJob> {

View File

@@ -19,7 +19,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
// Settings
override val maxFailureCount: Int = 10
companion object {
val TAG = MessageSendJob::class.qualifiedName
val TAG = MessageSendJob::class.simpleName
val KEY: String = "MessageSendJob"
//keys used for database storage purpose
@@ -32,13 +32,17 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
val message = message as? VisibleMessage
message?.let {
if(!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted
val attachments = message.attachmentIDs.map { messageDataProvider.getAttachmentStream(it) }.filterNotNull()
val attachmentsToUpload = attachments.filter { !it.isUploaded }
val attachmentIDs = mutableListOf<Long>()
attachmentIDs.addAll(message.attachmentIDs)
message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
message.linkPreview?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } }
val attachments = attachmentIDs.mapNotNull { messageDataProvider.getDatabaseAttachment(it) }
val attachmentsToUpload = attachments.filter { it.url.isNullOrEmpty() }
attachmentsToUpload.forEach {
if(MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId) != null) {
if (MessagingConfiguration.shared.storage.getAttachmentUploadJob(it.attachmentId.rowId) != null) {
// Wait for it to finish
} else {
val job = AttachmentUploadJob(it.attachmentId, message.threadID!!.toString(), message, id!!)
val job = AttachmentUploadJob(it.attachmentId.rowId, message.threadID!!.toString(), message, id!!)
JobQueue.shared.add(job)
}
}
@@ -79,21 +83,21 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
//serialize Message and Destination properties
val kryo = Kryo()
kryo.isRegistrationRequired = false
val serializedMessage = ByteArray(4096)
val serializedDestination = ByteArray(4096)
var output = Output(serializedMessage)
kryo.writeObject(output, message)
val output = Output(ByteArray(4096), -1) // maxBufferSize '-1' will dynamically grow internally if we run out of room serializing the message
kryo.writeClassAndObject(output, message)
output.close()
output = Output(serializedDestination)
kryo.writeObject(output, destination)
val serializedMessage = output.toBytes()
output.clear()
kryo.writeClassAndObject(output, destination)
output.close()
val serializedDestination = output.toBytes()
return Data.Builder().putByteArray(KEY_MESSAGE, serializedMessage)
.putByteArray(KEY_DESTINATION, serializedDestination)
.build();
}
override fun getFactoryKey(): String {
return AttachmentDownloadJob.KEY
return KEY
}
class Factory: Job.Factory<MessageSendJob> {
@@ -103,10 +107,10 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job {
//deserialize Message and Destination properties
val kryo = Kryo()
var input = Input(serializedMessage)
val message: Message = kryo.readObject(input, Message::class.java)
val message = kryo.readClassAndObject(input) as Message
input.close()
input = Input(serializedDestination)
val destination: Destination = kryo.readObject(input, Destination::class.java)
val destination = kryo.readClassAndObject(input) as Destination
input.close()
return MessageSendJob(message, destination)
}

View File

@@ -76,7 +76,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
}
override fun getFactoryKey(): String {
return AttachmentDownloadJob.KEY
return KEY
}
class Factory: Job.Factory<NotifyPNServerJob> {

View File

@@ -3,26 +3,39 @@ package org.session.libsession.messaging.messages
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.threads.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.service.loki.utilities.toHexString
sealed class Destination {
class Contact(val publicKey: String) : Destination()
class ClosedGroup(val groupPublicKey: String) : Destination()
class OpenGroup(val channel: Long, val server: String) : Destination()
class Contact(var publicKey: String) : Destination() {
internal constructor(): this("")
}
class ClosedGroup(var groupPublicKey: String) : Destination() {
internal constructor(): this("")
}
class OpenGroup(var channel: Long, var server: String) : Destination() {
internal constructor(): this(0, "")
}
companion object {
fun from(address: Address): Destination {
if (address.isContact) {
return Contact(address.contactIdentifier())
} else if (address.isClosedGroup) {
val groupID = address.contactIdentifier()
val groupPublicKey = GroupUtil.getDecodedGroupID(groupID)
return ClosedGroup(groupPublicKey)
} else if (address.isOpenGroup) {
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(address.contactIdentifier())!!
return OpenGroup(openGroup.channel, openGroup.server)
} else {
throw Exception("TODO: Handle legacy closed groups.")
return when {
address.isContact -> {
Contact(address.contactIdentifier())
}
address.isClosedGroup -> {
val groupID = address.toGroupString()
val groupPublicKey = GroupUtil.doubleDecodeGroupID(groupID).toHexString()
ClosedGroup(groupPublicKey)
}
address.isOpenGroup -> {
val threadID = MessagingConfiguration.shared.storage.getThreadID(address.contactIdentifier())!!
val openGroup = MessagingConfiguration.shared.storage.getOpenGroup(threadID)!!
OpenGroup(openGroup.channel, openGroup.server)
}
else -> {
throw Exception("TODO: Handle legacy closed groups.")
}
}
}
}

View File

@@ -1,5 +1,7 @@
package org.session.libsession.messaging.messages
import com.google.protobuf.ByteString
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.service.internal.push.SignalServiceProtos
abstract class Message {
@@ -18,11 +20,23 @@ abstract class Message {
// validation
open fun isValid(): Boolean {
sentTimestamp = if (sentTimestamp!! > 0) sentTimestamp else return false
receivedTimestamp = if (receivedTimestamp!! > 0) receivedTimestamp else return false
sentTimestamp?.let {
if (it <= 0) return false
}
receivedTimestamp?.let {
if (it <= 0) return false
}
return sender != null && recipient != null
}
abstract fun toProto(): SignalServiceProtos.Content?
fun setGroupContext(dataMessage: SignalServiceProtos.DataMessage.Builder) {
val groupProto = SignalServiceProtos.GroupContext.newBuilder()
val groupID = GroupUtil.doubleEncodeGroupID(recipient!!)
groupProto.id = ByteString.copyFrom(GroupUtil.getDecodedGroupIDAsData(groupID))
groupProto.type = SignalServiceProtos.GroupContext.Type.DELIVER
dataMessage.group = groupProto.build()
}
}

View File

@@ -1,12 +1,17 @@
package org.session.libsession.messaging.messages.control
import com.google.protobuf.ByteString
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.session.libsignal.utilities.Hex
@@ -25,18 +30,30 @@ class ClosedGroupControlMessage() : ControlMessage() {
// Kind enum
sealed class Kind {
class New(val publicKey: ByteString, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<ByteString>, val admins: List<ByteString>) : Kind()
class New(var publicKey: ByteString, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<ByteString>, var admins: List<ByteString>) : Kind() {
internal constructor(): this(ByteString.EMPTY, "", null, listOf(), listOf())
}
/// - Note: Deprecated in favor of more explicit group updates.
class Update(val name: String, val members: List<ByteString>) : Kind()
class Update(var name: String, var members: List<ByteString>) : Kind() {
internal constructor(): this("", listOf())
}
/// An encryption key pair encrypted for each member individually.
///
/// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
class EncryptionKeyPair(val publicKey: ByteString?, val wrappers: Collection<KeyPairWrapper>) : Kind()
class NameChange(val name: String) : Kind()
class MembersAdded(val members: List<ByteString>) : Kind()
class MembersRemoved( val members: List<ByteString>) : Kind()
object MemberLeft : Kind()
object EncryptionKeyPairRequest: Kind()
class EncryptionKeyPair(var publicKey: ByteString?, var wrappers: Collection<KeyPairWrapper>) : Kind() {
internal constructor(): this(null, listOf())
}
class NameChange(var name: String) : Kind() {
internal constructor(): this("")
}
class MembersAdded(var members: List<ByteString>) : Kind() {
internal constructor(): this(listOf())
}
class MembersRemoved(var members: List<ByteString>) : Kind() {
internal constructor(): this(listOf())
}
class MemberLeft() : Kind()
class EncryptionKeyPairRequest(): Kind()
val description: String =
when(this) {
@@ -46,8 +63,8 @@ class ClosedGroupControlMessage() : ControlMessage() {
is NameChange -> "nameChange"
is MembersAdded -> "membersAdded"
is MembersRemoved -> "membersRemoved"
MemberLeft -> "memberLeft"
EncryptionKeyPairRequest -> "encryptionKeyPairRequest"
is MemberLeft -> "memberLeft"
is EncryptionKeyPairRequest -> "encryptionKeyPairRequest"
}
}
@@ -91,10 +108,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
kind = Kind.MembersRemoved(closedGroupControlMessageProto.membersList)
}
DataMessage.ClosedGroupControlMessage.Type.MEMBER_LEFT -> {
kind = Kind.MemberLeft
kind = Kind.MemberLeft()
}
DataMessage.ClosedGroupControlMessage.Type.ENCRYPTION_KEY_PAIR_REQUEST -> {
kind = Kind.EncryptionKeyPairRequest
kind = Kind.EncryptionKeyPairRequest()
}
}
return ClosedGroupControlMessage(kind)
@@ -112,8 +129,8 @@ class ClosedGroupControlMessage() : ControlMessage() {
val kind = kind ?: return false
return when(kind) {
is Kind.New -> {
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair.publicKey != null
&& kind.encryptionKeyPair.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
!kind.publicKey.isEmpty && kind.name.isNotEmpty() && kind.encryptionKeyPair!!.publicKey != null
&& kind.encryptionKeyPair!!.privateKey != null && kind.members.isNotEmpty() && kind.admins.isNotEmpty()
}
is Kind.Update -> kind.name.isNotEmpty()
is Kind.EncryptionKeyPair -> true
@@ -138,16 +155,10 @@ class ClosedGroupControlMessage() : ControlMessage() {
closedGroupControlMessage.type = DataMessage.ClosedGroupControlMessage.Type.NEW
closedGroupControlMessage.publicKey = kind.publicKey
closedGroupControlMessage.name = kind.name
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(kind.encryptionKeyPair.publicKey.serialize())
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(kind.encryptionKeyPair.privateKey.serialize())
try {
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPairAsProto.build()
} catch (e: Exception) {
Log.w(TAG, "Couldn't construct closed group update proto from: $this")
return null
}
val encryptionKeyPair = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPair.publicKey = ByteString.copyFrom(kind.encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPair.privateKey = ByteString.copyFrom(kind.encryptionKeyPair!!.privateKey.serialize())
closedGroupControlMessage.encryptionKeyPair = encryptionKeyPair.build()
closedGroupControlMessage.addAllMembers(kind.members)
closedGroupControlMessage.addAllAdmins(kind.admins)
}
@@ -184,6 +195,11 @@ class ClosedGroupControlMessage() : ControlMessage() {
val dataMessageProto = DataMessage.newBuilder()
dataMessageProto.closedGroupControlMessage = closedGroupControlMessage.build()
// Group context
setGroupContext(dataMessageProto)
// Expiration timer
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
// if it receives a message without the current expiration timer value attached to it...
dataMessageProto.expireTimer = Recipient.from(MessagingConfiguration.shared.context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
contentProto.dataMessage = dataMessageProto.build()
return contentProto.build()
} catch (e: Exception) {

View File

@@ -14,11 +14,13 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.service.loki.utilities.toHexString
import org.session.libsignal.utilities.Hex
class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups: List<String>, val contacts: List<Contact>, val displayName: String, val profilePicture: String?, val profileKey: ByteArray): ControlMessage() {
class ConfigurationMessage(var closedGroups: List<ClosedGroup>, var openGroups: List<String>, var contacts: List<Contact>, var displayName: String, var profilePicture: String?, var profileKey: ByteArray): ControlMessage() {
class ClosedGroup(val publicKey: String, val name: String, val encryptionKeyPair: ECKeyPair, val members: List<String>, val admins: List<String>) {
class ClosedGroup(var publicKey: String, var name: String, var encryptionKeyPair: ECKeyPair?, var members: List<String>, var admins: List<String>) {
val isValid: Boolean get() = members.isNotEmpty() && admins.isNotEmpty()
internal constructor(): this("", "", null, listOf(), listOf())
override fun toString(): String {
return name
}
@@ -30,7 +32,7 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
val name = proto.name
val encryptionKeyPairAsProto = proto.encryptionKeyPair
val encryptionKeyPair = ECKeyPair(DjbECPublicKey(encryptionKeyPairAsProto.publicKey.toByteArray().removing05PrefixIfNeeded()),
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
DjbECPrivateKey(encryptionKeyPairAsProto.privateKey.toByteArray()))
val members = proto.membersList.map { it.toByteArray().toHexString() }
val admins = proto.adminsList.map { it.toByteArray().toHexString() }
return ClosedGroup(publicKey, name, encryptionKeyPair, members, admins)
@@ -42,8 +44,8 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
result.publicKey = ByteString.copyFrom(Hex.fromStringCondensed(publicKey))
result.name = name
val encryptionKeyPairAsProto = SignalServiceProtos.KeyPair.newBuilder()
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
encryptionKeyPairAsProto.publicKey = ByteString.copyFrom(encryptionKeyPair!!.publicKey.serialize().removing05PrefixIfNeeded())
encryptionKeyPairAsProto.privateKey = ByteString.copyFrom(encryptionKeyPair!!.privateKey.serialize())
result.encryptionKeyPair = encryptionKeyPairAsProto.build()
result.addAllMembers(members.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
result.addAllAdmins(admins.map { ByteString.copyFrom(Hex.fromStringCondensed(it)) })
@@ -51,7 +53,10 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
}
}
class Contact(val publicKey: String, val name: String, val profilePicture: String?, val profileKey: ByteArray?) {
class Contact(var publicKey: String, var name: String, var profilePicture: String?, var profileKey: ByteArray?) {
internal constructor(): this("", "", null, null)
companion object {
fun fromProto(proto: SignalServiceProtos.ConfigurationMessage.Contact): Contact? {
if (!proto.hasName() || !proto.hasProfileKey()) return null
@@ -128,6 +133,8 @@ class ConfigurationMessage(val closedGroups: List<ClosedGroup>, val openGroups:
}
}
internal constructor(): this(listOf(), listOf(), listOf(), "", null, byteArrayOf())
override fun toProto(): SignalServiceProtos.Content? {
val configurationProto = SignalServiceProtos.ConfigurationMessage.newBuilder()
configurationProto.addAllClosedGroups(closedGroups.mapNotNull { it.toProto() })

View File

@@ -1,10 +1,13 @@
package org.session.libsession.messaging.messages.control
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.internal.push.SignalServiceProtos
class ExpirationTimerUpdate() : ControlMessage() {
var syncTarget: String? = null
var duration: Int? = 0
companion object {
@@ -12,7 +15,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 //TODO validate that 'and' operator equivalent to Swift '&'
val isExpirationTimerUpdate = dataMessageProto.flags.and(SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0
if (!isExpirationTimerUpdate) return null
val duration = dataMessageProto.expireTimer
return ExpirationTimerUpdate(duration)
@@ -39,6 +42,16 @@ class ExpirationTimerUpdate() : ControlMessage() {
val dataMessageProto = SignalServiceProtos.DataMessage.newBuilder()
dataMessageProto.flags = SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE
dataMessageProto.expireTimer = duration
syncTarget?.let { dataMessageProto.syncTarget = it }
// Group context
if (MessagingConfiguration.shared.storage.isClosedGroup(recipient!!)) {
try {
setGroupContext(dataMessageProto)
} catch(e: Exception) {
Log.w(VisibleMessage.TAG, "Couldn't construct visible message proto from: $this")
return null
}
}
val contentProto = SignalServiceProtos.Content.newBuilder()
try {
contentProto.dataMessage = dataMessageProto.build()

View File

@@ -5,7 +5,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos
class ReadReceipt() : ControlMessage() {
var timestamps: LongArray? = null
var timestamps: List<Long>? = null
companion object {
const val TAG = "ReadReceipt"
@@ -15,12 +15,12 @@ class ReadReceipt() : ControlMessage() {
if (receiptProto.type != SignalServiceProtos.ReceiptMessage.Type.READ) return null
val timestamps = receiptProto.timestampList
if (timestamps.isEmpty()) return null
return ReadReceipt(timestamps = timestamps.toLongArray())
return ReadReceipt(timestamps = timestamps)
}
}
//constructor
internal constructor(timestamps: LongArray?) : this() {
internal constructor(timestamps: List<Long>?) : this() {
this.timestamps = timestamps
}

View File

@@ -0,0 +1,13 @@
package org.session.libsession.messaging.messages.signal;
public class IncomingEncryptedMessage extends IncomingTextMessage {
public IncomingEncryptedMessage(IncomingTextMessage base, String newBody) {
super(base, newBody);
}
@Override
public boolean isSecureMessage() {
return true;
}
}

View File

@@ -0,0 +1,27 @@
package org.session.libsession.messaging.messages.signal;
import static org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
public class IncomingGroupMessage extends IncomingTextMessage {
private final GroupContext groupContext;
public IncomingGroupMessage(IncomingTextMessage base, GroupContext groupContext, String body) {
super(base, body);
this.groupContext = groupContext;
}
@Override
public boolean isGroup() {
return true;
}
public boolean isUpdate() {
return groupContext.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
}
public boolean isQuit() {
return groupContext.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
}
}

View File

@@ -0,0 +1,134 @@
package org.session.libsession.messaging.messages.signal;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.utilities.GroupUtil;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.messages.SignalServiceAttachment;
import org.session.libsignal.service.api.messages.SignalServiceGroup;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class IncomingMediaMessage {
private final Address from;
private final Address groupId;
private final String body;
private final boolean push;
private final long sentTimeMillis;
private final int subscriptionId;
private final long expiresIn;
private final boolean expirationUpdate;
private final QuoteModel quote;
private final boolean unidentified;
private final List<Attachment> attachments = new LinkedList<>();
private final List<Contact> sharedContacts = new LinkedList<>();
private final List<LinkPreview> linkPreviews = new LinkedList<>();
public IncomingMediaMessage(Address from,
long sentTimeMillis,
int subscriptionId,
long expiresIn,
boolean expirationUpdate,
boolean unidentified,
Optional<String> body,
Optional<SignalServiceGroup> group,
Optional<List<SignalServiceAttachment>> attachments,
Optional<QuoteModel> quote,
Optional<List<Contact>> sharedContacts,
Optional<List<LinkPreview>> linkPreviews)
{
this.push = true;
this.from = from;
this.sentTimeMillis = sentTimeMillis;
this.body = body.orNull();
this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.expirationUpdate = expirationUpdate;
this.quote = quote.orNull();
this.unidentified = unidentified;
if (group.isPresent()) this.groupId = Address.fromSerialized(GroupUtil.INSTANCE.getEncodedId(group.get()));
else this.groupId = null;
this.attachments.addAll(PointerAttachment.forPointers(attachments));
this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
this.linkPreviews.addAll(linkPreviews.or(Collections.emptyList()));
}
public static IncomingMediaMessage from(VisibleMessage message,
Address from,
long expiresIn,
Optional<SignalServiceGroup> group,
Optional<List<SignalServiceAttachment>> attachments,
Optional<QuoteModel> quote,
Optional<List<LinkPreview>> linkPreviews)
{
return new IncomingMediaMessage(from, message.getReceivedTimestamp(), -1, expiresIn, false,
false, Optional.fromNullable(message.getText()), group, attachments, quote, Optional.absent(), linkPreviews);
}
public int getSubscriptionId() {
return subscriptionId;
}
public String getBody() {
return body;
}
public List<Attachment> getAttachments() {
return attachments;
}
public Address getFrom() {
return from;
}
public Address getGroupId() {
return groupId;
}
public boolean isPushMessage() {
return push;
}
public boolean isExpirationUpdate() {
return expirationUpdate;
}
public long getSentTimeMillis() {
return sentTimeMillis;
}
public long getExpiresIn() {
return expiresIn;
}
public boolean isGroupMessage() {
return groupId != null;
}
public QuoteModel getQuote() {
return quote;
}
public List<Contact> getSharedContacts() {
return sharedContacts;
}
public List<LinkPreview> getLinkPreviews() {
return linkPreviews;
}
public boolean isUnidentified() {
return unidentified;
}
}

View File

@@ -0,0 +1,185 @@
package org.session.libsession.messaging.messages.signal;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.threads.Address;
import org.session.libsession.utilities.GroupUtil;
import org.session.libsignal.libsignal.util.guava.Optional;
import org.session.libsignal.service.api.messages.SignalServiceGroup;
public class IncomingTextMessage implements Parcelable {
public static final Parcelable.Creator<IncomingTextMessage> CREATOR = new Parcelable.Creator<IncomingTextMessage>() {
@Override
public IncomingTextMessage createFromParcel(Parcel in) {
return new IncomingTextMessage(in);
}
@Override
public IncomingTextMessage[] newArray(int size) {
return new IncomingTextMessage[size];
}
};
private static final String TAG = IncomingTextMessage.class.getSimpleName();
private final String message;
private Address sender;
private final int senderDeviceId;
private final int protocol;
private final String serviceCenterAddress;
private final boolean replyPathPresent;
private final String pseudoSubject;
private final long sentTimestampMillis;
private final Address groupId;
private final boolean push;
private final int subscriptionId;
private final long expiresInMillis;
private final boolean unidentified;
public IncomingTextMessage(Address sender, int senderDeviceId, long sentTimestampMillis,
String encodedBody, Optional<SignalServiceGroup> group,
long expiresInMillis, boolean unidentified)
{
this.message = encodedBody;
this.sender = sender;
this.senderDeviceId = senderDeviceId;
this.protocol = 31337;
this.serviceCenterAddress = "GCM";
this.replyPathPresent = true;
this.pseudoSubject = "";
this.sentTimestampMillis = sentTimestampMillis;
this.push = true;
this.subscriptionId = -1;
this.expiresInMillis = expiresInMillis;
this.unidentified = unidentified;
if (group.isPresent()) {
this.groupId = Address.fromSerialized(GroupUtil.getEncodedId(group.get()));
} else {
this.groupId = null;
}
}
public IncomingTextMessage(Parcel in) {
this.message = in.readString();
this.sender = in.readParcelable(IncomingTextMessage.class.getClassLoader());
this.senderDeviceId = in.readInt();
this.protocol = in.readInt();
this.serviceCenterAddress = in.readString();
this.replyPathPresent = (in.readInt() == 1);
this.pseudoSubject = in.readString();
this.sentTimestampMillis = in.readLong();
this.groupId = in.readParcelable(IncomingTextMessage.class.getClassLoader());
this.push = (in.readInt() == 1);
this.subscriptionId = in.readInt();
this.expiresInMillis = in.readLong();
this.unidentified = in.readInt() == 1;
}
public IncomingTextMessage(IncomingTextMessage base, String newBody) {
this.message = newBody;
this.sender = base.getSender();
this.senderDeviceId = base.getSenderDeviceId();
this.protocol = base.getProtocol();
this.serviceCenterAddress = base.getServiceCenterAddress();
this.replyPathPresent = base.isReplyPathPresent();
this.pseudoSubject = base.getPseudoSubject();
this.sentTimestampMillis = base.getSentTimestampMillis();
this.groupId = base.getGroupId();
this.push = base.isPush();
this.subscriptionId = base.getSubscriptionId();
this.expiresInMillis = base.getExpiresIn();
this.unidentified = base.isUnidentified();
}
public static IncomingTextMessage from(VisibleMessage message,
Address sender,
Optional<SignalServiceGroup> group,
long expiresInMillis)
{
return new IncomingTextMessage(sender, 1, message.getReceivedTimestamp(), message.getText(), group, expiresInMillis, false);
}
public int getSubscriptionId() {
return subscriptionId;
}
public long getExpiresIn() {
return expiresInMillis;
}
public long getSentTimestampMillis() {
return sentTimestampMillis;
}
public String getPseudoSubject() {
return pseudoSubject;
}
public String getMessageBody() {
return message;
}
public Address getSender() {
return sender;
}
public int getSenderDeviceId() {
return senderDeviceId;
}
public int getProtocol() {
return protocol;
}
public String getServiceCenterAddress() {
return serviceCenterAddress;
}
public boolean isReplyPathPresent() {
return replyPathPresent;
}
public boolean isSecureMessage() {
return false;
}
public boolean isPush() {
return push;
}
public @Nullable Address getGroupId() {
return groupId;
}
public boolean isGroup() {
return false;
}
public boolean isUnidentified() {
return unidentified;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeString(message);
out.writeParcelable(sender, flags);
out.writeInt(senderDeviceId);
out.writeInt(protocol);
out.writeString(serviceCenterAddress);
out.writeInt(replyPathPresent ? 1 : 0);
out.writeString(pseudoSubject);
out.writeLong(sentTimestampMillis);
out.writeParcelable(groupId, flags);
out.writeInt(push ? 1 : 0);
out.writeInt(subscriptionId);
out.writeInt(unidentified ? 1 : 0);
}
}

View File

@@ -0,0 +1,29 @@
package org.session.libsession.messaging.messages.signal;
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.session.libsession.messaging.threads.recipients.Recipient;
import java.util.Collections;
import java.util.LinkedList;
public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage {
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
DistributionTypes.CONVERSATION, expiresIn, null, Collections.emptyList(),
Collections.emptyList());
}
public static OutgoingExpirationUpdateMessage from(ExpirationTimerUpdate message,
Recipient recipient) {
return new OutgoingExpirationUpdateMessage(recipient, message.getSentTimestamp(), message.getDuration() * 1000);
}
@Override
public boolean isExpirationUpdate() {
return true;
}
}

View File

@@ -0,0 +1,89 @@
package org.session.libsession.messaging.messages.signal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.threads.recipients.Recipient;
import org.session.libsignal.utilities.Base64;
import org.session.libsignal.service.internal.push.SignalServiceProtos.GroupContext;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
private final GroupContext group;
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
@NonNull String encodedGroupContext,
@NonNull List<Attachment> avatar,
long sentTimeMillis,
long expiresIn,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> previews)
throws IOException
{
super(recipient, encodedGroupContext, avatar, sentTimeMillis,
DistributionTypes.CONVERSATION, expiresIn, quote, contacts, previews);
this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
}
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
@NonNull GroupContext group,
@Nullable final Attachment avatar,
long expireIn,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> previews)
{
super(recipient, Base64.encodeBytes(group.toByteArray()),
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
System.currentTimeMillis(),
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
this.group = group;
}
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
@NonNull GroupContext group,
@Nullable final Attachment avatar,
long sentTime,
long expireIn,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> previews)
{
super(recipient, Base64.encodeBytes(group.toByteArray()),
new LinkedList<Attachment>() {{if (avatar != null) add(avatar);}},
sentTime,
DistributionTypes.CONVERSATION, expireIn, quote, contacts, previews);
this.group = group;
}
@Override
public boolean isGroup() {
return true;
}
public boolean isGroupUpdate() {
return group.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
}
public boolean isGroupQuit() {
return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
}
public GroupContext getGroupContext() {
return group;
}
}

View File

@@ -0,0 +1,140 @@
package org.session.libsession.messaging.messages.signal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.threads.DistributionTypes;
import org.session.libsession.database.documents.IdentityKeyMismatch;
import org.session.libsession.database.documents.NetworkFailure;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.threads.recipients.Recipient;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class OutgoingMediaMessage {
private final Recipient recipient;
protected final String body;
protected final List<Attachment> attachments;
private final long sentTimeMillis;
private final int distributionType;
private final int subscriptionId;
private final long expiresIn;
private final QuoteModel outgoingQuote;
private final List<NetworkFailure> networkFailures = new LinkedList<>();
private final List<IdentityKeyMismatch> identityKeyMismatches = new LinkedList<>();
private final List<Contact> contacts = new LinkedList<>();
private final List<LinkPreview> linkPreviews = new LinkedList<>();
public OutgoingMediaMessage(Recipient recipient, String message,
List<Attachment> attachments, long sentTimeMillis,
int subscriptionId, long expiresIn,
int distributionType,
@Nullable QuoteModel outgoingQuote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> linkPreviews,
@NonNull List<NetworkFailure> networkFailures,
@NonNull List<IdentityKeyMismatch> identityKeyMismatches)
{
this.recipient = recipient;
this.body = message;
this.sentTimeMillis = sentTimeMillis;
this.distributionType = distributionType;
this.attachments = attachments;
this.subscriptionId = subscriptionId;
this.expiresIn = expiresIn;
this.outgoingQuote = outgoingQuote;
this.contacts.addAll(contacts);
this.linkPreviews.addAll(linkPreviews);
this.networkFailures.addAll(networkFailures);
this.identityKeyMismatches.addAll(identityKeyMismatches);
}
public OutgoingMediaMessage(OutgoingMediaMessage that) {
this.recipient = that.getRecipient();
this.body = that.body;
this.distributionType = that.distributionType;
this.attachments = that.attachments;
this.sentTimeMillis = that.sentTimeMillis;
this.subscriptionId = that.subscriptionId;
this.expiresIn = that.expiresIn;
this.outgoingQuote = that.outgoingQuote;
this.identityKeyMismatches.addAll(that.identityKeyMismatches);
this.networkFailures.addAll(that.networkFailures);
this.contacts.addAll(that.contacts);
this.linkPreviews.addAll(that.linkPreviews);
}
public static OutgoingMediaMessage from(VisibleMessage message,
Recipient recipient,
List<Attachment> attachments,
@Nullable QuoteModel outgoingQuote,
@Nullable LinkPreview linkPreview)
{
List<LinkPreview> previews = Collections.emptyList();
if (linkPreview != null) {
previews = Collections.singletonList(linkPreview);
}
return new OutgoingMediaMessage(recipient, message.getText(), attachments, message.getSentTimestamp(), -1,
recipient.getExpireMessages() * 1000, DistributionTypes.DEFAULT, outgoingQuote, Collections.emptyList(),
previews, Collections.emptyList(), Collections.emptyList());
}
public Recipient getRecipient() {
return recipient;
}
public String getBody() {
return body;
}
public List<Attachment> getAttachments() {
return attachments;
}
public boolean isSecure() {
return true;
}
public boolean isGroup() {
return false;
}
public boolean isExpirationUpdate() {
return false;
}
public long getSentTimeMillis() {
return sentTimeMillis;
}
public int getSubscriptionId() {
return subscriptionId;
}
public long getExpiresIn() {
return expiresIn;
}
public @Nullable QuoteModel getOutgoingQuote() {
return outgoingQuote;
}
public @NonNull List<Contact> getSharedContacts() {
return contacts;
}
public @NonNull List<LinkPreview> getLinkPreviews() {
return linkPreviews;
}
}

View File

@@ -0,0 +1,37 @@
package org.session.libsession.messaging.messages.signal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.messaging.sending_receiving.attachments.Attachment;
import org.session.libsession.messaging.sending_receiving.sharecontacts.Contact;
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview;
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel;
import org.session.libsession.messaging.threads.recipients.Recipient;
import java.util.Collections;
import java.util.List;
public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
public OutgoingSecureMediaMessage(Recipient recipient, String body,
List<Attachment> attachments,
long sentTimeMillis,
int distributionType,
long expiresIn,
@Nullable QuoteModel quote,
@NonNull List<Contact> contacts,
@NonNull List<LinkPreview> previews)
{
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
}
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
super(base);
}
@Override
public boolean isSecure() {
return true;
}
}

View File

@@ -0,0 +1,43 @@
package org.session.libsession.messaging.messages.signal;
import org.session.libsession.messaging.messages.visible.VisibleMessage;
import org.session.libsession.messaging.threads.recipients.Recipient;
public class OutgoingTextMessage {
private final Recipient recipient;
private final String message;
private final int subscriptionId;
private final long expiresIn;
public OutgoingTextMessage(Recipient recipient, String message, long expiresIn, int subscriptionId) {
this.recipient = recipient;
this.message = message;
this.expiresIn = expiresIn;
this.subscriptionId = subscriptionId;
}
public static OutgoingTextMessage from(VisibleMessage message, Recipient recipient) {
return new OutgoingTextMessage(recipient, message.getText(), recipient.getExpireMessages() * 1000, -1);
}
public long getExpiresIn() {
return expiresIn;
}
public int getSubscriptionId() {
return subscriptionId;
}
public String getMessageBody() {
return message;
}
public Recipient getRecipient() {
return recipient;
}
public boolean isSecureMessage() {
return true;
}
}

View File

@@ -2,8 +2,11 @@ package org.session.libsession.messaging.messages.visible
import android.util.Size
import android.webkit.MimeTypeMap
import org.session.libsession.database.MessageDataProvider
import com.google.protobuf.ByteString
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsignal.service.api.messages.SignalServiceAttachmentPointer
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
import org.session.libsignal.service.internal.push.SignalServiceProtos
import java.io.File
@@ -32,7 +35,7 @@ class Attachment {
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) { //TODO validate that 'and' operator = swift '&'
val kind: Kind = if (proto.hasFlags() && proto.flags.and(SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE) > 0) {
Kind.VOICE_MESSAGE
} else {
Kind.GENERIC
@@ -42,13 +45,42 @@ class Attachment {
val size: Size = if (proto.hasWidth() && proto.width > 0 && proto.hasHeight() && proto.height > 0) {
Size(proto.width, proto.height)
} else {
Size(0,0) //TODO check that it's equivalent to swift: CGSize.zero
Size(0,0)
}
result.size = size
result.sizeInBytes = if (proto.size > 0) proto.size else null
result. url = proto.url
return result
}
fun createAttachmentPointer(attachment: SignalServiceAttachmentPointer): SignalServiceProtos.AttachmentPointer? {
val builder = SignalServiceProtos.AttachmentPointer.newBuilder()
.setContentType(attachment.contentType)
.setId(attachment.id)
.setKey(ByteString.copyFrom(attachment.key))
.setDigest(ByteString.copyFrom(attachment.digest.get()))
.setSize(attachment.size.get())
.setUrl(attachment.url)
if (attachment.fileName.isPresent) {
builder.fileName = attachment.fileName.get()
}
if (attachment.preview.isPresent) {
builder.thumbnail = ByteString.copyFrom(attachment.preview.get())
}
if (attachment.width > 0) {
builder.width = attachment.width
}
if (attachment.height > 0) {
builder.height = attachment.height
}
if (attachment.voiceNote) {
builder.flags = SignalServiceProtos.AttachmentPointer.Flags.VOICE_MESSAGE_VALUE
}
if (attachment.caption.isPresent) {
builder.caption = attachment.caption.get()
}
return builder.build()
}
}
enum class Kind {
@@ -66,8 +98,9 @@ class Attachment {
TODO("Not implemented")
}
fun toDatabaseAttachment(): org.session.libsession.messaging.sending_receiving.attachments.Attachment {
return DatabaseAttachment(null, 0, true, true, contentType, 0,
fun toSignalAttachment(): SignalAttachment? {
if (!isValid()) return null
return DatabaseAttachment(null, 0, false, false, contentType, 0,
sizeInBytes?.toLong() ?: 0, fileName, null, key.toString(), null, digest, null, kind == Kind.VOICE_MESSAGE,
size?.width ?: 0, size?.height ?: 0, false, caption, url)
}

View File

@@ -1,6 +1,7 @@
package org.session.libsession.messaging.messages.visible
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview as SignalLinkPreiview
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.internal.push.SignalServiceProtos
@@ -18,6 +19,14 @@ class LinkPreview() {
val url = proto.url
return LinkPreview(title, url, null)
}
fun from(signalLinkPreview: SignalLinkPreiview?): LinkPreview? {
return if (signalLinkPreview == null) {
null
} else {
LinkPreview(signalLinkPreview.title, signalLinkPreview.url, signalLinkPreview.attachmentId?.rowId)
}
}
}
//constructor
@@ -44,8 +53,10 @@ class LinkPreview() {
title?.let { linkPreviewProto.title = title }
val attachmentID = attachmentID
attachmentID?.let {
val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
attachmentProto?.let { linkPreviewProto.image = attachmentProto.toProto() }
MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID)?.let {
val attachmentProto = Attachment.createAttachmentPointer(it)
linkPreviewProto.image = attachmentProto
}
}
// Build
try {

View File

@@ -2,6 +2,8 @@ package org.session.libsession.messaging.messages.visible
import com.goterl.lazycode.lazysodium.BuildConfig
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.internal.push.SignalServiceProtos
@@ -21,6 +23,15 @@ class Quote() {
val text = proto.text
return Quote(timestamp, publicKey, text, null)
}
fun from(signalQuote: SignalQuote?): Quote? {
return if (signalQuote == null) {
null
} else {
val attachmentID = (signalQuote.attachments?.firstOrNull() as? DatabaseAttachment)?.attachmentId?.rowId
Quote(signalQuote.id, signalQuote.author.serialize(), signalQuote.text, attachmentID)
}
}
}
//constructor
@@ -58,13 +69,13 @@ class Quote() {
}
private fun addAttachmentsIfNeeded(quoteProto: SignalServiceProtos.DataMessage.Quote.Builder) {
val attachmentID = attachmentID ?: return
val attachmentProto = MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(attachmentID)
if (attachmentProto == null) {
if (attachmentID == null) return
val attachment = MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID!!)
if (attachment == null) {
Log.w(TAG, "Ignoring invalid attachment for quoted message.")
return
}
if (!attachmentProto.isUploaded) {
if (attachment.url.isNullOrEmpty()) {
if (BuildConfig.DEBUG) {
//TODO equivalent to iOS's preconditionFailure
Log.d(TAG,"Sending a message before all associated attachments have been uploaded.")
@@ -72,10 +83,9 @@ class Quote() {
}
}
val quotedAttachmentProto = SignalServiceProtos.DataMessage.Quote.QuotedAttachment.newBuilder()
quotedAttachmentProto.contentType = attachmentProto.contentType
val fileName = attachmentProto.fileName?.get()
fileName?.let { quotedAttachmentProto.fileName = fileName }
quotedAttachmentProto.thumbnail = attachmentProto.toProto()
quotedAttachmentProto.contentType = attachment.contentType
if (attachment.fileName.isPresent) quotedAttachmentProto.fileName = attachment.fileName.get()
quotedAttachmentProto.thumbnail = Attachment.createAttachmentPointer(attachment)
try {
quoteProto.addAttachments(quotedAttachmentProto.build())
} catch (e: Exception) {

View File

@@ -1,12 +1,15 @@
package org.session.libsession.messaging.messages.visible
import com.goterl.lazycode.lazysodium.BuildConfig
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.messages.Message
import org.session.libsignal.utilities.logging.Log
import org.session.libsession.messaging.sending_receiving.attachments.DatabaseAttachment
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.threads.recipients.Recipient
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.utilities.logging.Log
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
class VisibleMessage : Message() {
@@ -46,6 +49,14 @@ class VisibleMessage : Message() {
}
}
fun addSignalAttachments(signalAttachments: List<SignalAttachment>) {
val attachmentIDs = signalAttachments.map {
val databaseAttachment = it as DatabaseAttachment
databaseAttachment.attachmentId.rowId
}
this.attachmentIDs = attachmentIDs as ArrayList<Long>
}
fun isMediaMessage(): Boolean {
return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null || contact != null
}
@@ -61,7 +72,6 @@ class VisibleMessage : Message() {
override fun toProto(): SignalServiceProtos.Content? {
val proto = SignalServiceProtos.Content.newBuilder()
var attachmentIDs = this.attachmentIDs
val dataMessage: SignalServiceProtos.DataMessage.Builder
// Profile
val profile = profile
@@ -74,44 +84,49 @@ class VisibleMessage : Message() {
// 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()
val quoteProto = it.toProto()
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()
val linkPreviewProto = it.toProto()
linkPreviewProto?.let {
dataMessage.addAllPreview(listOf(linkPreviewProto))
}
}
//Attachments
val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getAttachmentStream(it) }
if (!attachments.all { it.isUploaded }) {
val attachments = attachmentIDs.mapNotNull { MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) }
if (!attachments.all { !it.url.isNullOrEmpty() }) {
if (BuildConfig.DEBUG) {
//TODO equivalent to iOS's preconditionFailure
Log.d(TAG,"Sending a message before all associated attachments have been uploaded.")
Log.d(TAG, "Sending a message before all associated attachments have been uploaded.")
}
}
val attachmentPointers = attachments.mapNotNull { Attachment.createAttachmentPointer(it) }
dataMessage.addAllAttachments(attachmentPointers)
// TODO Contact
// Expiration timer
// TODO: We * want * expiration timer updates to be explicit. But currently Android will disable the expiration timer for a conversation
// if it receives a message without the current expiration timer value attached to it...
val storage = MessagingConfiguration.shared.storage
val context = MessagingConfiguration.shared.context
val expiration = if (storage.isClosedGroup(recipient!!)) Recipient.from(context, Address.fromSerialized(GroupUtil.doubleEncodeGroupID(recipient!!)), false).expireMessages
else Recipient.from(context, Address.fromSerialized(recipient!!), false).expireMessages
dataMessage.expireTimer = expiration
// Group context
if (storage.isClosedGroup(recipient!!)) {
try {
setGroupContext(dataMessage)
} catch(e: Exception) {
Log.w(TAG, "Couldn't construct visible message proto from: $this")
return null
}
}
val attachmentProtos = attachments.mapNotNull { it.toProto() }
dataMessage.addAllAttachments(attachmentProtos)
// Sync target
if (syncTarget != null) {
dataMessage.syncTarget = syncTarget
}
// TODO Contact
// Build
try {
proto.dataMessage = dataMessage.build()

View File

@@ -41,6 +41,7 @@ object OpenGroupAPI: DotNetAPI() {
return listOf() // Don't auto-join any open groups right now
}
@JvmStatic
fun isUserModerator(hexEncodedPublicKey: String, channel: Long, server: String): Boolean {
if (moderators[server] != null && moderators[server]!![channel] != null) {
return moderators[server]!![channel]!!.contains(hexEncodedPublicKey)
@@ -113,7 +114,7 @@ object OpenGroupAPI: DotNetAPI() {
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 fileName = attachmentAsJSON["fileName"] as? String
val flags = 0
val url = attachmentAsJSON["url"] as String
val caption = attachmentAsJSON["caption"] as? String
@@ -209,8 +210,7 @@ object OpenGroupAPI: DotNetAPI() {
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.first, userDisplayName, text, timestamp, openGroupMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp)
message
OpenGroupMessage(serverID, userKeyPair.first, userDisplayName, text, timestamp, openGroupMessageType, message.quote, message.attachments, null, signedMessage.signature, timestamp)
} catch (exception: Exception) {
Log.d("Loki", "Couldn't parse message for open group with ID: $channel on server: $server.")
throw exception
@@ -238,6 +238,7 @@ object OpenGroupAPI: DotNetAPI() {
}
}
@JvmStatic
fun deleteMessages(messageServerIDs: List<Long>, channel: Long, server: String, isSentByUser: Boolean): Promise<List<Long>, Exception> {
return retryIfNeeded(maxRetryCount) {
val isModerationRequest = !isSentByUser
@@ -338,6 +339,7 @@ object OpenGroupAPI: DotNetAPI() {
}
}
@JvmStatic
fun ban(publicKey: String, server: String): Promise<Unit,Exception> {
return retryIfNeeded(maxRetryCount) {
execute(HTTPVerb.POST, server, "/loki/v1/moderation/blacklist/@$publicKey").then {

View File

@@ -26,7 +26,6 @@ data class OpenGroupMessage(
fun from(message: VisibleMessage, server: String): OpenGroupMessage? {
val storage = MessagingConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey() ?: return null
var attachmentIDs = message.attachmentIDs
// Validation
if (!message.isValid()) { return null } // Should be valid at this point
// Quote
@@ -34,10 +33,8 @@ data class OpenGroupMessage(
val quote = message.quote
if (quote != null && quote.isValid()) {
val quotedMessageBody = quote.text ?: quote.timestamp!!.toString()
val quotedAttachmentID = quote.attachmentID
if (quotedAttachmentID != null) { attachmentIDs.remove(quotedAttachmentID) }
// FIXME: For some reason the server always returns a 500 if quotedMessageServerID is set...
Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, null)
val serverID = storage.getQuoteServerID(quote.timestamp!!, quote.publicKey!!)
Quote(quote.timestamp!!, quote.publicKey!!, quotedMessageBody, serverID)
} else {
null
}
@@ -51,18 +48,18 @@ data class OpenGroupMessage(
linkPreview?.let {
if (!linkPreview.isValid()) { return@let }
val attachmentID = linkPreview.attachmentID ?: return@let
val attachment = MessagingConfiguration.shared.messageDataProvider.getAttachmentPointer(attachmentID) ?: return@let
val attachment = MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(attachmentID) ?: return@let
val openGroupLinkPreview = Attachment(
Attachment.Kind.LinkPreview,
server,
attachment.id,
attachment.contentType!!,
attachment.size.get(),
attachment.fileName.get(),
attachment.fileName.orNull(),
0,
attachment.width,
attachment.height,
attachment.caption.get(),
attachment.caption.orNull(),
attachment.url,
linkPreview.url,
linkPreview.title)
@@ -70,18 +67,18 @@ data class OpenGroupMessage(
}
// Attachments
val attachments = message.attachmentIDs.mapNotNull {
val attachment = MessagingConfiguration.shared.messageDataProvider.getAttachmentPointer(it) ?: return@mapNotNull null
val attachment = MessagingConfiguration.shared.messageDataProvider.getSignalAttachmentPointer(it) ?: return@mapNotNull null
return@mapNotNull Attachment(
Attachment.Kind.Attachment,
server,
attachment.id,
attachment.contentType!!,
attachment.size.get(),
attachment.fileName.get(),
attachment.size.orNull(),
attachment.fileName.orNull() ?: "",
0,
attachment.width,
attachment.height,
attachment.caption.get(),
attachment.caption.orNull(),
attachment.url,
null,
null)
@@ -121,7 +118,7 @@ data class OpenGroupMessage(
val serverID: Long,
val contentType: String,
val size: Int,
val fileName: String,
val fileName: String?,
val flags: Int,
val width: Int,
val height: Int,

View File

@@ -11,7 +11,7 @@ object MessageReceiver {
private val lastEncryptionKeyPairRequest = mutableMapOf<String, Long>()
internal sealed class Error(val description: String) : Exception() {
internal sealed class Error(val description: String) : Exception(description) {
object DuplicateMessage: Error("Duplicate message.")
object InvalidMessage: Error("Invalid message.")
object UnknownMessage: Error("Unknown message type.")

View File

@@ -50,7 +50,7 @@ fun MessageReceiver.handle(message: Message, proto: SignalServiceProtos.Content,
private fun MessageReceiver.handleReadReceipt(message: ReadReceipt) {
val context = MessagingConfiguration.shared.context
SSKEnvironment.shared.readReceiptManager.processReadReceipts(context, message.sender!!, message.timestamps!!.asList(), message.receivedTimestamp!!)
SSKEnvironment.shared.readReceiptManager.processReadReceipts(context, message.sender!!, message.timestamps!!, message.receivedTimestamp!!)
}
private fun MessageReceiver.handleTypingIndicator(message: TypingIndicator) {
@@ -110,7 +110,7 @@ private fun MessageReceiver.handleConfigurationMessage(message: ConfigurationMes
val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys()
for (closeGroup in message.closedGroups) {
if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue
handleNewClosedGroup(message.sender!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!)
}
val allOpenGroups = storage.getAllOpenGroups().map { it.value.server }
for (openGroup in message.openGroups) {
@@ -212,8 +212,8 @@ private fun MessageReceiver.handleClosedGroupControlMessage(message: ClosedGroup
is ClosedGroupControlMessage.Kind.NameChange -> handleClosedGroupNameChanged(message)
is ClosedGroupControlMessage.Kind.MembersAdded -> handleClosedGroupMembersAdded(message)
is ClosedGroupControlMessage.Kind.MembersRemoved -> handleClosedGroupMembersRemoved(message)
ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message)
ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest -> handleClosedGroupEncryptionKeyPairRequest(message)
is ClosedGroupControlMessage.Kind.MemberLeft -> handleClosedGroupMemberLeft(message)
is ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest -> handleClosedGroupEncryptionKeyPairRequest(message)
}
}
@@ -222,11 +222,11 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess
val groupPublicKey = kind.publicKey.toByteArray().toHexString()
val members = kind.members.map { it.toByteArray().toHexString() }
val admins = kind.admins.map { it.toByteArray().toHexString() }
handleNewClosedGroup(message.sender!!, groupPublicKey, kind.name, kind.encryptionKeyPair, members, admins, message.sentTimestamp!!)
handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, groupPublicKey, kind.name, kind.encryptionKeyPair!!, members, admins, message.sentTimestamp!!)
}
// Parameter @sender:String is just for inserting incoming info message
private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List<String>, admins: List<String>, formationTimestamp: Long) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
// Create the group
@@ -239,7 +239,7 @@ private fun handleNewClosedGroup(sender: String, groupPublicKey: String, name: S
storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }),
null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp)
// Notify the user
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
storage.insertIncomingInfoMessage(context, sender, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, sentTimestamp)
}
storage.setProfileSharing(Address.fromSerialized(groupID), true)
// Add the group to the user's set of public keys to poll for
@@ -297,7 +297,7 @@ private fun MessageReceiver.handleClosedGroupUpdated(message: ClosedGroupControl
val wasSenderRemoved = !members.contains(senderPublicKey)
val type0 = if (wasSenderRemoved) SignalServiceProtos.GroupContext.Type.QUIT else SignalServiceProtos.GroupContext.Type.UPDATE
val type1 = if (wasSenderRemoved) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.UPDATE
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() })
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, type0, type1, name, members, group.admins.map { it.toString() }, message.sentTimestamp!!)
}
private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGroupControlMessage) {
@@ -315,7 +315,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr
return
}
if (!group.members.map { it.toString() }.contains(senderPublicKey)) {
android.util.Log.d("Loki", "Ignoring closed group encryption key pair from non-member.")
Log.d("Loki", "Ignoring closed group encryption key pair from non-member.")
return
}
// Find our wrapper and decrypt it if possible
@@ -356,12 +356,13 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon
val name = kind.name
storage.updateTitle(groupID, name)
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
}
private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupControlMessage) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
val senderPublicKey = message.sender ?: return
val kind = message.kind!! as? ClosedGroupControlMessage.Kind.MembersAdded ?: return
val groupPublicKey = message.groupPublicKey ?: return
@@ -370,9 +371,7 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
Log.d("Loki", "Ignoring closed group info message for nonexistent group.")
return
}
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) {
return
}
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
val name = group.title
// Check common group update logic
val members = group.members.map { it.serialize() }
@@ -381,14 +380,25 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo
val updateMembers = kind.members.map { it.toByteArray().toHexString() }
val newMembers = members + updateMembers
storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) })
// Send the latest encryption key pair to the added members if the current user is the admin of the group
val isCurrentUserAdmin = admins.contains(storage.getUserPublicKey()!!)
if (isCurrentUserAdmin) {
for (member in updateMembers) {
MessageSender.sendLatestEncryptionKeyPair(member, groupPublicKey)
if (userPublicKey == senderPublicKey) {
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, message.sentTimestamp!!)
} else {
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
}
if (userPublicKey in admins) {
// send current encryption key to the latest added members
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) {
android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.")
} else {
for (user in updateMembers) {
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(user), targetUser = user, force = false)
}
}
}
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins)
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, SignalServiceGroup.Type.UPDATE, name, members, admins, message.sentTimestamp!!)
}
private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroupControlMessage) {
@@ -437,7 +447,7 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup
if (senderLeft) SignalServiceProtos.GroupContext.Type.QUIT to SignalServiceGroup.Type.QUIT
else SignalServiceProtos.GroupContext.Type.UPDATE to SignalServiceGroup.Type.UPDATE
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins)
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, contextType, signalType, name, members, admins, message.sentTimestamp!!)
}
private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupControlMessage) {
@@ -472,7 +482,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont
MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey, updatedMemberList)
}
}
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins)
storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceProtos.GroupContext.Type.QUIT, SignalServiceGroup.Type.QUIT, name, members, admins, message.sentTimestamp!!)
}
private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: ClosedGroupControlMessage) {
@@ -491,7 +501,13 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPairRequest(message: C
return
}
if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return }
MessageSender.sendLatestEncryptionKeyPair(senderPublicKey, groupPublicKey)
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey)
if (encryptionKeyPair == null) {
Log.d("Loki", "Couldn't get encryption key pair for closed group.")
} else {
MessageSender.sendEncryptionKeyPair(groupPublicKey, encryptionKeyPair, setOf(senderPublicKey), targetUser = senderPublicKey, force = false)
}
}
private fun isValidGroupUpdate(group: GroupRecord,

View File

@@ -1,37 +1,41 @@
package org.session.libsession.messaging.sending_receiving
import android.util.Size
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageSendJob
import org.session.libsession.messaging.jobs.NotifyPNServerJob
import org.session.libsession.messaging.messages.Destination
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.messages.control.ClosedGroupControlMessage
import org.session.libsession.messaging.messages.control.ConfigurationMessage
import org.session.libsession.messaging.messages.visible.Attachment
import org.session.libsession.messaging.messages.visible.Profile
import org.session.libsession.messaging.messages.visible.VisibleMessage
import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate
import org.session.libsession.messaging.messages.visible.*
import org.session.libsession.messaging.opengroups.OpenGroupAPI
import org.session.libsession.messaging.opengroups.OpenGroupMessage
import org.session.libsession.messaging.threads.Address
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.snode.RawResponsePromise
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeConfiguration
import org.session.libsession.snode.SnodeMessage
import org.session.libsession.utilities.SSKEnvironment
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.api.messages.SignalServiceAttachment
import org.session.libsignal.service.internal.push.PushTransportDetails
import org.session.libsignal.service.internal.push.SignalServiceProtos
import org.session.libsignal.utilities.Base64
import org.session.libsignal.service.loki.api.crypto.ProofOfWork
import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.logging.Log
import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment
import org.session.libsession.messaging.sending_receiving.linkpreview.LinkPreview as SignalLinkPreview
import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote
object MessageSender {
// Error
internal sealed class Error(val description: String) : Exception() {
sealed class Error(val description: String) : Exception(description) {
object InvalidMessage : Error("Invalid message.")
object ProtoConversionFailed : Error("Couldn't convert message to proto.")
object ProofOfWorkCalculationFailed : Error("Proof of work calculation failed.")
@@ -46,6 +50,9 @@ object MessageSender {
object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.")
object InvalidClosedGroupUpdate : Error("Invalid group update.")
// Precondition
class PreconditionFailure(val reason: String): Error(reason)
internal val isRetryable: Boolean = when (this) {
is InvalidMessage -> false
is ProtoConversionFailed -> false
@@ -55,29 +62,6 @@ object MessageSender {
}
}
// Preparation
fun prep(signalAttachments: List<SignalServiceAttachment>, message: VisibleMessage) {
// TODO: Deal with SignalServiceAttachmentStream
val attachments = mutableListOf<Attachment>()
for (signalAttachment in signalAttachments) {
val attachment = Attachment()
if (signalAttachment.isPointer) {
val signalAttachmentPointer = signalAttachment.asPointer()
attachment.fileName = signalAttachmentPointer.fileName.orNull()
attachment.caption = signalAttachmentPointer.caption.orNull()
attachment.contentType = signalAttachmentPointer.contentType
attachment.digest = signalAttachmentPointer.digest.orNull()
attachment.key = signalAttachmentPointer.key
attachment.sizeInBytes = signalAttachmentPointer.size.orNull()
attachment.url = signalAttachmentPointer.url
attachment.size = Size(signalAttachmentPointer.width, signalAttachmentPointer.height)
attachments.add(attachment)
}
}
val attachmentIDs = MessagingConfiguration.shared.storage.persistAttachments(message.id ?: 0, attachments)
message.attachmentIDs.addAll(attachmentIDs)
}
// Convenience
fun send(message: Message, destination: Destination): Promise<Unit, Exception> {
if (destination is Destination.OpenGroup) {
@@ -87,30 +71,28 @@ object MessageSender {
}
// One-on-One Chats & Closed Groups
fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
private fun sendToSnodeDestination(destination: Destination, message: Message, isSyncMessage: Boolean = false): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>()
val promise = deferred.promise
val storage = MessagingConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()
val preconditionFailure = Exception("Destination should not be open groups!")
var snodeMessage: SnodeMessage? = null
// Set the timestamp, sender and recipient
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */
message.sender = userPublicKey
val isSelfSend = (message.recipient == userPublicKey)
// Set the failure handler (need it here already for precondition failure handling)
fun handleFailure(error: Exception) {
handleFailedMessageSend(message, error)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeConfiguration.shared.broadcaster.broadcast("messageFailed", message.sentTimestamp!!)
}
deferred.reject(error)
}
try {
when (destination) {
is Destination.Contact -> message.recipient = destination.publicKey
is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey
is Destination.OpenGroup -> throw preconditionFailure
}
val isSelfSend = (message.recipient == userPublicKey)
// Set the failure handler (need it here already for precondition failure handling)
fun handleFailure(error: Exception) {
handleFailedMessageSend(message, error)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
//TODO Notify user for send failure
}
deferred.reject(error)
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
}
// Validate the message
if (!message.isValid()) { throw Error.InvalidMessage }
@@ -139,11 +121,8 @@ object MessageSender {
// Convert it to protobuf
val proto = message.toProto() ?: throw Error.ProtoConversionFailed
// Serialize the protobuf
val plaintext = proto.toByteArray()
val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray())
// Encrypt the serialized protobuf
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
//TODO Notify user for encrypting message
}
val ciphertext: ByteArray
when (destination) {
is Destination.Contact -> ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, destination.publicKey)
@@ -151,7 +130,7 @@ object MessageSender {
val encryptionKeyPair = MessagingConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!!
ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey)
}
is Destination.OpenGroup -> throw preconditionFailure
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
}
// Wrap the result
val kind: SignalServiceProtos.Envelope.Type
@@ -165,19 +144,22 @@ object MessageSender {
kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_CIPHERTEXT
senderPublicKey = destination.groupPublicKey
}
is Destination.OpenGroup -> throw preconditionFailure
is Destination.OpenGroup -> throw Error.PreconditionFailure("Destination should not be open groups!")
}
val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext)
// Calculate proof of work
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
//TODO Notify user for proof of work calculating
SnodeConfiguration.shared.broadcaster.broadcast("calculatingPoW", message.sentTimestamp!!)
}
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)
val snodeMessage = SnodeMessage(recipient, base64EncodedData, message.ttl, timestamp, nonce)
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
SnodeConfiguration.shared.broadcaster.broadcast("sendingMessage", message.sentTimestamp!!)
}
SnodeAPI.sendMessage(snodeMessage).success { promises: Set<RawResponsePromise> ->
var isSuccess = false
val promiseCount = promises.size
@@ -187,7 +169,7 @@ object MessageSender {
if (isSuccess) { return@success } // Succeed as soon as the first promise succeeds
isSuccess = true
if (destination is Destination.Contact && message is VisibleMessage && !isSelfSend) {
//TODO Notify user for message sent
SnodeConfiguration.shared.broadcaster.broadcast("messageSent", message.sentTimestamp!!)
}
handleSuccessfulMessageSend(message, destination, isSyncMessage)
var shouldNotify = (message is VisibleMessage && !isSyncMessage)
@@ -199,57 +181,52 @@ object MessageSender {
JobQueue.shared.add(notifyPNServerJob)
deferred.resolve(Unit)
}
}
promise.fail {
errorCount += 1
if (errorCount != promiseCount) { return@fail } // Only error out if all promises failed
handleFailure(it)
deferred.reject(it)
}
}
}.fail {
Log.d("Loki", "Couldn't send message due to error: $it.")
deferred.reject(it)
handleFailure(it)
}
} catch (exception: Exception) {
deferred.reject(exception)
handleFailure(exception)
}
return promise
}
// Open Groups
fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>()
val storage = MessagingConfiguration.shared.storage
val preconditionFailure = Exception("Destination should not be contacts or closed groups!")
message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() }
message.sender = storage.getUserPublicKey()
// Set the failure handler (need it here already for precondition failure handling)
fun handleFailure(error: Exception) {
handleFailedMessageSend(message, error)
deferred.reject(error)
}
try {
val server: String
val channel: Long
when (destination) {
is Destination.Contact -> throw preconditionFailure
is Destination.ClosedGroup -> throw preconditionFailure
is Destination.Contact -> throw Error.PreconditionFailure("Destination should not be contacts!")
is Destination.ClosedGroup -> throw Error.PreconditionFailure("Destination should not be closed groups!")
is Destination.OpenGroup -> {
message.recipient = "${destination.server}.${destination.channel}"
server = destination.server
channel = destination.channel
}
}
// Set the failure handler (need it here already for precondition failure handling)
fun handleFailure(error: Exception) {
handleFailedMessageSend(message, error)
deferred.reject(error)
}
// Validate the message
if (message !is VisibleMessage || !message.isValid()) {
handleFailure(Error.InvalidMessage)
throw Error.InvalidMessage
}
// Convert the message to an open group message
val openGroupMessage = OpenGroupMessage.from(message, server) ?: kotlin.run {
handleFailure(Error.InvalidMessage)
throw Error.InvalidMessage
}
// Send the result
@@ -261,7 +238,7 @@ object MessageSender {
handleFailure(it)
}
} catch (exception: Exception) {
deferred.reject(exception)
handleFailure(exception)
}
return deferred.promise
}
@@ -269,7 +246,8 @@ object MessageSender {
// Result Handling
fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) {
val storage = MessagingConfiguration.shared.storage
val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender!!) ?: return
val userPublicKey = storage.getUserPublicKey()!!
val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey) ?: return
// Ignore future self-sends
storage.addReceivedMessageTimestamp(message.sentTimestamp!!)
// Track the open group server message ID
@@ -277,23 +255,89 @@ object MessageSender {
storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!)
}
// Mark the message as sent
storage.markAsSent(messageId)
storage.markUnidentified(messageId)
storage.markAsSent(message.sentTimestamp!!, message.sender?:userPublicKey)
storage.markUnidentified(message.sentTimestamp!!, message.sender?:userPublicKey)
// Start the disappearing messages timer if needed
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(messageId)
if (message is VisibleMessage && !isSyncMessage) {
SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, message.sender?:userPublicKey)
}
// Sync the message if:
// • it's a visible message
// • the destination was a contact
// • we didn't sync it already
val userPublicKey = storage.getUserPublicKey()!!
if (destination is Destination.Contact && !isSyncMessage && message is VisibleMessage) {
sendToSnodeDestination(Destination.Contact(userPublicKey), message, true).get()
if (destination is Destination.Contact && !isSyncMessage) {
if (message is VisibleMessage) { message.syncTarget = destination.publicKey }
if (message is ExpirationTimerUpdate) { message.syncTarget = destination.publicKey }
sendToSnodeDestination(Destination.Contact(userPublicKey), message, true)
}
}
fun handleFailedMessageSend(message: Message, error: Exception) {
val storage = MessagingConfiguration.shared.storage
val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender!!) ?: return
storage.setErrorMessage(messageId, error)
val userPublicKey = storage.getUserPublicKey()!!
storage.setErrorMessage(message.sentTimestamp!!, message.sender?:userPublicKey, error)
}
// Convenience
@JvmStatic
fun send(message: VisibleMessage, address: Address, attachments: List<SignalAttachment>, quote: SignalQuote?, linkPreview: SignalLinkPreview?) {
val dataProvider = MessagingConfiguration.shared.messageDataProvider
val attachmentIDs = dataProvider.getAttachmentIDsFor(message.id!!)
message.attachmentIDs.addAll(attachmentIDs)
message.quote = Quote.from(quote)
message.linkPreview = LinkPreview.from(linkPreview)
message.linkPreview?.let {
if (it.attachmentID == null) {
dataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let {
message.linkPreview!!.attachmentID = it
message.attachmentIDs.remove(it)
}
}
}
send(message, address)
}
@JvmStatic
fun send(message: Message, address: Address) {
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
message.threadID = threadID
val destination = Destination.from(address)
val job = MessageSendJob(message, destination)
JobQueue.shared.add(job)
}
fun sendNonDurably(message: VisibleMessage, attachments: List<SignalAttachment>, address: Address): Promise<Unit, Exception> {
val attachmentIDs = MessagingConfiguration.shared.messageDataProvider.getAttachmentIDsFor(message.id!!)
message.attachmentIDs.addAll(attachmentIDs)
return sendNonDurably(message, address)
}
fun sendNonDurably(message: Message, address: Address): Promise<Unit, Exception> {
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
message.threadID = threadID
val destination = Destination.from(address)
return send(message, destination)
}
// Closed groups
fun createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
return create(name, members)
}
fun explicitNameChange(groupPublicKey: String, newName: String) {
return setName(groupPublicKey, newName)
}
fun explicitAddMembers(groupPublicKey: String, membersToAdd: List<String>) {
return addMembers(groupPublicKey, membersToAdd)
}
fun explicitRemoveMembers(groupPublicKey: String, membersToRemove: List<String>) {
return removeMembers(groupPublicKey, membersToRemove)
}
@JvmStatic
fun explicitLeave(groupPublicKey: String): Promise<Unit, Exception> {
return leave(groupPublicKey)
}
}

View File

@@ -12,6 +12,7 @@ import org.session.libsession.messaging.sending_receiving.notifications.PushNoti
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsession.messaging.threads.Address
import org.session.libsession.utilities.GroupUtil
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Hex
import org.session.libsignal.libsignal.ecc.Curve
@@ -25,9 +26,10 @@ import org.session.libsignal.utilities.logging.Log
import java.util.*
import java.util.concurrent.ConcurrentHashMap
private val pendingKeyPair = ConcurrentHashMap<String, Optional<ECKeyPair>>()
const val groupSizeLimit = 100
val pendingKeyPair = ConcurrentHashMap<String, Optional<ECKeyPair>>()
fun MessageSender.createClosedGroup(name: String, members: Collection<String>): Promise<String, Exception> {
fun MessageSender.create(name: String, members: Collection<String>): Promise<String, Exception> {
val deferred = deferred<String, Exception>()
ThreadUtils.queue {
// Prepare
@@ -48,9 +50,11 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
storage.setProfileSharing(Address.fromSerialized(groupID), true)
// Send a closed group update message to all members individually
val closedGroupUpdateKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
val sentTime = System.currentTimeMillis()
for (member in members) {
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupUpdateKind)
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).get()
closedGroupControlMessage.sentTimestamp = sentTime
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(member)).get()
}
// Add the group to the user's set of public keys to poll for
storage.addClosedGroupPublicKey(groupPublicKey)
@@ -58,7 +62,7 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
storage.addClosedGroupEncryptionKeyPair(encryptionKeyPair, groupPublicKey)
// Notify the user
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID)
storage.insertOutgoingInfoMessage(context, groupID, SignalServiceProtos.GroupContext.Type.UPDATE, name, members, admins, threadID, sentTime)
// Notify the PN server
PushNotificationAPI.performOperation(PushNotificationAPI.ClosedGroupOperation.Subscribe, groupPublicKey, userPublicKey)
// Fulfill the promise
@@ -68,7 +72,7 @@ fun MessageSender.createClosedGroup(name: String, members: Collection<String>):
return deferred.promise
}
fun MessageSender.v2_update(groupPublicKey: String, members: List<String>, name: String) {
fun MessageSender.update(groupPublicKey: String, members: List<String>, name: String) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
@@ -98,14 +102,16 @@ fun MessageSender.setName(groupPublicKey: String, newName: String) {
val admins = group.admins.map { it.serialize() }
// Send the update to the group
val kind = ClosedGroupControlMessage.Kind.NameChange(newName)
val sentTime = System.currentTimeMillis()
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID))
// Update the group
storage.updateTitle(groupID, newName)
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID)
storage.insertOutgoingInfoMessage(context, groupID, infoType, newName, members, admins, threadID, sentTime)
}
fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>) {
@@ -134,18 +140,21 @@ fun MessageSender.addMembers(groupPublicKey: String, membersToAdd: List<String>)
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersAdded(newMembersAsData)
val sentTime = System.currentTimeMillis()
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID))
// Send closed group update messages to any new members individually
for (member in membersToAdd) {
val closedGroupNewKind = ClosedGroupControlMessage.Kind.New(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), name, encryptionKeyPair, membersAsData, adminsAsData)
val closedGroupControlMessage = ClosedGroupControlMessage(closedGroupNewKind)
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(member))
}
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
}
fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<String>) {
@@ -173,7 +182,9 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
val name = group.title
// Send the update to the group
val memberUpdateKind = ClosedGroupControlMessage.Kind.MembersRemoved(removeMembersAsData)
val sentTime = System.currentTimeMillis()
val closedGroupControlMessage = ClosedGroupControlMessage(memberUpdateKind)
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID))
val isCurrentUserAdmin = admins.contains(userPublicKey)
if (isCurrentUserAdmin) {
@@ -182,31 +193,37 @@ fun MessageSender.removeMembers(groupPublicKey: String, membersToRemove: List<St
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.UPDATE
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
}
fun MessageSender.v2_leave(groupPublicKey: String) {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val userPublicKey = storage.getUserPublicKey()!!
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Can't leave nonexistent closed group.")
throw Error.NoThread
fun MessageSender.leave(groupPublicKey: String, notifyUser: Boolean = true): Promise<Unit, Exception> {
val deferred = deferred<Unit, Exception>()
ThreadUtils.queue {
val context = MessagingConfiguration.shared.context
val storage = MessagingConfiguration.shared.storage
val userPublicKey = TextSecurePreferences.getLocalNumber(context)!!
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: return@queue deferred.reject(Error.NoThread)
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
val admins = group.admins.map { it.serialize() }
val name = group.title
// Send the update to the group
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft())
val sentTime = System.currentTimeMillis()
closedGroupControlMessage.sentTimestamp = sentTime
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.QUIT
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
if (notifyUser) {
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID, sentTime)
}
// Remove the group private key and unsubscribe from PNs
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
deferred.resolve(Unit)
}
}
val updatedMembers = group.members.map { it.serialize() }.toSet() - userPublicKey
val admins = group.admins.map { it.serialize() }
val name = group.title
// Send the update to the group
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.MemberLeft)
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
// Remove the group private key and unsubscribe from PNs
MessageReceiver.disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey)
}
// Notify the user
val infoType = SignalServiceProtos.GroupContext.Type.QUIT
val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID))
storage.insertOutgoingInfoMessage(context, groupID, infoType, name, updatedMembers, admins, threadID)
return deferred.promise
}
fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, targetMembers: Collection<String>) {
@@ -230,6 +247,15 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
// make sure we set the pendingKeyPair or wait until it is not null
} while (!pendingKeyPair.replace(groupPublicKey,Optional.absent(),Optional.fromNullable(newKeyPair)))
// Distribute it
sendEncryptionKeyPair(groupPublicKey, newKeyPair, targetMembers)?.success {
// Store it * after * having sent out the message to the group
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
pendingKeyPair[groupPublicKey] = Optional.absent()
}
}
fun MessageSender.sendEncryptionKeyPair(groupPublicKey: String, newKeyPair: ECKeyPair, targetMembers: Collection<String>, targetUser: String? = null, force: Boolean = true): Promise<Unit, Exception>? {
val destination = targetUser ?: GroupUtil.doubleEncodeGroupID(groupPublicKey)
val proto = SignalServiceProtos.KeyPair.newBuilder()
proto.publicKey = ByteString.copyFrom(newKeyPair.publicKey.serialize().removing05PrefixIfNeeded())
proto.privateKey = ByteString.copyFrom(newKeyPair.privateKey.serialize())
@@ -238,12 +264,15 @@ fun MessageSender.generateAndSendNewEncryptionKeyPair(groupPublicKey: String, ta
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
}
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(null, wrappers)
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers)
val sentTime = System.currentTimeMillis()
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
sendNonDurably(closedGroupControlMessage, Address.fromSerialized(groupID)).success {
// Store it * after * having sent out the message to the group
storage.addClosedGroupEncryptionKeyPair(newKeyPair, groupPublicKey)
pendingKeyPair[groupPublicKey] = Optional.absent()
closedGroupControlMessage.sentTimestamp = sentTime
return if (force) {
MessageSender.sendNonDurably(closedGroupControlMessage, Address.fromSerialized(destination))
} else {
MessageSender.send(closedGroupControlMessage, Address.fromSerialized(destination))
null
}
}
@@ -258,34 +287,8 @@ fun MessageSender.requestEncryptionKeyPair(groupPublicKey: String) {
val members = group.members.map { it.serialize() }.toSet()
if (!members.contains(storage.getUserPublicKey()!!)) return
// Send the request to the group
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest)
val sentTime = System.currentTimeMillis()
val closedGroupControlMessage = ClosedGroupControlMessage(ClosedGroupControlMessage.Kind.EncryptionKeyPairRequest())
closedGroupControlMessage.sentTimestamp = sentTime
send(closedGroupControlMessage, Address.fromSerialized(groupID))
}
fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey: String) {
val storage = MessagingConfiguration.shared.storage
val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey)
val group = storage.getGroup(groupID) ?: run {
Log.d("Loki", "Can't send encryption key pair for nonexistent closed group.")
throw Error.NoThread
}
val members = group.members.map { it.serialize() }
if (!members.contains(publicKey)) {
Log.d("Loki", "Refusing to send latest encryption key pair to non-member.")
return
}
// Get the latest encryption key pair
val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull()
?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return
// Send it
val proto = SignalServiceProtos.KeyPair.newBuilder()
proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize())
proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize())
val plaintext = proto.build().toByteArray()
val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey)
Log.d("Loki", "Sending latest encryption key pair to: $publicKey.")
val wrapper = ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext))
val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), listOf(wrapper))
val closedGroupControlMessage = ClosedGroupControlMessage(kind)
MessageSender.send(closedGroupControlMessage, Address.fromSerialized(publicKey))
}

View File

@@ -1,38 +0,0 @@
package org.session.libsession.messaging.sending_receiving
import nl.komponents.kovenant.Promise
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageSendJob
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.threads.Address
import org.session.libsignal.service.api.messages.SignalServiceAttachment
fun MessageSender.send(message: VisibleMessage, attachments: List<SignalServiceAttachment>, address: Address) {
prep(attachments, message)
send(message, address)
}
fun MessageSender.send(message: Message, address: Address) {
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
message.threadID = threadID
val destination = Destination.from(address)
val job = MessageSendJob(message, destination)
JobQueue.shared.add(job)
}
fun MessageSender.sendNonDurably(message: VisibleMessage, attachments: List<SignalServiceAttachment>, address: Address): Promise<Unit, Exception> {
prep(attachments, message)
return sendNonDurably(message, address)
}
fun MessageSender.sendNonDurably(message: Message, address: Address): Promise<Unit, Exception> {
val threadID = MessagingConfiguration.shared.storage.getOrCreateThreadIdFor(address)
message.threadID = threadID
val destination = Destination.from(address)
return MessageSender.send(message, destination)
}

View File

@@ -1,11 +1,53 @@
package org.session.libsession.messaging.sending_receiving
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
import com.goterl.lazycode.lazysodium.SodiumAndroid
import com.goterl.lazycode.lazysodium.interfaces.Box
import com.goterl.lazycode.lazysodium.interfaces.Sign
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.sending_receiving.MessageSender.Error
import org.session.libsession.utilities.KeyPairUtilities
import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded
import org.session.libsignal.utilities.Hex
import org.session.libsignal.utilities.logging.Log
object MessageSenderEncryption {
internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientPublicKey: String): ByteArray{
return MessagingConfiguration.shared.sessionProtocol.encrypt(plaintext, recipientPublicKey)
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
/**
* Encrypts `plaintext` using the Session protocol for `hexEncodedX25519PublicKey`.
*
* @param plaintext the plaintext to encrypt. Must already be padded.
* @param recipientHexEncodedX25519PublicKey the X25519 public key to encrypt for. Could be the Session ID of a user, or the public key of a closed group.
*
* @return the encrypted message.
*/
internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{
val context = MessagingConfiguration.shared.context
val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair
val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
val verificationData = plaintext + userED25519KeyPair.publicKey.asBytes + recipientX25519PublicKey
val signature = ByteArray(Sign.BYTES)
try {
sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes)
} catch (exception: Exception) {
Log.d("Loki", "Couldn't sign message due to error: $exception.")
throw Error.SigningFailed
}
val plaintextWithMetadata = plaintext + userED25519KeyPair.publicKey.asBytes + signature
val ciphertext = ByteArray(plaintextWithMetadata.size + Box.SEALBYTES)
try {
sodium.cryptoBoxSeal(ciphertext, plaintextWithMetadata, plaintextWithMetadata.size.toLong(), recipientX25519PublicKey)
} catch (exception: Exception) {
Log.d("Loki", "Couldn't encrypt message due to error: $exception.")
throw Error.EncryptionFailed
}
return ciphertext
}
}

View File

@@ -12,10 +12,10 @@ import org.session.libsession.utilities.Util;
public class AttachmentId implements Parcelable {
@JsonProperty
private final long rowId;
private final long rowId; // This is the field id in the database
@JsonProperty
private final long uniqueId;
private final long uniqueId; // This is the timestamp when the attachment is written into the database
public AttachmentId(@JsonProperty("rowId") long rowId, @JsonProperty("uniqueId") long uniqueId) {
this.rowId = rowId;

View File

@@ -7,10 +7,10 @@ import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveJob
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.snode.Snode
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeConfiguration
import org.session.libsignal.service.loki.api.Snode
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Base64

View File

@@ -0,0 +1,9 @@
package org.session.libsession.messaging.threads
object DistributionTypes {
const val DEFAULT = 2
const val BROADCAST = 1
const val CONVERSATION = 2
const val ARCHIVE = 3
const val INBOX_ZERO = 4
}

View File

@@ -17,19 +17,7 @@
package org.session.libsession.messaging.threads.recipients;
public class RecipientFormattingException extends Exception {
public RecipientFormattingException() {
super();
}
public RecipientFormattingException(String message) {
super(message);
}
public RecipientFormattingException(String message, Throwable nested) {
super(message, nested);
}
public RecipientFormattingException(Throwable nested) {
super(nested);
}
}

View File

@@ -1,76 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.session.libsession.messaging.threads.recipients;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
public class RecipientsFormatter {
private static String parseBracketedNumber(String recipient) throws RecipientFormattingException {
int begin = recipient.indexOf('<');
int end = recipient.indexOf('>');
String value = recipient.substring(begin + 1, end);
if (PhoneNumberUtils.isWellFormedSmsAddress(value))
return value;
else
throw new RecipientFormattingException("Bracketed value: " + value + " is not valid.");
}
private static String parseRecipient(String recipient) throws RecipientFormattingException {
recipient = recipient.trim();
if ((recipient.indexOf('<') != -1) && (recipient.indexOf('>') != -1))
return parseBracketedNumber(recipient);
if (PhoneNumberUtils.isWellFormedSmsAddress(recipient))
return recipient;
throw new RecipientFormattingException("Recipient: " + recipient + " is badly formatted.");
}
public static List<String> getRecipients(String rawText) throws RecipientFormattingException {
ArrayList<String> results = new ArrayList<String>();
StringTokenizer tokenizer = new StringTokenizer(rawText, ",");
while (tokenizer.hasMoreTokens()) {
results.add(parseRecipient(tokenizer.nextToken()));
}
return results;
}
public static String formatNameAndNumber(String name, String number) {
// Format like this: Mike Cleron <(650) 555-1234>
// Erick Tseng <(650) 555-1212>
// Tutankhamun <tutank1341@gmail.com>
// (408) 555-1289
String formattedNumber = PhoneNumberUtils.formatNumber(number);
if (!TextUtils.isEmpty(name) && !name.equals(number)) {
return name + " <" + formattedNumber + ">";
} else {
return formattedNumber;
}
}
}

View File

@@ -42,7 +42,7 @@ open class DotNetAPI {
internal enum class HTTPVerb { GET, PUT, POST, DELETE, PATCH }
// Error
internal sealed class Error(val description: String) : Exception() {
internal sealed class Error(val description: String) : Exception(description) {
object Generic : Error("An error occurred.")
object InvalidURL : Error("Invalid URL.")
object ParsingFailed : Error("Invalid file server response.")

View File

@@ -10,7 +10,7 @@ import java.security.SecureRandom
object MessageWrapper {
// region Types
sealed class Error(val description: String) : Exception() {
sealed class Error(val description: String) : Exception(description) {
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.")

View File

@@ -1,63 +0,0 @@
package org.session.libsession.messaging.utilities
import android.content.Context
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
import com.goterl.lazycode.lazysodium.SodiumAndroid
import org.session.libsession.messaging.MessagingConfiguration
import org.session.libsession.utilities.TextSecurePreferences.isUniversalUnidentifiedAccess
import org.session.libsession.utilities.Util.getSecretBytes
import org.session.libsignal.metadata.SignalProtos
import org.session.libsignal.service.api.crypto.UnidentifiedAccess
import org.session.libsignal.utilities.logging.Log
object UnidentifiedAccessUtil {
private val TAG = UnidentifiedAccessUtil::class.simpleName
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
fun getAccessFor(recipientPublicKey: String): UnidentifiedAccess? {
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))
return if (theirUnidentifiedAccessKey != null && ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
UnidentifiedAccess(theirUnidentifiedAccessKey)
} else null
}
fun getAccessForSync(context: Context): UnidentifiedAccess? {
var ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey()
val ourUnidentifiedAccessCertificate = getUnidentifiedAccessCertificate()
if (isUniversalUnidentifiedAccess(context)) {
ourUnidentifiedAccessKey = getSecretBytes(16)
}
return if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
UnidentifiedAccess(ourUnidentifiedAccessKey)
} else null
}
private fun getTargetUnidentifiedAccessKey(recipientPublicKey: String): ByteArray? {
val theirProfileKey = MessagingConfiguration.shared.storage.getProfileKeyForRecipient(recipientPublicKey) ?: return sodium.randomBytesBuf(16)
return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
}
private fun getSelfUnidentifiedAccessKey(): ByteArray? {
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
if (userPublicKey != null) {
return sodium.randomBytesBuf(16)
}
return null
}
private fun getUnidentifiedAccessCertificate(): ByteArray? {
val userPublicKey = MessagingConfiguration.shared.storage.getUserPublicKey()
if (userPublicKey != null) {
val certificate = SignalProtos.SenderCertificate.newBuilder().setSender(userPublicKey).setSenderDevice(1).build()
return certificate.toByteArray()
}
return null
}
}

View File

@@ -10,7 +10,7 @@ import org.session.libsession.utilities.AESGCM
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.*
import org.session.libsignal.service.loki.api.*
import org.session.libsignal.service.loki.api.Snode
import org.session.libsignal.service.loki.api.fileserver.FileServerAPI
import org.session.libsignal.service.loki.api.utilities.*
import org.session.libsession.utilities.AESGCM.EncryptionResult
@@ -74,7 +74,7 @@ object OnionRequestAPI {
)
internal sealed class Destination {
class Snode(val snode: org.session.libsession.snode.Snode) : Destination()
class Snode(val snode: org.session.libsignal.service.loki.api.Snode) : Destination()
class Server(val host: String, val target: String, val x25519PublicKey: String) : Destination()
}

View File

@@ -1,6 +1,6 @@
package org.session.libsession.snode
public class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) {
val ip: String get() = address.removePrefix("https://")

View File

@@ -10,6 +10,9 @@ import org.session.libsession.snode.utilities.getRandomElement
import org.session.libsignal.utilities.logging.Log
import org.session.libsignal.service.loki.api.utilities.HTTP
import org.session.libsignal.service.loki.api.Snode
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.Broadcaster
import org.session.libsignal.service.loki.utilities.prettifiedDescription
import org.session.libsignal.service.loki.utilities.retryIfNeeded
import org.session.libsignal.utilities.*
@@ -17,10 +20,11 @@ import org.session.libsignal.utilities.*
import java.security.SecureRandom
object SnodeAPI {
val database = SnodeConfiguration.shared.storage
val broadcaster = SnodeConfiguration.shared.broadcaster
val database: LokiAPIDatabaseProtocol
get() = SnodeConfiguration.shared.storage
val broadcaster: Broadcaster
get() = SnodeConfiguration.shared.broadcaster
val sharedContext = Kovenant.createContext()
val messageSendingContext = Kovenant.createContext()
val messagePollingContext = Kovenant.createContext()
internal var snodeFailureCount: MutableMap<Snode, Int> = mutableMapOf()
@@ -41,7 +45,7 @@ object SnodeAPI {
internal var powDifficulty = 1
// Error
internal sealed class Error(val description: String) : Exception() {
internal sealed class Error(val description: String) : Exception(description) {
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.")
@@ -158,7 +162,7 @@ object SnodeAPI {
val parameters = mapOf( "pubKey" to publicKey )
return getRandomSnode().bind {
invoke(Snode.Method.GetSwarm, it, publicKey, parameters)
}.map(SnodeAPI.sharedContext) {
}.map(sharedContext) {
parseSnodes(it).toSet()
}.success {
database.setSwarm(publicKey, it)
@@ -182,19 +186,12 @@ object SnodeAPI {
fun sendMessage(message: SnodeMessage): Promise<Set<RawResponsePromise>, 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 ->
getTargetSnodes(destination).map { swarm ->
swarm.map { snode ->
broadcast("sendingMessage")
val parameters = message.toJSON()
retryIfNeeded(maxRetryCount) {
invoke(Snode.Method.SendMessage, snode, destination, parameters).map(messageSendingContext) { rawResponse ->
invoke(Snode.Method.SendMessage, snode, destination, parameters).map { rawResponse ->
val json = rawResponse as? Map<*, *>
val powDifficulty = json?.get("difficulty") as? Int
if (powDifficulty != null) {

View File

@@ -1,12 +1,13 @@
package org.session.libsession.snode
import org.session.libsignal.service.loki.database.LokiAPIDatabaseProtocol
import org.session.libsignal.service.loki.utilities.Broadcaster
class SnodeConfiguration(val storage: SnodeStorageProtocol, val broadcaster: Broadcaster) {
class SnodeConfiguration(val storage: LokiAPIDatabaseProtocol, val broadcaster: Broadcaster) {
companion object {
lateinit var shared: SnodeConfiguration
fun configure(storage: SnodeStorageProtocol, broadcaster: Broadcaster) {
fun configure(storage: LokiAPIDatabaseProtocol, broadcaster: Broadcaster) {
if (Companion::shared.isInitialized) { return }
shared = SnodeConfiguration(storage, broadcaster)
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.session.libsession.utilities;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import androidx.annotation.NonNull;
import org.session.libsignal.libsignal.ecc.ECPublicKey;
import org.session.libsignal.libsignal.IdentityKey;
import org.session.libsignal.libsignal.IdentityKeyPair;
import org.session.libsignal.libsignal.InvalidKeyException;
import org.session.libsignal.libsignal.ecc.Curve;
import org.session.libsignal.libsignal.ecc.ECKeyPair;
import org.session.libsignal.libsignal.ecc.ECPrivateKey;
import org.session.libsignal.utilities.Base64;
import java.io.IOException;
/**
* Utility class for working with identity keys.
*
* @author Moxie Marlinspike
*/
public class IdentityKeyUtil {
private static final String MASTER_SECRET_UTIL_PREFERENCES_NAME = "SecureSMS-Preferences";
@SuppressWarnings("unused")
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
public static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
public static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
public static final String ED25519_PUBLIC_KEY = "pref_ed25519_public_key";
public static final String ED25519_SECRET_KEY = "pref_ed25519_secret_key";
public static final String LOKI_SEED = "loki_seed";
public static boolean hasIdentityKey(Context context) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
return
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
preferences.contains(IDENTITY_PRIVATE_KEY_PREF);
}
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_PREF));
return new IdentityKey(publicKeyBytes, 0);
} catch (IOException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
public static @NonNull IdentityKeyPair getIdentityKeyPair(@NonNull Context context) {
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
try {
IdentityKey publicKey = getIdentityKey(context);
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF)));
return new IdentityKeyPair(publicKey, privateKey);
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static void generateIdentityKeyPair(@NonNull Context context) {
ECKeyPair keyPair = Curve.generateKeyPair();
ECPublicKey publicKey = keyPair.getPublicKey();
ECPrivateKey privateKey = keyPair.getPrivateKey();
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(publicKey.serialize()));
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(privateKey.serialize()));
}
public static String retrieve(Context context, String key) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
return preferences.getString(key, null);
}
public static void save(Context context, String key, String value) {
SharedPreferences preferences = context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0);
Editor preferencesEditor = preferences.edit();
preferencesEditor.putString(key, value);
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
}
public static void delete(Context context, String key) {
context.getSharedPreferences(MASTER_SECRET_UTIL_PREFERENCES_NAME, 0).edit().remove(key).commit();
}
}

View File

@@ -0,0 +1,60 @@
package org.session.libsession.utilities
import android.content.Context
import com.goterl.lazycode.lazysodium.LazySodiumAndroid
import com.goterl.lazycode.lazysodium.SodiumAndroid
import com.goterl.lazycode.lazysodium.utils.Key
import com.goterl.lazycode.lazysodium.utils.KeyPair
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Hex
import org.session.libsignal.libsignal.ecc.DjbECPrivateKey
import org.session.libsignal.libsignal.ecc.DjbECPublicKey
import org.session.libsignal.libsignal.ecc.ECKeyPair
object KeyPairUtilities {
private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) }
fun generate(): KeyPairGenerationResult {
val seed = sodium.randomBytesBuf(16)
try {
return generate(seed)
} catch (exception: Exception) {
return generate()
}
}
fun generate(seed: ByteArray): KeyPairGenerationResult {
val padding = ByteArray(16) { 0 }
val ed25519KeyPair = sodium.cryptoSignSeedKeypair(seed + padding)
val sodiumX25519KeyPair = sodium.convertKeyPairEd25519ToCurve25519(ed25519KeyPair)
val x25519KeyPair = ECKeyPair(DjbECPublicKey(sodiumX25519KeyPair.publicKey.asBytes), DjbECPrivateKey(sodiumX25519KeyPair.secretKey.asBytes))
return KeyPairGenerationResult(seed, ed25519KeyPair, x25519KeyPair)
}
fun store(context: Context, seed: ByteArray, ed25519KeyPair: KeyPair, x25519KeyPair: ECKeyPair) {
IdentityKeyUtil.save(context, IdentityKeyUtil.LOKI_SEED, Hex.toStringCondensed(seed))
IdentityKeyUtil.save(context, IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(x25519KeyPair.publicKey.serialize()))
IdentityKeyUtil.save(context, IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(x25519KeyPair.privateKey.serialize()))
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_PUBLIC_KEY, Base64.encodeBytes(ed25519KeyPair.publicKey.asBytes))
IdentityKeyUtil.save(context, IdentityKeyUtil.ED25519_SECRET_KEY, Base64.encodeBytes(ed25519KeyPair.secretKey.asBytes))
}
fun hasV2KeyPair(context: Context): Boolean {
return (IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) != null)
}
fun getUserED25519KeyPair(context: Context): KeyPair? {
val base64EncodedED25519PublicKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_PUBLIC_KEY) ?: return null
val base64EncodedED25519SecretKey = IdentityKeyUtil.retrieve(context, IdentityKeyUtil.ED25519_SECRET_KEY) ?: return null
val ed25519PublicKey = Key.fromBytes(Base64.decode(base64EncodedED25519PublicKey))
val ed25519SecretKey = Key.fromBytes(Base64.decode(base64EncodedED25519SecretKey))
return KeyPair(ed25519PublicKey, ed25519SecretKey)
}
data class KeyPairGenerationResult(
val seed: ByteArray,
val ed25519KeyPair: KeyPair,
val x25519KeyPair: ECKeyPair
)
}

View File

@@ -38,7 +38,7 @@ class SSKEnvironment(
interface MessageExpirationManagerProtocol {
fun setExpirationTimer(messageID: Long?, duration: Int, senderPublicKey: String, content: SignalServiceProtos.Content)
fun disableExpirationTimer(messageID: Long?, senderPublicKey: String, content: SignalServiceProtos.Content)
fun startAnyExpiration(messageID: Long)
fun startAnyExpiration(timestamp: Long, author: String)
}
companion object {