From 0faeb7becfc9da9ea5ad2f161234e39bd292fdf4 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 13 May 2021 11:11:42 +1000 Subject: [PATCH 01/27] Update AttachmentUploadJob for the V2 file server --- .../messaging/jobs/AttachmentUploadJob.kt | 56 ++++++++++++------- .../session/libsession/utilities/AESGCM.kt | 13 ++--- .../libsession/utilities/mentions/Mention.kt | 0 3 files changed, 43 insertions(+), 26 deletions(-) delete mode 100644 libsession/src/main/java/org/session/libsession/utilities/mentions/Mention.kt diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index a4ef41431d..72c42b03a5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -3,8 +3,10 @@ package org.session.libsession.messaging.jobs import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output +import nl.komponents.kovenant.Promise import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.file_server.FileServerAPI +import org.session.libsession.messaging.file_server.FileServerAPIV2 import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.sending_receiving.MessageSender @@ -15,6 +17,7 @@ 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.push.http.DigestingRequestBody import org.session.libsignal.service.internal.util.Util import org.session.libsignal.service.loki.PlaintextOutputStreamFactory import org.session.libsignal.utilities.logging.Log @@ -45,27 +48,29 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess override fun execute() { try { - val attachment = MessagingModuleConfiguration.shared.messageDataProvider.getScaledSignalAttachmentStream(attachmentID) + val storage = MessagingModuleConfiguration.shared.storage + val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider + val attachment = messageDataProvider.getScaledSignalAttachmentStream(attachmentID) ?: return handleFailure(Error.NoAttachment) - val usePadding = false - val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadID) - val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(threadID) - val server = openGroupV2?.server ?: openGroup?.server ?: FileServerAPI.shared.server - val shouldEncrypt = (openGroup == null && openGroupV2 == null) // Encrypt if this isn't an open group - 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 outputStreamFactory = if (shouldEncrypt) AttachmentCipherOutputStreamFactory(attachmentKey) else PlaintextOutputStreamFactory() - val attachmentData = PushAttachmentData(attachment.contentType, dataStream, ciphertextLength, outputStreamFactory, attachment.listener) - val uploadResult = if (openGroupV2 != null) { - val dataBytes = attachmentData.data.readBytes() - val result = OpenGroupAPIV2.upload(dataBytes, openGroupV2.room, openGroupV2.server).get() - DotNetAPI.UploadResult(result, "${openGroupV2.server}/files/$result", byteArrayOf()) - } else { - FileServerAPI.shared.uploadAttachment(server, attachmentData) + val v2OpenGroup = storage.getV2OpenGroup(threadID) + val v1OpenGroup = storage.getOpenGroup(threadID) + if (v2OpenGroup != null) { + val keyAndResult = upload(attachment, v2OpenGroup.server, false) { + OpenGroupAPIV2.upload(it, v2OpenGroup.room, v2OpenGroup.server) + } + handleSuccess(attachment, keyAndResult.first, keyAndResult.second) + } else if (v1OpenGroup == null) { + val keyAndResult = upload(attachment, FileServerAPIV2.DEFAULT_SERVER, true) { + FileServerAPIV2.upload(it) + } + handleSuccess(attachment, keyAndResult.first, keyAndResult.second) + } else { // V1 open group + val server = v1OpenGroup.server + val pushData = PushAttachmentData(attachment.contentType, attachment.inputStream, + attachment.length, PlaintextOutputStreamFactory(), attachment.listener) + val result = FileServerAPI.shared.uploadAttachment(server, pushData) + handleSuccess(attachment, ByteArray(0), result) } - handleSuccess(attachment, attachmentKey, uploadResult) } catch (e: java.lang.Exception) { if (e == Error.NoAttachment) { this.handlePermanentFailure(e) @@ -77,6 +82,19 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess } } + private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise): Pair { + val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0) + val rawLength = attachment.length + val length = if (encrypt) PaddingInputStream.getPaddedSize(rawLength) else rawLength + val stream = if (encrypt) PaddingInputStream(attachment.inputStream, rawLength) else attachment.inputStream + val ciphertextLength = if (encrypt) AttachmentCipherOutputStream.getCiphertextLength(length) else rawLength + val outputStreamFactory = if (encrypt) AttachmentCipherOutputStreamFactory(key) else PlaintextOutputStreamFactory() + val pushData = PushAttachmentData(attachment.contentType, stream, ciphertextLength, outputStreamFactory, attachment.listener) + val file = DigestingRequestBody(pushData.data, outputStreamFactory, attachment.contentType, pushData.dataSize, attachment.listener) + val id = upload(pushData.data.readBytes()).get() + return Pair(key, DotNetAPI.UploadResult(id, "${server}/files/$id", file.transmittedDigest)) + } + private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { Log.d(TAG, "Attachment uploaded successfully.") delegate?.handleJobSucceeded(this) diff --git a/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt b/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt index f5a170d8bd..102ef8dc1d 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/AESGCM.kt @@ -12,16 +12,15 @@ import javax.crypto.spec.SecretKeySpec @WorkerThread internal object AESGCM { - - internal data class EncryptionResult( - internal val ciphertext: ByteArray, - internal val symmetricKey: ByteArray, - internal val ephemeralPublicKey: ByteArray - ) - internal val gcmTagSize = 128 internal val ivSize = 12 + internal data class EncryptionResult( + internal val ciphertext: ByteArray, + internal val symmetricKey: ByteArray, + internal val ephemeralPublicKey: ByteArray + ) + /** * Sync. Don't call from the main thread. */ diff --git a/libsession/src/main/java/org/session/libsession/utilities/mentions/Mention.kt b/libsession/src/main/java/org/session/libsession/utilities/mentions/Mention.kt deleted file mode 100644 index e69de29bb2..0000000000 From 7c5b4aafec87a477dabfbcd7391d3c9cdd5f5e08 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 13 May 2021 13:19:08 +1000 Subject: [PATCH 02/27] Debug --- .../attachments/DatabaseAttachmentProvider.kt | 22 ++++----- .../database/MessageDataProvider.kt | 2 +- .../messaging/file_server/FileServerAPIV2.kt | 8 ++-- .../messaging/jobs/AttachmentUploadJob.kt | 45 +++++++++++++++---- .../libsession/utilities/DownloadUtilities.kt | 3 +- .../AttachmentCipherOutputStreamFactory.java | 2 - 6 files changed, 53 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index 40981b5cc7..8b13757f8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -108,20 +108,20 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) return null // TODO: Implement } - override fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { + override fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { val database = DatabaseFactory.getAttachmentDatabase(context) val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return val attachmentPointer = SignalServiceAttachmentPointer(uploadResult.id, - attachmentStream.contentType, - attachmentKey, - Optional.of(Util.toIntExact(attachmentStream.length)), - attachmentStream.preview, - attachmentStream.width, attachmentStream.height, - Optional.fromNullable(uploadResult.digest), - attachmentStream.fileName, - attachmentStream.voiceNote, - attachmentStream.caption, - uploadResult.url); + attachmentStream.contentType, + attachmentKey, + Optional.of(Util.toIntExact(attachmentStream.length)), + attachmentStream.preview, + attachmentStream.width, attachmentStream.height, + Optional.fromNullable(uploadResult.digest), + attachmentStream.fileName, + attachmentStream.voiceNote, + attachmentStream.caption, + uploadResult.url); val attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer), databaseAttachment.fastPreflightId).get() database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment) } diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt index b34a4ac3be..be4596d57b 100644 --- a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -29,7 +29,7 @@ interface MessageDataProvider { fun isOutgoingMessage(timestamp: Long): Boolean - fun updateAttachmentAfterUploadSucceeded(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) + fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) fun updateAttachmentAfterUploadFailed(attachmentId: Long) fun getMessageForQuote(timestamp: Long, author: Address): Pair? diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt index c8db066692..89bea6bc88 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerAPIV2.kt @@ -15,8 +15,8 @@ import org.session.libsignal.utilities.logging.Log object FileServerAPIV2 { - private const val DEFAULT_SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" - const val DEFAULT_SERVER = "http://88.99.175.227" + private const val SERVER_PUBLIC_KEY = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" + const val SERVER = "http://88.99.175.227" sealed class Error(message: String) : Exception(message) { object ParsingFailed : Error("Invalid response.") @@ -43,7 +43,7 @@ object FileServerAPIV2 { } private fun send(request: Request): Promise, Exception> { - val url = HttpUrl.parse(DEFAULT_SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL) + val url = HttpUrl.parse(SERVER) ?: return Promise.ofFail(OpenGroupAPIV2.Error.InvalidURL) val urlBuilder = HttpUrl.Builder() .scheme(url.scheme()) .host(url.host()) @@ -64,7 +64,7 @@ object FileServerAPIV2 { HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters)) } if (request.useOnionRouting) { - return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), DEFAULT_SERVER, DEFAULT_SERVER_PUBLIC_KEY).fail { e -> + return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), SERVER, SERVER_PUBLIC_KEY).fail { e -> Log.e("Loki", "File server request failed.", e) } } else { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index 72c42b03a5..b601731e69 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -4,6 +4,8 @@ import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import nl.komponents.kovenant.Promise +import okhttp3.MultipartBody +import okio.Buffer import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.file_server.FileServerAPI import org.session.libsession.messaging.file_server.FileServerAPIV2 @@ -21,6 +23,7 @@ import org.session.libsignal.service.internal.push.http.DigestingRequestBody import org.session.libsignal.service.internal.util.Util import org.session.libsignal.service.loki.PlaintextOutputStreamFactory import org.session.libsignal.utilities.logging.Log +import java.util.* class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val message: Message, val messageSendJobID: String) : Job { override var delegate: JobDelegate? = null @@ -60,7 +63,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess } handleSuccess(attachment, keyAndResult.first, keyAndResult.second) } else if (v1OpenGroup == null) { - val keyAndResult = upload(attachment, FileServerAPIV2.DEFAULT_SERVER, true) { + val keyAndResult = upload(attachment, FileServerAPIV2.SERVER, true) { FileServerAPIV2.upload(it) } handleSuccess(attachment, keyAndResult.first, keyAndResult.second) @@ -83,22 +86,46 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess } private fun upload(attachment: SignalServiceAttachmentStream, server: String, encrypt: Boolean, upload: (ByteArray) -> Promise): Pair { + // Key val key = if (encrypt) Util.getSecretBytes(64) else ByteArray(0) + // Length val rawLength = attachment.length - val length = if (encrypt) PaddingInputStream.getPaddedSize(rawLength) else rawLength - val stream = if (encrypt) PaddingInputStream(attachment.inputStream, rawLength) else attachment.inputStream - val ciphertextLength = if (encrypt) AttachmentCipherOutputStream.getCiphertextLength(length) else rawLength + val length = if (encrypt) { + val paddedLength = PaddingInputStream.getPaddedSize(rawLength) + AttachmentCipherOutputStream.getCiphertextLength(paddedLength) + } else { + attachment.length + } + // In & out streams + // PaddingInputStream adds padding as data is read out from it. AttachmentCipherOutputStream + // encrypts as it writes data. + val inputStream = if (encrypt) PaddingInputStream(attachment.inputStream, rawLength) else attachment.inputStream val outputStreamFactory = if (encrypt) AttachmentCipherOutputStreamFactory(key) else PlaintextOutputStreamFactory() - val pushData = PushAttachmentData(attachment.contentType, stream, ciphertextLength, outputStreamFactory, attachment.listener) - val file = DigestingRequestBody(pushData.data, outputStreamFactory, attachment.contentType, pushData.dataSize, attachment.listener) - val id = upload(pushData.data.readBytes()).get() - return Pair(key, DotNetAPI.UploadResult(id, "${server}/files/$id", file.transmittedDigest)) + // Create a multipart request body but immediately read it out to a buffer. Doing this makes + // it easier to deal with inputStream and outputStreamFactory. + val pad = PushAttachmentData(attachment.contentType, inputStream, length, outputStreamFactory, attachment.listener) + val contentType = "application/octet-stream" + val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, contentType, pad.dataSize, attachment.listener) + Log.d("Loki", "File size: ${length.toDouble() / 1000} kb.") + val mpb = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("Content-Type", contentType) + .addFormDataPart("content", UUID.randomUUID().toString(), drb) + .build() + val b = Buffer() + mpb.writeTo(b) + val data = b.readByteArray() + // Upload the data + val id = upload(data).get() + val digest = drb.transmittedDigest + // Return + return Pair(key, DotNetAPI.UploadResult(id, "${server}/files/$id", digest)) } private fun handleSuccess(attachment: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) { Log.d(TAG, "Attachment uploaded successfully.") delegate?.handleJobSucceeded(this) - MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadSucceeded(attachmentID, attachment, attachmentKey, uploadResult) + MessagingModuleConfiguration.shared.messageDataProvider.handleSuccessfulAttachmentUpload(attachmentID, attachment, attachmentKey, uploadResult) MessagingModuleConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID) } diff --git a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt index 9ea2d8e3a5..fc2375c1c8 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/DownloadUtilities.kt @@ -5,7 +5,6 @@ import okhttp3.Request import org.session.libsession.messaging.file_server.FileServerAPI import org.session.libsession.messaging.file_server.FileServerAPIV2 import org.session.libsession.snode.OnionRequestAPI -import org.session.libsignal.service.api.crypto.AttachmentCipherInputStream import org.session.libsignal.utilities.logging.Log import org.session.libsignal.service.api.messages.SignalServiceAttachment import org.session.libsignal.service.api.push.exceptions.NonSuccessfulResponseCodeException @@ -42,7 +41,7 @@ object DownloadUtilities { @JvmStatic fun downloadFile(outputStream: OutputStream, url: String, maxSize: Int, listener: SignalServiceAttachment.ProgressListener?) { - if (url.contains(FileServerAPIV2.DEFAULT_SERVER)) { + if (url.contains(FileServerAPIV2.SERVER)) { val httpUrl = HttpUrl.parse(url)!! val fileId = httpUrl.pathSegments().last() try { diff --git a/libsignal/src/main/java/org/session/libsignal/service/internal/push/http/AttachmentCipherOutputStreamFactory.java b/libsignal/src/main/java/org/session/libsignal/service/internal/push/http/AttachmentCipherOutputStreamFactory.java index c6137962c8..8a6599ab05 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/internal/push/http/AttachmentCipherOutputStreamFactory.java +++ b/libsignal/src/main/java/org/session/libsignal/service/internal/push/http/AttachmentCipherOutputStreamFactory.java @@ -1,6 +1,5 @@ package org.session.libsignal.service.internal.push.http; - import org.session.libsignal.service.api.crypto.AttachmentCipherOutputStream; import org.session.libsignal.service.api.crypto.DigestingOutputStream; @@ -19,5 +18,4 @@ public class AttachmentCipherOutputStreamFactory implements OutputStreamFactory public DigestingOutputStream createFor(OutputStream wrap) throws IOException { return new AttachmentCipherOutputStream(key, wrap); } - } From 3e1727fdbceca533080a6b9e13cc4202caa5420d Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 13 May 2021 13:34:35 +1000 Subject: [PATCH 03/27] Debug --- .../attachments/DatabaseAttachmentProvider.kt | 4 ++-- .../securesms/database/AttachmentDatabase.java | 2 +- .../libsession/database/MessageDataProvider.kt | 2 +- .../libsession/messaging/jobs/AttachmentUploadJob.kt | 11 +++-------- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt index 8b13757f8e..a083d94cda 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachmentProvider.kt @@ -126,10 +126,10 @@ class DatabaseAttachmentProvider(context: Context, helper: SQLCipherOpenHelper) database.updateAttachmentAfterUploadSucceeded(databaseAttachment.attachmentId, attachment) } - override fun updateAttachmentAfterUploadFailed(attachmentId: Long) { + override fun handleFailedAttachmentUpload(attachmentId: Long) { val database = DatabaseFactory.getAttachmentDatabase(context) val databaseAttachment = getDatabaseAttachment(attachmentId) ?: return - database.updateAttachmentAfterUploadFailed(databaseAttachment.attachmentId) + database.handleFailedAttachmentUpload(databaseAttachment.attachmentId) } override fun getMessageID(serverID: Long): Long? { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index b69f879993..59b7f52c7d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -393,7 +393,7 @@ public class AttachmentDatabase extends Database { database.update(TABLE_NAME, values, PART_ID_WHERE, id.toStrings()); } - public void updateAttachmentAfterUploadFailed(@NonNull AttachmentId id) { + public void handleFailedAttachmentUpload(@NonNull AttachmentId id) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt index be4596d57b..ba33ec5a58 100644 --- a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -30,7 +30,7 @@ interface MessageDataProvider { fun isOutgoingMessage(timestamp: Long): Boolean fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: DotNetAPI.UploadResult) - fun updateAttachmentAfterUploadFailed(attachmentId: Long) + fun handleFailedAttachmentUpload(attachmentId: Long) fun getMessageForQuote(timestamp: Long, author: Address): Pair? fun getAttachmentsAndLinkPreviewFor(mmsId: Long): List diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index b601731e69..a2485f3599 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -105,15 +105,10 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess // it easier to deal with inputStream and outputStreamFactory. val pad = PushAttachmentData(attachment.contentType, inputStream, length, outputStreamFactory, attachment.listener) val contentType = "application/octet-stream" - val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, contentType, pad.dataSize, attachment.listener) + val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, contentType, pad.dataSize, pad.listener) Log.d("Loki", "File size: ${length.toDouble() / 1000} kb.") - val mpb = MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("Content-Type", contentType) - .addFormDataPart("content", UUID.randomUUID().toString(), drb) - .build() val b = Buffer() - mpb.writeTo(b) + drb.writeTo(b) val data = b.readByteArray() // Upload the data val id = upload(data).get() @@ -132,7 +127,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess private fun handlePermanentFailure(e: Exception) { Log.w(TAG, "Attachment upload failed permanently due to error: $this.") delegate?.handleJobFailedPermanently(this, e) - MessagingModuleConfiguration.shared.messageDataProvider.updateAttachmentAfterUploadFailed(attachmentID) + MessagingModuleConfiguration.shared.messageDataProvider.handleFailedAttachmentUpload(attachmentID) failAssociatedMessageSendJob(e) } From d83c25749188baa3b7f95eda4bf881d6ac372426 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 13 May 2021 14:24:27 +1000 Subject: [PATCH 04/27] Use V2 file server for profile pictures Also don't randomly rotate profile key --- .../securesms/ApplicationContext.java | 33 ++++++++--- .../loki/activities/SettingsActivity.kt | 57 +++++++------------ .../messaging/jobs/AttachmentDownloadJob.kt | 19 ++++--- .../messaging/jobs/AttachmentUploadJob.kt | 2 +- .../utilities/ProfilePictureUtilities.kt | 46 +++++++++++++++ 5 files changed, 102 insertions(+), 55 deletions(-) create mode 100644 libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 64a9929630..16ee21cabb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -42,6 +42,7 @@ import org.session.libsession.messaging.sending_receiving.pollers.Poller; import org.session.libsession.messaging.threads.Address; import org.session.libsession.snode.SnodeModule; import org.session.libsession.utilities.IdentityKeyUtil; +import org.session.libsession.utilities.ProfilePictureUtilities; import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.Util; @@ -50,6 +51,7 @@ import org.session.libsession.utilities.dynamiclanguage.LocaleParser; import org.session.libsession.utilities.preferences.ProfileKeyUtil; import org.session.libsignal.service.api.util.StreamDetails; import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol; +import org.session.libsignal.utilities.ThreadUtils; import org.session.libsignal.utilities.logging.Log; import org.signal.aesgcmprovider.AesGcmProvider; import org.thoughtcrime.securesms.components.TypingStatusSender; @@ -91,8 +93,10 @@ import org.webrtc.PeerConnectionFactory.InitializationOptions; import org.webrtc.voiceengine.WebRtcAudioManager; import org.webrtc.voiceengine.WebRtcAudioUtils; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; +import java.io.InputStream; import java.security.SecureRandom; import java.security.Security; import java.util.Date; @@ -101,6 +105,7 @@ import java.util.Set; import dagger.ObjectGraph; import kotlin.Unit; +import kotlin.jvm.functions.Function1; import kotlinx.coroutines.Job; import network.loki.messenger.BuildConfig; @@ -481,21 +486,31 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc } private void resubmitProfilePictureIfNeeded() { + // Files expire on the file server after a while, so we simply re-upload the user's profile picture + // at a certain interval to ensure it's always available. String userPublicKey = TextSecurePreferences.getLocalNumber(this); if (userPublicKey == null) return; long now = new Date().getTime(); long lastProfilePictureUpload = TextSecurePreferences.getLastProfilePictureUpload(this); if (now - lastProfilePictureUpload <= 14 * 24 * 60 * 60 * 1000) return; - AsyncTask.execute(() -> { - String encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this); - byte[] profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey); + ThreadUtils.queue(() -> { + // Don't generate a new profile key here; we do that when the user changes their profile picture + String encodedProfileKey = TextSecurePreferences.getProfileKey(ApplicationContext.this); try { - File profilePicture = AvatarHelper.getAvatarFile(this, Address.fromSerialized(userPublicKey)); - StreamDetails stream = new StreamDetails(new FileInputStream(profilePicture), "image/jpeg", profilePicture.length()); - FileServerAPI.shared.uploadProfilePicture(FileServerAPI.shared.getServer(), profileKey, stream, () -> { - TextSecurePreferences.setLastProfilePictureUpload(this, new Date().getTime()); - TextSecurePreferences.setProfileAvatarId(this, new SecureRandom().nextInt()); - ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey); + // Read the file into a byte array + InputStream inputStream = AvatarHelper.getInputStreamFor(ApplicationContext.this, Address.fromSerialized(userPublicKey)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int count; + byte[] buffer = new byte[1024]; + while ((count = inputStream.read(buffer, 0, buffer.length)) != -1) { + baos.write(buffer, 0, count); + } + baos.flush(); + byte[] profilePicture = baos.toByteArray(); + // Re-upload it + ProfilePictureUtilities.INSTANCE.upload(profilePicture, encodedProfileKey, ApplicationContext.this).success(unit -> { + // Update the last profile picture upload date + TextSecurePreferences.setLastProfilePictureUpload(ApplicationContext.this, new Date().getTime()); return Unit.INSTANCE; }); } catch (Exception exception) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt index c046fd9115..077ced7b11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt @@ -23,18 +23,14 @@ import network.loki.messenger.BuildConfig import network.loki.messenger.R import nl.komponents.kovenant.Promise import nl.komponents.kovenant.all -import nl.komponents.kovenant.deferred -import nl.komponents.kovenant.functional.bind -import nl.komponents.kovenant.task import nl.komponents.kovenant.ui.alwaysUi import org.session.libsession.messaging.avatars.AvatarHelper -import org.session.libsession.messaging.file_server.FileServerAPI import org.session.libsession.messaging.open_groups.OpenGroupAPI import org.session.libsession.messaging.threads.Address +import org.session.libsession.utilities.ProfilePictureUtilities import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.preferences.ProfileKeyUtil -import org.session.libsignal.service.api.util.StreamDetails import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.avatar.AvatarSelection @@ -51,7 +47,6 @@ import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints import org.thoughtcrime.securesms.util.BitmapDecodingException import org.thoughtcrime.securesms.util.BitmapUtil -import java.io.ByteArrayInputStream import java.io.File import java.security.SecureRandom import java.util.* @@ -127,7 +122,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { AvatarSelection.REQUEST_CODE_AVATAR -> { - if (resultCode != Activity.RESULT_OK) { return } + if (resultCode != Activity.RESULT_OK) { + return + } val outputFile = Uri.fromFile(File(cacheDir, "cropped")) var inputFile: Uri? = data?.data if (inputFile == null && tempFile != null) { @@ -136,7 +133,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar) } AvatarSelection.REQUEST_CODE_CROP_IMAGE -> { - if (resultCode != Activity.RESULT_OK) { return } + if (resultCode != Activity.RESULT_OK) { + return + } AsyncTask.execute { try { profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap @@ -186,37 +185,23 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } val profilePicture = profilePictureToBeUploaded val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this) - val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey) if (isUpdatingProfilePicture && profilePicture != null) { - val storageAPI = FileServerAPI.shared - val deferred = deferred() - AsyncTask.execute { - val stream = StreamDetails(ByteArrayInputStream(profilePicture), "image/jpeg", profilePicture.size.toLong()) - val (_, url) = storageAPI.uploadProfilePicture(storageAPI.server, profileKey, stream) { - TextSecurePreferences.setLastProfilePictureUpload(this@SettingsActivity, Date().time) - } - TextSecurePreferences.setProfilePictureURL(this, url) - deferred.resolve(Unit) - } - promises.add(deferred.promise) + promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)) } - - all(promises).bind { - // updating the profile name or picture - if (profilePicture != null || displayName != null) { - task { - if (isUpdatingProfilePicture && profilePicture != null) { - AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) - TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt()) - ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) - ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded() - } - MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) - } - } else { - Promise.of(Unit) + val compoundPromise = all(promises) + compoundPromise.success { + if (isUpdatingProfilePicture && profilePicture != null) { + AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) + TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt()) + TextSecurePreferences.setLastProfilePictureUpload(this, Date().time) + ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) + ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded() } - }.alwaysUi { + if (profilePicture != null || displayName != null) { + MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@SettingsActivity) + } + } + compoundPromise.alwaysUi { if (displayName != null) { btnGroupNameDisplay.text = displayName } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt index a08ddb911d..d4ee80a47d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentDownloadJob.kt @@ -36,28 +36,29 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) } override fun execute() { + val storage = MessagingModuleConfiguration.shared.storage + val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val handleFailure: (java.lang.Exception) -> Unit = { exception -> if (exception == Error.NoAttachment) { - MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) + messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) this.handlePermanentFailure(exception) } else if (exception == DotNetAPI.Error.ParsingFailed) { // No need to retry if the response is invalid. Most likely this means we (incorrectly) // got a "Cannot GET ..." error from the file server. - MessagingModuleConfiguration.shared.messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) + messageDataProvider.setAttachmentState(AttachmentState.FAILED, attachmentID, databaseMessageID) this.handlePermanentFailure(exception) } else { this.handleFailure(exception) } } try { - val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val attachment = messageDataProvider.getDatabaseAttachment(attachmentID) ?: return handleFailure(Error.NoAttachment) messageDataProvider.setAttachmentState(AttachmentState.STARTED, attachmentID, this.databaseMessageID) val tempFile = createTempFile() - val threadId = MessagingModuleConfiguration.shared.storage.getThreadIdForMms(databaseMessageID) - val openGroupV2 = MessagingModuleConfiguration.shared.storage.getV2OpenGroup(threadId.toString()) - val stream = if (openGroupV2 == null) { + val threadID = storage.getThreadIdForMms(databaseMessageID) + val openGroupV2 = storage.getV2OpenGroup(threadID.toString()) + val inputStream = if (openGroupV2 == null) { DownloadUtilities.downloadFile(tempFile, attachment.url, FileServerAPI.maxFileSize, null) // Assume we're retrieving an attachment for an open group server if the digest is not set if (attachment.digest?.size ?: 0 == 0 || attachment.key.isNullOrEmpty()) { @@ -67,13 +68,13 @@ class AttachmentDownloadJob(val attachmentID: Long, val databaseMessageID: Long) } } else { val url = HttpUrl.parse(attachment.url)!! - val fileId = url.pathSegments().last() - OpenGroupAPIV2.download(fileId.toLong(), openGroupV2.room, openGroupV2.server).get().let { + val fileID = url.pathSegments().last() + OpenGroupAPIV2.download(fileID.toLong(), openGroupV2.room, openGroupV2.server).get().let { tempFile.writeBytes(it) } FileInputStream(tempFile) } - messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, stream) + messageDataProvider.insertAttachment(databaseMessageID, attachment.attachmentId, inputStream) tempFile.delete() handleSuccess() } catch (e: Exception) { diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt index a2485f3599..4f76bb9b9b 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/AttachmentUploadJob.kt @@ -101,7 +101,7 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess // encrypts as it writes data. val inputStream = if (encrypt) PaddingInputStream(attachment.inputStream, rawLength) else attachment.inputStream val outputStreamFactory = if (encrypt) AttachmentCipherOutputStreamFactory(key) else PlaintextOutputStreamFactory() - // Create a multipart request body but immediately read it out to a buffer. Doing this makes + // Create a digesting request body but immediately read it out to a buffer. Doing this makes // it easier to deal with inputStream and outputStreamFactory. val pad = PushAttachmentData(attachment.contentType, inputStream, length, outputStreamFactory, attachment.listener) val contentType = "application/octet-stream" diff --git a/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt b/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt new file mode 100644 index 0000000000..e95305b122 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/ProfilePictureUtilities.kt @@ -0,0 +1,46 @@ +package org.session.libsession.utilities + +import android.content.Context +import nl.komponents.kovenant.Promise +import nl.komponents.kovenant.deferred +import okio.Buffer +import org.session.libsession.messaging.file_server.FileServerAPIV2 +import org.session.libsession.utilities.preferences.ProfileKeyUtil +import org.session.libsignal.service.api.crypto.ProfileCipherOutputStream +import org.session.libsignal.service.internal.push.ProfileAvatarData +import org.session.libsignal.service.internal.push.http.DigestingRequestBody +import org.session.libsignal.service.internal.push.http.ProfileCipherOutputStreamFactory +import org.session.libsignal.service.loki.utilities.retryIfNeeded +import org.session.libsignal.utilities.ThreadUtils +import java.io.ByteArrayInputStream +import java.util.* + +object ProfilePictureUtilities { + + fun upload(profilePicture: ByteArray, encodedProfileKey: String, context: Context): Promise { + val deferred = deferred() + ThreadUtils.queue { + val inputStream = ByteArrayInputStream(profilePicture) + val outputStream = ProfileCipherOutputStream.getCiphertextLength(profilePicture.size.toLong()) + val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey) + val pad = ProfileAvatarData(inputStream, outputStream, "image/jpeg", ProfileCipherOutputStreamFactory(profileKey)) + val drb = DigestingRequestBody(pad.data, pad.outputStreamFactory, pad.contentType, pad.dataLength, null) + val b = Buffer() + drb.writeTo(b) + val data = b.readByteArray() + var id: Long = 0 + try { + id = retryIfNeeded(4) { + FileServerAPIV2.upload(data) + }.get() + } catch (e: Exception) { + deferred.reject(e) + } + TextSecurePreferences.setLastProfilePictureUpload(context, Date().time) + val url = "${FileServerAPIV2.SERVER}/files/$id" + TextSecurePreferences.setProfilePictureURL(context, url) + deferred.resolve(Unit) + } + return deferred.promise + } +} \ No newline at end of file From d9348c5442b568f9004e6ee886c535736de50f3a Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 13 May 2021 15:27:08 +1000 Subject: [PATCH 05/27] Remove weird roundabout way of doing decryption --- .../securesms/ApplicationContext.java | 4 +- .../securesms/database/Storage.kt | 2 +- .../loki/activities/SettingsActivity.kt | 5 +- .../loki/protocol/ClosedGroupsProtocolV2.kt | 8 +-- .../messaging/MessagingModuleConfiguration.kt | 9 ++-- .../libsession/messaging/StorageProtocol.kt | 2 +- .../sending_receiving/MessageDecrypter.kt | 21 +++----- .../sending_receiving/MessageReceiver.kt | 36 +++++-------- .../MessageReceiverDecryption.kt | 11 ---- .../ReceivedMessageHandler.kt | 4 +- .../api/crypto/SignalServiceCipher.java | 29 +--------- .../libsignal/service/loki/SessionProtocol.kt | 53 ------------------- 12 files changed, 37 insertions(+), 147 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt => libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt (80%) delete mode 100644 libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt delete mode 100644 libsignal/src/main/java/org/session/libsignal/service/loki/SessionProtocol.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 16ee21cabb..2e8294b565 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -70,7 +70,6 @@ import org.thoughtcrime.securesms.loki.activities.HomeActivity; import org.thoughtcrime.securesms.loki.api.BackgroundPollWorker; import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager; import org.thoughtcrime.securesms.loki.api.PublicChatManager; -import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl; import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase; import org.thoughtcrime.securesms.loki.database.LokiThreadDatabase; import org.thoughtcrime.securesms.loki.database.LokiUserDatabase; @@ -178,8 +177,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc String userPublicKey = TextSecurePreferences.getLocalNumber(this); MessagingModuleConfiguration.Companion.configure(this, DatabaseFactory.getStorage(this), - DatabaseFactory.getAttachmentProvider(this), - new SessionProtocolImpl(this)); + DatabaseFactory.getAttachmentProvider(this)); SnodeModule.Companion.configure(apiDB, broadcaster); if (userPublicKey != null) { MentionsManager.Companion.configureIfNeeded(userPublicKey, userDB); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 2c82d60aa7..42cb63eb1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -347,7 +347,7 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, DatabaseFactory.getLokiAPIDatabase(context).removeLastDeletionServerID(group, server) } - override fun isMessageDuplicated(timestamp: Long, sender: String): Boolean { + override fun isDuplicateMessage(timestamp: Long, sender: String): Boolean { return getReceivedMessageTimestamps().contains(timestamp) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt index 077ced7b11..6c6d95511f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt @@ -24,6 +24,7 @@ import network.loki.messenger.R import nl.komponents.kovenant.Promise import nl.komponents.kovenant.all import nl.komponents.kovenant.ui.alwaysUi +import nl.komponents.kovenant.ui.successUi import org.session.libsession.messaging.avatars.AvatarHelper import org.session.libsession.messaging.open_groups.OpenGroupAPI import org.session.libsession.messaging.threads.Address @@ -189,7 +190,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { promises.add(ProfilePictureUtilities.upload(profilePicture, encodedProfileKey, this)) } val compoundPromise = all(promises) - compoundPromise.success { + compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below if (isUpdatingProfilePicture && profilePicture != null) { AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt()) @@ -206,7 +207,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { btnGroupNameDisplay.text = displayName } if (isUpdatingProfilePicture && profilePicture != null) { - profilePictureView.recycle() // clear cached image before update tje profilePictureView + profilePictureView.recycle() // Clear the cached image before updating profilePictureView.update() } displayNameToBeUploaded = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index 5f61506504..5d181e5207 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.loki.protocol import android.content.Context import android.util.Log import com.google.protobuf.ByteString +import org.session.libsession.messaging.sending_receiving.* import org.session.libsignal.libsignal.ecc.DjbECPrivateKey import org.session.libsignal.libsignal.ecc.DjbECPublicKey import org.session.libsignal.libsignal.ecc.ECKeyPair @@ -15,12 +16,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager import org.thoughtcrime.securesms.loki.api.LokiPushNotificationManager.ClosedGroupOperation -import org.thoughtcrime.securesms.loki.api.SessionProtocolImpl import org.thoughtcrime.securesms.loki.database.LokiAPIDatabase -import org.session.libsession.messaging.sending_receiving.MessageSender -import org.session.libsession.messaging.sending_receiving.generateAndSendNewEncryptionKeyPair -import org.session.libsession.messaging.sending_receiving.pendingKeyPair -import org.session.libsession.messaging.sending_receiving.sendEncryptionKeyPair import org.session.libsession.messaging.threads.Address import org.session.libsession.messaging.threads.GroupRecord @@ -330,7 +326,7 @@ object ClosedGroupsProtocolV2 { // Find our wrapper and decrypt it if possible val wrapper = closedGroupUpdate.wrappersList.firstOrNull { it.publicKey.toByteArray().toHexString() == userPublicKey } ?: return val encryptedKeyPair = wrapper.encryptedKeyPair.toByteArray() - val plaintext = SessionProtocolImpl(context).decrypt(encryptedKeyPair, userKeyPair).first + val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first // Parse it val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext) val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray())) diff --git a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt index 68610f9638..98a9588332 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/MessagingModuleConfiguration.kt @@ -2,13 +2,11 @@ package org.session.libsession.messaging import android.content.Context import org.session.libsession.database.MessageDataProvider -import org.session.libsignal.service.loki.api.crypto.SessionProtocol class MessagingModuleConfiguration( val context: Context, val storage: StorageProtocol, - val messageDataProvider: MessageDataProvider, - val sessionProtocol: SessionProtocol + val messageDataProvider: MessageDataProvider ) { companion object { @@ -16,11 +14,10 @@ class MessagingModuleConfiguration( fun configure(context: Context, storage: StorageProtocol, - messageDataProvider: MessageDataProvider, - sessionProtocol: SessionProtocol + messageDataProvider: MessageDataProvider ) { if (Companion::shared.isInitialized) { return } - shared = MessagingModuleConfiguration(context, storage, messageDataProvider, sessionProtocol) + shared = MessagingModuleConfiguration(context, storage, messageDataProvider) } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt index d850e30664..816ede3414 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/StorageProtocol.kt @@ -92,7 +92,7 @@ interface StorageProtocol { fun removeLastDeletionServerId(room: String, server: String) // Message Handling - fun isMessageDuplicated(timestamp: Long, sender: String): Boolean + fun isDuplicateMessage(timestamp: Long, sender: String): Boolean fun getReceivedMessageTimestamps(): Set fun addReceivedMessageTimestamp(timestamp: Long) fun removeReceivedMessageTimestamps(timestamps: Set) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt similarity index 80% rename from app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt rename to libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt index 4916ef483d..e7b09bc623 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/api/SessionProtocolImpl.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt @@ -1,26 +1,21 @@ -package org.thoughtcrime.securesms.loki.api +package org.session.libsession.messaging.sending_receiving -import android.content.Context import android.util.Log 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.libsignal.utilities.Hex - import org.session.libsignal.libsignal.ecc.ECKeyPair -import org.session.libsignal.service.loki.api.crypto.SessionProtocol import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.service.loki.utilities.toHexString -import org.session.libsession.utilities.KeyPairUtilities +import org.session.libsignal.utilities.Hex -class SessionProtocolImpl(private val context: Context) : SessionProtocol { +object MessageDecrypter { private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } - override fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair { + public fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair { val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize() val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded()) val signatureSize = Sign.BYTES @@ -32,9 +27,9 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol { sodium.cryptoBoxSealOpen(plaintextWithMetadata, ciphertext, ciphertext.size.toLong(), recipientX25519PublicKey, recipientX25519PrivateKey) } catch (exception: Exception) { Log.d("Loki", "Couldn't decrypt message due to error: $exception.") - throw SessionProtocol.Exception.DecryptionFailed + throw MessageReceiver.Error.DecryptionFailed } - if (plaintextWithMetadata.size <= (signatureSize + ed25519PublicKeySize)) { throw SessionProtocol.Exception.DecryptionFailed } + if (plaintextWithMetadata.size <= (signatureSize + ed25519PublicKeySize)) { throw MessageReceiver.Error.DecryptionFailed } // 2. ) Get the message parts val signature = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - signatureSize until plaintextWithMetadata.size) val senderED25519PublicKey = plaintextWithMetadata.sliceArray(plaintextWithMetadata.size - (signatureSize + ed25519PublicKeySize) until plaintextWithMetadata.size - signatureSize) @@ -43,10 +38,10 @@ class SessionProtocolImpl(private val context: Context) : SessionProtocol { val verificationData = (plaintext + senderED25519PublicKey + recipientX25519PublicKey) try { val isValid = sodium.cryptoSignVerifyDetached(signature, verificationData, verificationData.size, senderED25519PublicKey) - if (!isValid) { throw SessionProtocol.Exception.InvalidSignature } + if (!isValid) { throw MessageReceiver.Error.InvalidSignature } } catch (exception: Exception) { Log.d("Loki", "Couldn't verify message signature due to error: $exception.") - throw SessionProtocol.Exception.InvalidSignature + throw MessageReceiver.Error.InvalidSignature } // 4. ) Get the sender's X25519 public key val senderX25519PublicKey = ByteArray(Sign.CURVE25519_PUBLICKEYBYTES) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index d6c2d82c76..28f73521f2 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -10,35 +10,25 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos object MessageReceiver { - private val lastEncryptionKeyPairRequest = mutableMapOf() - - internal sealed class Error(val description: String) : Exception(description) { + internal sealed class Error(message: String) : Exception(message) { object DuplicateMessage: Error("Duplicate message.") object InvalidMessage: Error("Invalid message.") object UnknownMessage: Error("Unknown message type.") object UnknownEnvelopeType: Error("Unknown envelope type.") - object NoUserX25519KeyPair: Error("Couldn't find user X25519 key pair.") - object NoUserED25519KeyPair: Error("Couldn't find user ED25519 key pair.") + object DecryptionFailed : Exception("Couldn't decrypt message.") object InvalidSignature: Error("Invalid message signature.") object NoData: Error("Received an empty envelope.") object SenderBlocked: Error("Received a message from a blocked user.") object NoThread: Error("Couldn't find thread for message.") object SelfSend: Error("Message addressed at self.") - object ParsingFailed : Error("Couldn't parse ciphertext message.") // Shared sender keys object InvalidGroupPublicKey: Error("Invalid group public key.") object NoGroupKeyPair: Error("Missing group key pair.") internal val isRetryable: Boolean = when (this) { - is DuplicateMessage -> false - is InvalidMessage -> false - is UnknownMessage -> false - is UnknownEnvelopeType -> false - is InvalidSignature -> false - is NoData -> false - is NoThread -> false - is SenderBlocked -> false - is SelfSend -> false + is DuplicateMessage, is InvalidMessage, is UnknownMessage, + is UnknownEnvelopeType, is InvalidSignature, is NoData, + is SenderBlocked, is SelfSend -> false else -> true } } @@ -46,13 +36,15 @@ object MessageReceiver { internal fun parse(data: ByteArray, openGroupServerID: Long?, isRetry: Boolean = false): Pair { val storage = MessagingModuleConfiguration.shared.storage val userPublicKey = storage.getUserPublicKey() - val isOpenGroupMessage = openGroupServerID != null + val isOpenGroupMessage = (openGroupServerID != null) // Parse the envelope val envelope = SignalServiceProtos.Envelope.parseFrom(data) // If the message failed to process the first time around we retry it later (if the error is retryable). In this case the timestamp // will already be in the database but we don't want to treat the message as a duplicate. The isRetry flag is a simple workaround // for this issue. - if (storage.isMessageDuplicated(envelope.timestamp, GroupUtil.doubleEncodeGroupID(envelope.source)) && !isRetry) throw Error.DuplicateMessage + if (storage.isDuplicateMessage(envelope.timestamp, GroupUtil.doubleEncodeGroupID(envelope.source)) && !isRetry) { + throw Error.DuplicateMessage + } // Decrypt the contents val ciphertext = envelope.content ?: throw Error.NoData var plaintext: ByteArray? = null @@ -65,7 +57,7 @@ object MessageReceiver { when (envelope.type) { SignalServiceProtos.Envelope.Type.SESSION_MESSAGE -> { val userX25519KeyPair = MessagingModuleConfiguration.shared.storage.getUserX25519KeyPair() - val decryptionResult = MessageReceiverDecryption.decryptWithSessionProtocol(ciphertext.toByteArray(), userX25519KeyPair) + val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), userX25519KeyPair) plaintext = decryptionResult.first sender = decryptionResult.second } @@ -81,7 +73,7 @@ object MessageReceiver { var encryptionKeyPair = encryptionKeyPairs.removeLast() fun decrypt() { try { - val decryptionResult = MessageReceiverDecryption.decryptWithSessionProtocol(ciphertext.toByteArray(), encryptionKeyPair) + val decryptionResult = MessageDecrypter.decrypt(ciphertext.toByteArray(), encryptionKeyPair) plaintext = decryptionResult.first sender = decryptionResult.second } catch (e: Exception) { @@ -100,9 +92,9 @@ object MessageReceiver { } } // Don't process the envelope any further if the message has been handled already - if (storage.isMessageDuplicated(envelope.timestamp, sender!!) && !isRetry) throw Error.DuplicateMessage + if (storage.isDuplicateMessage(envelope.timestamp, sender!!) && !isRetry) throw Error.DuplicateMessage // Don't process the envelope any further if the sender is blocked - if (isBlock(sender!!)) throw Error.SenderBlocked + if (isBlocked(sender!!)) throw Error.SenderBlocked // Parse the proto val proto = SignalServiceProtos.Content.parseFrom(PushTransportDetails.getStrippedPaddingMessageBody(plaintext)) // Parse the message @@ -113,7 +105,7 @@ object MessageReceiver { ExpirationTimerUpdate.fromProto(proto) ?: ConfigurationMessage.fromProto(proto) ?: VisibleMessage.fromProto(proto) ?: throw Error.UnknownMessage - // Ignore self sends if needed + // Ignore self send if needed if (!message.isSelfSendValid && sender == userPublicKey) throw Error.SelfSend // Guard against control messages in open groups if (isOpenGroupMessage && message !is VisibleMessage) throw Error.InvalidMessage diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt deleted file mode 100644 index 6c0fb5953e..0000000000 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiverDecryption.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.session.libsession.messaging.sending_receiving - -import org.session.libsession.messaging.MessagingModuleConfiguration -import org.session.libsignal.libsignal.ecc.ECKeyPair - -object MessageReceiverDecryption { - - internal fun decryptWithSessionProtocol(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair { - return MessagingModuleConfiguration.shared.sessionProtocol.decrypt(ciphertext, x25519KeyPair) - } -} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index be331891b0..ec5744c7f0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -34,7 +34,7 @@ import java.security.MessageDigest import java.util.* import kotlin.collections.ArrayList -internal fun MessageReceiver.isBlock(publicKey: String): Boolean { +internal fun MessageReceiver.isBlocked(publicKey: String): Boolean { val context = MessagingModuleConfiguration.shared.context val recipient = Recipient.from(context, Address.fromSerialized(publicKey), false) return recipient.isBlocked @@ -323,7 +323,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr // Find our wrapper and decrypt it if possible val wrapper = kind.wrappers.firstOrNull { it.publicKey!! == userPublicKey } ?: return val encryptedKeyPair = wrapper.encryptedKeyPair!!.toByteArray() - val plaintext = MessageReceiverDecryption.decryptWithSessionProtocol(encryptedKeyPair, userKeyPair).first + val plaintext = MessageDecrypter.decrypt(encryptedKeyPair, userKeyPair).first // Parse it val proto = SignalServiceProtos.KeyPair.parseFrom(plaintext) val keyPair = ECKeyPair(DjbECPublicKey(proto.publicKey.toByteArray().removing05PrefixIfNeeded()), DjbECPrivateKey(proto.privateKey.toByteArray())) diff --git a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java index 5e425464ea..7f1cd6051e 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java +++ b/libsignal/src/main/java/org/session/libsignal/service/api/crypto/SignalServiceCipher.java @@ -31,8 +31,6 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos.Content; import org.session.libsignal.service.internal.push.SignalServiceProtos.DataMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.ReceiptMessage; import org.session.libsignal.service.internal.push.SignalServiceProtos.TypingMessage; -import org.session.libsignal.service.loki.api.crypto.SessionProtocol; -import org.session.libsignal.service.loki.api.crypto.SessionProtocolUtilities; import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol; import java.util.ArrayList; @@ -51,13 +49,10 @@ public class SignalServiceCipher { @SuppressWarnings("unused") private static final String TAG = SignalServiceCipher.class.getSimpleName(); - private final SessionProtocol sessionProtocolImpl; private final LokiAPIDatabaseProtocol apiDB; - public SignalServiceCipher(SessionProtocol sessionProtocolImpl, - LokiAPIDatabaseProtocol apiDB) + public SignalServiceCipher(LokiAPIDatabaseProtocol apiDB) { - this.sessionProtocolImpl = sessionProtocolImpl; this.apiDB = apiDB; } @@ -125,27 +120,7 @@ public class SignalServiceCipher { protected Plaintext decrypt(SignalServiceEnvelope envelope, byte[] ciphertext) throws InvalidMetadataMessageException { - byte[] paddedMessage; - Metadata metadata; - - if (envelope.isClosedGroupCiphertext()) { - String groupPublicKey = envelope.getSource(); - kotlin.Pair plaintextAndSenderPublicKey = SessionProtocolUtilities.INSTANCE.decryptClosedGroupCiphertext(ciphertext, groupPublicKey, apiDB, sessionProtocolImpl); - paddedMessage = plaintextAndSenderPublicKey.getFirst(); - String senderPublicKey = plaintextAndSenderPublicKey.getSecond(); - metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false); - } else if (envelope.isUnidentifiedSender()) { - ECKeyPair userX25519KeyPair = apiDB.getUserX25519KeyPair(); - kotlin.Pair plaintextAndSenderPublicKey = sessionProtocolImpl.decrypt(ciphertext, userX25519KeyPair); - paddedMessage = plaintextAndSenderPublicKey.getFirst(); - String senderPublicKey = plaintextAndSenderPublicKey.getSecond(); - metadata = new Metadata(senderPublicKey, 1, envelope.getTimestamp(), false); - } else { - throw new InvalidMetadataMessageException("Unknown type: " + envelope.getType()); - } - byte[] data = PushTransportDetails.getStrippedPaddingMessageBody(paddedMessage); - - return new Plaintext(metadata, data); + throw new IllegalStateException("This shouldn't be used anymore"); } private SignalServiceDataMessage createSignalServiceMessage(Metadata metadata, DataMessage content) throws ProtocolInvalidMessageException { diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/SessionProtocol.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/SessionProtocol.kt deleted file mode 100644 index 201ad4e85d..0000000000 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/SessionProtocol.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.session.libsignal.service.loki.api.crypto - -import org.session.libsignal.libsignal.ecc.ECKeyPair -import org.session.libsignal.service.loki.LokiAPIDatabaseProtocol - -interface SessionProtocol { - - sealed class Exception(val description: String) : kotlin.Exception(description) { - // Encryption - object NoUserED25519KeyPair : Exception("Couldn't find user ED25519 key pair.") - object SigningFailed : Exception("Couldn't sign message.") - object EncryptionFailed : Exception("Couldn't encrypt message.") - // Decryption - object NoData : Exception("Received an empty envelope.") - object InvalidGroupPublicKey : Exception("Invalid group public key.") - object NoGroupKeyPair : Exception("Missing group key pair.") - object DecryptionFailed : Exception("Couldn't decrypt message.") - object InvalidSignature : Exception("Invalid message signature.") - } - /** - * Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`. - * - * @param ciphertext the data to decrypt. - * @param x25519KeyPair the key pair to use for decryption. This could be the current user's key pair, or the key pair of a closed group. - * - * @return the padded plaintext. - */ - fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair -} - -object SessionProtocolUtilities { - - fun decryptClosedGroupCiphertext(ciphertext: ByteArray, groupPublicKey: String, apiDB: LokiAPIDatabaseProtocol, sessionProtocolImpl: SessionProtocol): Pair { - val encryptionKeyPairs = apiDB.getClosedGroupEncryptionKeyPairs(groupPublicKey).toMutableList() - if (encryptionKeyPairs.isEmpty()) { throw SessionProtocol.Exception.NoGroupKeyPair } - // Loop through all known group key pairs in reverse order (i.e. try the latest key pair first (which'll more than - // likely be the one we want) but try older ones in case that didn't work) - var encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex) - fun decrypt(): Pair { - try { - return sessionProtocolImpl.decrypt(ciphertext, encryptionKeyPair) - } catch(exception: Exception) { - if (encryptionKeyPairs.isNotEmpty()) { - encryptionKeyPair = encryptionKeyPairs.removeAt(encryptionKeyPairs.lastIndex) - return decrypt() - } else { - throw exception - } - } - } - return decrypt() - } -} \ No newline at end of file From ea71d285b74c6c1e92c3c9832f97c8afcd96d570 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 13 May 2021 15:42:14 +1000 Subject: [PATCH 06/27] Clean --- .../ReceivedMessageHandler.kt | 160 ++++++++---------- 1 file changed, 68 insertions(+), 92 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index ec5744c7f0..ea5cac4c77 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -93,12 +93,9 @@ private fun MessageReceiver.handleExpirationTimerUpdate(message: ExpirationTimer } } -// Data Extraction Notification handling - private fun MessageReceiver.handleDataExtractionNotification(message: DataExtractionNotification) { - // we don't handle data extraction messages for groups (they shouldn't be sent, but in case we filter them here too) + // We don't handle data extraction messages for groups (they shouldn't be sent, but just in case we filter them here too) if (message.groupPublicKey != null) return - val storage = MessagingModuleConfiguration.shared.storage val senderPublicKey = message.sender!! val notification: DataExtractionNotificationInfoMessage = when(message.kind) { @@ -109,20 +106,20 @@ private fun MessageReceiver.handleDataExtractionNotification(message: DataExtrac storage.insertDataExtractionNotificationMessage(senderPublicKey, notification, message.sentTimestamp!!) } -// Configuration message handling - private fun handleConfigurationMessage(message: ConfigurationMessage) { val context = MessagingModuleConfiguration.shared.context val storage = MessagingModuleConfiguration.shared.storage - if (TextSecurePreferences.getConfigurationMessageSynced(context) && !TextSecurePreferences.shouldUpdateProfile(context, message.sentTimestamp!!)) return + if (TextSecurePreferences.getConfigurationMessageSynced(context) + && !TextSecurePreferences.shouldUpdateProfile(context, message.sentTimestamp!!)) return val userPublicKey = storage.getUserPublicKey() if (userPublicKey == null || message.sender != storage.getUserPublicKey()) return TextSecurePreferences.setConfigurationMessageSynced(context, true) TextSecurePreferences.setLastProfileUpdateTime(context, message.sentTimestamp!!) val allClosedGroupPublicKeys = storage.getAllClosedGroupPublicKeys() - for (closeGroup in message.closedGroups) { - if (allClosedGroupPublicKeys.contains(closeGroup.publicKey)) continue - handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closeGroup.publicKey, closeGroup.name, closeGroup.encryptionKeyPair!!, closeGroup.members, closeGroup.admins, message.sentTimestamp!!) + for (closedGroup in message.closedGroups) { + if (allClosedGroupPublicKeys.contains(closedGroup.publicKey)) continue + handleNewClosedGroup(message.sender!!, message.sentTimestamp!!, closedGroup.publicKey, closedGroup.name, + closedGroup.encryptionKeyPair!!, closedGroup.members, closedGroup.admins, message.sentTimestamp!!) } val allOpenGroups = storage.getAllOpenGroups().map { it.value.server } val allV2OpenGroups = storage.getAllV2OpenGroups().map { it.value.joinURL } @@ -134,14 +131,12 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) { TextSecurePreferences.setProfileName(context, message.displayName) storage.setDisplayName(userPublicKey, message.displayName) } - if (message.profileKey.isNotEmpty()) { + if (message.profileKey.isNotEmpty() && !message.profilePicture.isNullOrEmpty() + && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) { val profileKey = Base64.encodeBytes(message.profileKey) ProfileKeyUtil.setEncodedProfileKey(context, profileKey) storage.setProfileKeyForRecipient(userPublicKey, message.profileKey) - // handle profile photo - if (!message.profilePicture.isNullOrEmpty() && TextSecurePreferences.getProfilePictureURL(context) != message.profilePicture) { - storage.setUserProfilePictureUrl(message.profilePicture!!) - } + storage.setUserProfilePictureUrl(message.profilePicture!!) } storage.addContacts(message.contacts) } @@ -150,41 +145,32 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS val storage = MessagingModuleConfiguration.shared.storage val context = MessagingModuleConfiguration.shared.context val userPublicKey = storage.getUserPublicKey() - // Get or create thread + // FIXME: In case this is an open group this actually * doesn't * create the thread if it doesn't yet + // exist. This is intentional, but it's very non-obvious. val threadID = storage.getOrCreateThreadIdFor(message.syncTarget ?: message.sender!!, message.groupPublicKey, openGroupID) - if (threadID < 0) { - // thread doesn't exist, should only be reached in a case where we are processing open group messages for no longer existent thread + // Thread doesn't exist; should only be reached in a case where we are processing open group messages for a no longer existent thread throw MessageReceiver.Error.NoThread } - val openGroup = threadID.let { storage.getOpenGroup(it.toString()) } - // Update profile if needed - val newProfile = message.profile - - if (newProfile != null && userPublicKey != message.sender && openGroup == null) { + val profile = message.profile + if (profile != null && userPublicKey != message.sender && openGroup == null) { // Don't do this in V1 open groups val profileManager = SSKEnvironment.shared.profileManager val recipient = Recipient.from(context, Address.fromSerialized(message.sender!!), false) - val displayName = newProfile.displayName!! + val displayName = profile.displayName!! if (displayName.isNotEmpty()) { profileManager.setDisplayName(context, recipient, displayName) } - if (newProfile.profileKey?.isNotEmpty() == true - && (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, newProfile.profileKey))) { - profileManager.setProfileKey(context, recipient, newProfile.profileKey!!) + if (profile.profileKey?.isNotEmpty() == true && profile.profilePictureURL?.isNotEmpty() == true + && (recipient.profileKey == null || !MessageDigest.isEqual(recipient.profileKey, profile.profileKey))) { + profileManager.setProfileKey(context, recipient, profile.profileKey!!) profileManager.setUnidentifiedAccessMode(context, recipient, Recipient.UnidentifiedAccessMode.UNKNOWN) - val newUrl = newProfile.profilePictureURL - if (!newUrl.isNullOrEmpty()) { - profileManager.setProfilePictureURL(context, recipient, newUrl) - if (userPublicKey == message.sender) { - profileManager.updateOpenGroupProfilePicturesIfNeeded(context) - } - } + profileManager.setProfilePictureURL(context, recipient, profile.profilePictureURL!!) } } // Parse quote if needed @@ -192,10 +178,11 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS if (message.quote != null && proto.dataMessage.hasQuote()) { val quote = proto.dataMessage.quote val author = Address.fromSerialized(quote.author) - val messageInfo = MessagingModuleConfiguration.shared.messageDataProvider.getMessageForQuote(quote.id, author) + val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider + val messageInfo = messageDataProvider.getMessageForQuote(quote.id, author) if (messageInfo != null) { - val attachments = if (messageInfo.second) MessagingModuleConfiguration.shared.messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList() - quoteModel = QuoteModel(quote.id, author, MessagingModuleConfiguration.shared.messageDataProvider.getMessageBodyFor(quote.id, quote.author), false, attachments) + val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList() + quoteModel = QuoteModel(quote.id, author, messageDataProvider.getMessageBodyFor(quote.id, quote.author), false, attachments) } else { quoteModel = QuoteModel(quote.id, author, quote.text, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList)) } @@ -216,6 +203,7 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS } } } + // Parse attachments if needed val attachments = proto.dataMessage.attachmentsList.mapNotNull { proto -> val attachment = Attachment.fromProto(proto) if (!attachment.isValid()) { @@ -224,7 +212,6 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, proto: SignalS return@mapNotNull attachment } } - // Parse stickers if needed // Persist the message message.threadID = threadID val messageID = storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, attachments) ?: throw MessageReceiver.Error.DuplicateMessage @@ -265,7 +252,6 @@ private fun MessageReceiver.handleNewClosedGroup(message: ClosedGroupControlMess 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, sentTimestamp: Long, groupPublicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: List, admins: List, formationTimestamp: Long) { val context = MessagingModuleConfiguration.shared.context val storage = MessagingModuleConfiguration.shared.storage @@ -277,11 +263,10 @@ private fun handleNewClosedGroup(sender: String, sentTimestamp: Long, groupPubli storage.updateMembers(groupID, members.map { Address.fromSerialized(it) }) } else { storage.createGroup(groupID, name, LinkedList(members.map { Address.fromSerialized(it) }), - null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp) + null, null, LinkedList(admins.map { Address.fromSerialized(it) }), formationTimestamp) val userPublicKey = TextSecurePreferences.getLocalNumber(context) // Notify the user if (userPublicKey == sender) { - // sender is a linked device val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.CREATION, name, members, admins, threadID, sentTimestamp) } else { @@ -309,11 +294,11 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr // Unwrap the message val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val group = storage.getGroup(groupID) ?: run { - Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + Log.d("Loki", "Ignoring closed group encryption key pair for nonexistent group.") return } if (!group.isActive) { - Log.d("Loki", "Ignoring closed group info message for inactive group") + Log.d("Loki", "Ignoring closed group encryption key pair for inactive group.") return } if (!group.admins.map { it.toString() }.contains(senderPublicKey)) { @@ -334,7 +319,7 @@ private fun MessageReceiver.handleClosedGroupEncryptionKeyPair(message: ClosedGr return } storage.addClosedGroupEncryptionKeyPair(keyPair, groupPublicKey) - Log.d("Loki", "Received a new closed group encryption key pair") + Log.d("Loki", "Received a new closed group encryption key pair.") } private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupControlMessage) { @@ -347,11 +332,11 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon // Check that the sender is a member of the group (before the update) val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val group = storage.getGroup(groupID) ?: run { - Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + Log.d("Loki", "Ignoring closed group update for nonexistent group.") return } if (!group.isActive) { - Log.d("Loki", "Ignoring closed group info message for inactive group") + Log.d("Loki", "Ignoring closed group update for inactive group.") return } // Check common group update logic @@ -362,10 +347,8 @@ private fun MessageReceiver.handleClosedGroupNameChanged(message: ClosedGroupCon val admins = group.admins.map { it.serialize() } val name = kind.name storage.updateTitle(groupID, name) - // Notify the user if (userPublicKey == senderPublicKey) { - // sender is a linked device val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.NAME_CHANGE, name, members, admins, threadID, message.sentTimestamp!!) } else { @@ -382,11 +365,11 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo val groupPublicKey = message.groupPublicKey ?: return val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val group = storage.getGroup(groupID) ?: run { - Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + Log.d("Loki", "Ignoring closed group update for nonexistent group.") return } if (!group.isActive) { - Log.d("Loki", "Ignoring closed group info message for inactive group") + Log.d("Loki", "Ignoring closed group update for inactive group.") return } if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return } @@ -398,19 +381,28 @@ 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) }) - // Notify the user if (userPublicKey == senderPublicKey) { - // sender is a linked device val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, threadID, message.sentTimestamp!!) } else { storage.insertIncomingInfoMessage(context, senderPublicKey, groupID, SignalServiceGroup.Type.MEMBER_ADDED, name, updateMembers, admins, message.sentTimestamp!!) } if (userPublicKey in admins) { - // send current encryption key to the latest added members + // Send the latest encryption key pair to the added members if the current user is the admin of the group + // + // This fixes a race condition where: + // • A member removes another member. + // • A member adds someone to the group and sends them the latest group key pair. + // • The admin is offline during all of this. + // • When the admin comes back online they see the member removed message and generate + distribute a new key pair, + // but they don't know about the added member yet. + // • Now they see the member added message. + // + // Without the code below, the added member(s) would never get the key pair that was generated by the admin when they saw + // the member removed message. val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() - ?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) + ?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) if (encryptionKeyPair == null) { android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.") } else { @@ -435,65 +427,54 @@ private fun MessageReceiver.handleClosedGroupMembersRemoved(message: ClosedGroup val groupPublicKey = message.groupPublicKey ?: return val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val group = storage.getGroup(groupID) ?: run { - Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + Log.d("Loki", "Ignoring closed group update for nonexistent group.") return } if (!group.isActive) { - Log.d("Loki", "Ignoring closed group info message for inactive group.") + Log.d("Loki", "Ignoring closed group update for inactive group.") return } val name = group.title // Check common group update logic val members = group.members.map { it.serialize() } val admins = group.admins.map { it.toString() } - - // Users that are part of this remove update - val updateMembers = kind.members.map { it.toByteArray().toHexString() } - + val removedMembers = kind.members.map { it.toByteArray().toHexString() } // Check that the admin wasn't removed - if (updateMembers.contains(admins.first())) { + if (removedMembers.contains(admins.first())) { Log.d("Loki", "Ignoring invalid closed group update.") return } - // Check that the message was sent by the group admin if (!admins.contains(senderPublicKey)) { Log.d("Loki", "Ignoring invalid closed group update.") return } - if (!isValidGroupUpdate(group, message.sentTimestamp!!, senderPublicKey)) { return } - // If admin leaves the group is disbanded - val didAdminLeave = admins.any { it in updateMembers } - // newMembers to save is old members minus removed members - val newMembers = members - updateMembers - // user should be posting MEMBERS_LEFT so this should not be encountered - val senderLeft = senderPublicKey in updateMembers + // If the admin leaves the group is disbanded + val didAdminLeave = admins.any { it in removedMembers } + val newMembers = members - removedMembers + // A user should be posting a MEMBERS_LEFT in case they leave, so this shouldn't be encountered + val senderLeft = senderPublicKey in removedMembers if (senderLeft) { - android.util.Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender $senderPublicKey") + Log.d("Loki", "Received a MEMBERS_REMOVED instead of a MEMBERS_LEFT from sender: $senderPublicKey.") } - val wasCurrentUserRemoved = userPublicKey in updateMembers - - // admin should send a MEMBERS_LEFT message but handled here in case + val wasCurrentUserRemoved = userPublicKey in removedMembers + // Admin should send a MEMBERS_LEFT message but handled here just in case if (didAdminLeave || wasCurrentUserRemoved) { disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) } else { storage.updateMembers(groupID, newMembers.map { Address.fromSerialized(it) }) } - // update zombie members + // Update zombie members val zombies = storage.getZombieMember(groupID) - storage.updateZombieMembers(groupID, zombies.minus(updateMembers).map { Address.fromSerialized(it) }) - - val type = if (senderLeft) SignalServiceGroup.Type.QUIT - else SignalServiceGroup.Type.MEMBER_REMOVED - + storage.updateZombieMembers(groupID, zombies.minus(removedMembers).map { Address.fromSerialized(it) }) + val type = if (senderLeft) SignalServiceGroup.Type.QUIT else SignalServiceGroup.Type.MEMBER_REMOVED // Notify the user - // we don't display zombie members in the notification as users have already been notified when those members left - val notificationMembers = updateMembers.minus(zombies) + // We don't display zombie members in the notification as users have already been notified when those members left + val notificationMembers = removedMembers.minus(zombies) if (notificationMembers.isNotEmpty()) { - // no notification to display when only zombies have been removed + // No notification to display when only zombies have been removed if (userPublicKey == senderPublicKey) { - // sender is a linked device val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) storage.insertOutgoingInfoMessage(context, groupID, type, name, notificationMembers, admins, threadID, message.sentTimestamp!!) } else { @@ -515,11 +496,11 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont val groupPublicKey = message.groupPublicKey ?: return val groupID = GroupUtil.doubleEncodeGroupID(groupPublicKey) val group = storage.getGroup(groupID) ?: run { - Log.d("Loki", "Ignoring closed group info message for nonexistent group.") + Log.d("Loki", "Ignoring closed group update for nonexistent group.") return } if (!group.isActive) { - Log.d("Loki", "Ignoring closed group info message for inactive group") + Log.d("Loki", "Ignoring closed group update for inactive group.") return } val name = group.title @@ -533,19 +514,16 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont val didAdminLeave = admins.contains(senderPublicKey) val updatedMemberList = members - senderPublicKey val userLeft = (userPublicKey == senderPublicKey) - if (didAdminLeave || userLeft) { - // admin left the group of linked device left the group disableLocalGroupAndUnsubscribe(groupPublicKey, groupID, userPublicKey) } else { storage.updateMembers(groupID, updatedMemberList.map { Address.fromSerialized(it) }) - // update zombie members + // Update zombie members val zombies = storage.getZombieMember(groupID) storage.updateZombieMembers(groupID, zombies.plus(senderPublicKey).map { Address.fromSerialized(it) }) } // Notify the user if (userLeft) { - //sender is a linked device val threadID = storage.getOrCreateThreadIdFor(Address.fromSerialized(groupID)) storage.insertOutgoingInfoMessage(context, groupID, SignalServiceGroup.Type.QUIT, name, members, admins, threadID, message.sentTimestamp!!) } else { @@ -553,9 +531,7 @@ private fun MessageReceiver.handleClosedGroupMemberLeft(message: ClosedGroupCont } } -private fun isValidGroupUpdate(group: GroupRecord, - sentTimestamp: Long, - senderPublicKey: String): Boolean { +private fun isValidGroupUpdate(group: GroupRecord, sentTimestamp: Long, senderPublicKey: String): Boolean { val oldMembers = group.members.map { it.serialize() } // Check that the message isn't from before the group was created if (group.formationTimestamp > sentTimestamp) { From a2c886468d4f225bf196f40ecedab3f5b569105d Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 13 May 2021 16:14:54 +1000 Subject: [PATCH 07/27] Clean --- .../loki/protocol/ClosedGroupsProtocolV2.kt | 2 +- .../sending_receiving/MessageDecrypter.kt | 8 +++ ...enderEncryption.kt => MessageEncrypter.kt} | 4 +- .../sending_receiving/MessageReceiver.kt | 1 - .../sending_receiving/MessageSender.kt | 68 +++++++------------ .../MessageSenderClosedGroupHandler.kt | 36 +++++----- .../ReceivedMessageHandler.kt | 2 +- 7 files changed, 54 insertions(+), 67 deletions(-) rename libsession/src/main/java/org/session/libsession/messaging/sending_receiving/{MessageSenderEncryption.kt => MessageEncrypter.kt} (93%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt index 5d181e5207..745e9067f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/protocol/ClosedGroupsProtocolV2.kt @@ -191,7 +191,7 @@ object ClosedGroupsProtocolV2 { } if (userPublicKey in admins) { // send current encryption key to the latest added members - val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() + val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull() ?: apiDB.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) if (encryptionKeyPair == null) { Log.d("Loki", "Couldn't get encryption key pair for closed group.") diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt index e7b09bc623..a3c81c9390 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageDecrypter.kt @@ -15,6 +15,14 @@ object MessageDecrypter { private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } + /** + * Decrypts `ciphertext` using the Session protocol and `x25519KeyPair`. + * + * @param ciphertext the data to decrypt. + * @param x25519KeyPair the key pair to use for decryption. This could be the current user's key pair, or the key pair of a closed group. + * + * @return the padded plaintext. + */ public fun decrypt(ciphertext: ByteArray, x25519KeyPair: ECKeyPair): Pair { val recipientX25519PrivateKey = x25519KeyPair.privateKey.serialize() val recipientX25519PublicKey = Hex.fromStringCondensed(x25519KeyPair.hexEncodedPublicKey.removing05PrefixIfNeeded()) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageEncrypter.kt similarity index 93% rename from libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt rename to libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageEncrypter.kt index c96fc82b16..287fa0cef4 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderEncryption.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageEncrypter.kt @@ -13,7 +13,7 @@ import org.session.libsignal.service.loki.utilities.removing05PrefixIfNeeded import org.session.libsignal.utilities.Hex import org.session.libsignal.utilities.logging.Log -object MessageSenderEncryption { +object MessageEncrypter { private val sodium by lazy { LazySodiumAndroid(SodiumAndroid()) } @@ -25,7 +25,7 @@ object MessageSenderEncryption { * * @return the encrypted message. */ - internal fun encryptWithSessionProtocol(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{ + internal fun encrypt(plaintext: ByteArray, recipientHexEncodedX25519PublicKey: String): ByteArray{ val context = MessagingModuleConfiguration.shared.context val userED25519KeyPair = KeyPairUtilities.getUserED25519KeyPair(context) ?: throw Error.NoUserED25519KeyPair val recipientX25519PublicKey = Hex.fromStringCondensed(recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded()) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt index 28f73521f2..6335bd92f8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageReceiver.kt @@ -21,7 +21,6 @@ object MessageReceiver { object SenderBlocked: Error("Received a message from a blocked user.") object NoThread: Error("Couldn't find thread for message.") object SelfSend: Error("Message addressed at self.") - // Shared sender keys object InvalidGroupPublicKey: Error("Invalid group public key.") object NoGroupKeyPair: Error("Missing group key pair.") diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index df7a0d7df8..4477633c16 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -14,7 +14,6 @@ import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.visible.* import org.session.libsession.messaging.open_groups.* import org.session.libsession.messaging.threads.Address -import org.session.libsession.messaging.threads.recipients.Recipient import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.snode.RawResponsePromise import org.session.libsession.snode.SnodeAPI @@ -27,6 +26,7 @@ import org.session.libsignal.service.internal.push.SignalServiceProtos import org.session.libsignal.service.loki.utilities.hexEncodedPublicKey import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.logging.Log +import java.lang.IllegalStateException import org.session.libsession.messaging.sending_receiving.attachments.Attachment as SignalAttachment import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview as SignalLinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel as SignalQuote @@ -37,8 +37,6 @@ object MessageSender { 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.") - object NoUserX25519KeyPair : Error("Couldn't find user X25519 key pair.") object NoUserED25519KeyPair : Error("Couldn't find user ED25519 key pair.") object SigningFailed : Error("Couldn't sign message.") object EncryptionFailed : Error("Couldn't encrypt message.") @@ -46,17 +44,10 @@ object MessageSender { // Closed groups object NoThread : Error("Couldn't find a thread associated with the given group public key.") object NoKeyPair: Error("Couldn't find a private key associated with the given group public key.") - object NoPrivateKey : Error("Couldn't find a private key associated with the given group public key.") object InvalidClosedGroupUpdate : Error("Invalid group update.") - // Precondition - class PreconditionFailure(val reason: String): Error(reason) - internal val isRetryable: Boolean = when (this) { - is InvalidMessage -> false - is ProtoConversionFailed -> false - is ProofOfWorkCalculationFailed -> false - is InvalidClosedGroupUpdate -> false + is InvalidMessage, ProtoConversionFailed, InvalidClosedGroupUpdate -> false else -> true } } @@ -76,7 +67,9 @@ object MessageSender { val storage = MessagingModuleConfiguration.shared.storage val userPublicKey = storage.getUserPublicKey() // Set the timestamp, sender and recipient - message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } /* Visible messages will already have their sent timestamp set */ + if (message.sentTimestamp == null) { + 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) @@ -91,8 +84,7 @@ object MessageSender { when (destination) { is Destination.Contact -> message.recipient = destination.publicKey is Destination.ClosedGroup -> message.recipient = destination.groupPublicKey - is Destination.OpenGroup, - is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!") + is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be an open group.") } // Validate the message if (!message.isValid()) { throw Error.InvalidMessage } @@ -125,13 +117,12 @@ object MessageSender { // Encrypt the serialized protobuf val ciphertext: ByteArray when (destination) { - is Destination.Contact -> ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, destination.publicKey) + is Destination.Contact -> ciphertext = MessageEncrypter.encrypt(plaintext, destination.publicKey) is Destination.ClosedGroup -> { val encryptionKeyPair = MessagingModuleConfiguration.shared.storage.getLatestClosedGroupEncryptionKeyPair(destination.groupPublicKey)!! - ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, encryptionKeyPair.hexEncodedPublicKey) + ciphertext = MessageEncrypter.encrypt(plaintext, encryptionKeyPair.hexEncodedPublicKey) } - is Destination.OpenGroup, - is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!") + is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.") } // Wrap the result val kind: SignalServiceProtos.Envelope.Type @@ -145,8 +136,7 @@ object MessageSender { kind = SignalServiceProtos.Envelope.Type.CLOSED_GROUP_MESSAGE senderPublicKey = destination.groupPublicKey } - is Destination.OpenGroup, - is Destination.OpenGroupV2 -> throw Error.PreconditionFailure("Destination should not be open groups!") + is Destination.OpenGroup, is Destination.OpenGroupV2 -> throw IllegalStateException("Destination should not be open group.") } val wrappedMessage = MessageWrapper.wrap(kind, message.sentTimestamp!!, senderPublicKey, ciphertext) // Send the result @@ -201,7 +191,9 @@ object MessageSender { private fun sendToOpenGroupDestination(destination: Destination, message: Message): Promise { val deferred = deferred() val storage = MessagingModuleConfiguration.shared.storage - message.sentTimestamp ?: run { message.sentTimestamp = System.currentTimeMillis() } + if (message.sentTimestamp == null) { + message.sentTimestamp = System.currentTimeMillis() + } message.sender = storage.getUserPublicKey() // Set the failure handler (need it here already for precondition failure handling) fun handleFailure(error: Exception) { @@ -210,18 +202,15 @@ object MessageSender { } try { when (destination) { - 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.Contact, is Destination.ClosedGroup -> throw IllegalStateException("Invalid destination.") is Destination.OpenGroup -> { message.recipient = "${destination.server}.${destination.channel}" val server = destination.server val channel = destination.channel - // Validate the message if (message !is VisibleMessage || !message.isValid()) { throw Error.InvalidMessage } - // Convert the message to an open group message val openGroupMessage = OpenGroupMessage.from(message, server) ?: run { throw Error.InvalidMessage @@ -239,7 +228,6 @@ object MessageSender { message.recipient = "${destination.server}.${destination.room}" val server = destination.server val room = destination.room - // Attach the user's profile if needed if (message is VisibleMessage) { val displayName = storage.getUserDisplayName()!! @@ -251,20 +239,17 @@ object MessageSender { message.profile = Profile(displayName) } } - // Validate the message if (message !is VisibleMessage || !message.isValid()) { throw Error.InvalidMessage } - val proto = message.toProto()!! val plaintext = PushTransportDetails.getPaddedMessageBody(proto.toByteArray()) val openGroupMessage = OpenGroupMessageV2( - sender = message.sender, - sentTimestamp = message.sentTimestamp!!, - base64EncodedData = Base64.encodeBytes(plaintext), + sender = message.sender, + sentTimestamp = message.sentTimestamp!!, + base64EncodedData = Base64.encodeBytes(plaintext), ) - OpenGroupAPIV2.send(openGroupMessage,room,server).success { message.openGroupServerMessageID = it.serverID handleSuccessfulMessageSend(message, destination) @@ -272,7 +257,6 @@ object MessageSender { }.fail { handleFailure(it) } - } } } catch (exception: Exception) { @@ -285,7 +269,7 @@ object MessageSender { fun handleSuccessfulMessageSend(message: Message, destination: Destination, isSyncMessage: Boolean = false) { val storage = MessagingModuleConfiguration.shared.storage val userPublicKey = storage.getUserPublicKey()!! - val messageId = storage.getMessageIdInDatabase(message.sentTimestamp!!, message.sender?:userPublicKey) ?: return + 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 @@ -293,7 +277,7 @@ object MessageSender { val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.room}".toByteArray()) val threadID = storage.getThreadIdFor(Address.fromSerialized(encoded)) if (threadID != null && threadID >= 0) { - storage.setOpenGroupServerMessageID(messageId, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage()) + storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage()) } } // Mark the message as sent @@ -323,16 +307,16 @@ object MessageSender { // Convenience @JvmStatic fun send(message: VisibleMessage, address: Address, attachments: List, quote: SignalQuote?, linkPreview: SignalLinkPreview?) { - val dataProvider = MessagingModuleConfiguration.shared.messageDataProvider - val attachmentIDs = dataProvider.getAttachmentIDsFor(message.id!!) + val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider + val attachmentIDs = messageDataProvider.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) + message.linkPreview?.let { linkPreview -> + if (linkPreview.attachmentID == null) { + messageDataProvider.getLinkPreviewAttachmentIDFor(message.id!!)?.let { attachmentID -> + message.linkPreview!!.attachmentID = attachmentID + message.attachmentIDs.remove(attachmentID) } } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt index a7c6dd97f7..f42127762c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSenderClosedGroupHandler.kt @@ -26,7 +26,8 @@ import java.util.* import java.util.concurrent.ConcurrentHashMap const val groupSizeLimit = 100 -val pendingKeyPair = ConcurrentHashMap>() + +val pendingKeyPairs = ConcurrentHashMap>() fun MessageSender.create(name: String, members: Collection): Promise { val deferred = deferred() @@ -45,7 +46,7 @@ fun MessageSender.create(name: String, members: Collection): Promise - val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey) + val ciphertext = MessageEncrypter.encrypt(plaintext, publicKey) ClosedGroupControlMessage.KeyPairWrapper(publicKey, ByteString.copyFrom(ciphertext)) } val kind = ClosedGroupControlMessage.Kind.EncryptionKeyPair(ByteString.copyFrom(Hex.fromStringCondensed(groupPublicKey)), wrappers) @@ -307,14 +303,14 @@ fun MessageSender.sendLatestEncryptionKeyPair(publicKey: String, groupPublicKey: return } // Get the latest encryption key pair - val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() - ?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return + val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull() + ?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) ?: return // Send it val proto = SignalServiceProtos.KeyPair.newBuilder() proto.publicKey = ByteString.copyFrom(encryptionKeyPair.publicKey.serialize().removing05PrefixIfNeeded()) proto.privateKey = ByteString.copyFrom(encryptionKeyPair.privateKey.serialize()) val plaintext = proto.build().toByteArray() - val ciphertext = MessageSenderEncryption.encryptWithSessionProtocol(plaintext, publicKey) + val ciphertext = MessageEncrypter.encrypt(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)) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index ea5cac4c77..6059ca44cd 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -401,7 +401,7 @@ private fun MessageReceiver.handleClosedGroupMembersAdded(message: ClosedGroupCo // // Without the code below, the added member(s) would never get the key pair that was generated by the admin when they saw // the member removed message. - val encryptionKeyPair = pendingKeyPair[groupPublicKey]?.orNull() + val encryptionKeyPair = pendingKeyPairs[groupPublicKey]?.orNull() ?: storage.getLatestClosedGroupEncryptionKeyPair(groupPublicKey) if (encryptionKeyPair == null) { android.util.Log.d("Loki", "Couldn't get encryption key pair for closed group.") From 1efd516eafe8bbdf6cc4e5ab4c83040e2b8e5f4c Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 08:56:08 +1000 Subject: [PATCH 08/27] Fix HTTP utility --- .../session/libsignal/service/loki/HTTP.kt | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/libsignal/src/main/java/org/session/libsignal/service/loki/HTTP.kt b/libsignal/src/main/java/org/session/libsignal/service/loki/HTTP.kt index 774e17cd8d..90a20d2282 100644 --- a/libsignal/src/main/java/org/session/libsignal/service/loki/HTTP.kt +++ b/libsignal/src/main/java/org/session/libsignal/service/loki/HTTP.kt @@ -39,6 +39,25 @@ object HTTP { .build() } + private fun getDefaultConnection(timeout: Long): OkHttpClient { + // Snode to snode communication uses self-signed certificates but clients can safely ignore this + val trustManager = object : X509TrustManager { + + override fun checkClientTrusted(chain: Array?, authorizationType: String?) { } + override fun checkServerTrusted(chain: Array?, authorizationType: String?) { } + override fun getAcceptedIssuers(): Array { return arrayOf() } + } + val sslContext = SSLContext.getInstance("SSL") + sslContext.init(null, arrayOf( trustManager ), SecureRandom()) + return OkHttpClient().newBuilder() + .sslSocketFactory(sslContext.socketFactory, trustManager) + .hostnameVerifier { _, _ -> true } + .connectTimeout(timeout, TimeUnit.SECONDS) + .readTimeout(timeout, TimeUnit.SECONDS) + .writeTimeout(timeout, TimeUnit.SECONDS) + .build() + } + private const val timeout: Long = 10 class HTTPRequestFailedException(val statusCode: Int, val json: Map<*, *>?) @@ -89,12 +108,7 @@ object HTTP { if (useSeedNodeConnection) { throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.") } - connection = OkHttpClient() - .newBuilder() - .connectTimeout(timeout, TimeUnit.SECONDS) - .readTimeout(timeout, TimeUnit.SECONDS) - .writeTimeout(timeout, TimeUnit.SECONDS) - .build() + connection = getDefaultConnection(timeout) } else { connection = if (useSeedNodeConnection) seedNodeConnection else defaultConnection } From 61c210837e3377b35397b7ee4155d493addaf284 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 08:56:25 +1000 Subject: [PATCH 09/27] Clean --- .../loki/activities/JoinPublicChatActivity.kt | 79 ++++++++----------- .../loki/viewmodel/DefaultGroupsViewModel.kt | 1 - .../fragment_enter_chat_url.xml | 5 +- .../main/res/layout/default_group_chip.xml | 3 +- .../res/layout/fragment_enter_chat_url.xml | 5 +- .../messaging/open_groups/OpenGroupAPIV2.kt | 2 +- .../pollers/OpenGroupV2Poller.kt | 2 +- 7 files changed, 45 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index f0490d379d..37b5055ff3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -11,7 +11,6 @@ import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.Toast -import androidx.activity.viewModels import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.view.isVisible import androidx.fragment.app.* @@ -42,9 +41,6 @@ import org.thoughtcrime.securesms.loki.viewmodel.DefaultGroupsViewModel import org.thoughtcrime.securesms.loki.viewmodel.State class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { - - private val viewModel by viewModels() - private val adapter = JoinPublicChatActivityAdapter(this) // region Lifecycle @@ -83,23 +79,18 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode } fun joinPublicChatIfPossible(url: String) { - // add http if just an IP style / host style URL is entered but leave it if scheme is included - val properString = if (!url.startsWith("http")) "http://$url" else url - val httpUrl = HttpUrl.parse(properString) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show() - - val room = httpUrl.pathSegments().firstOrNull() - val publicKey = httpUrl.queryParameter("public_key") + // Add "http" if not entered explicitly + val stringWithExplicitScheme = if (!url.startsWith("http")) "http://$url" else url + val url = HttpUrl.parse(stringWithExplicitScheme) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show() + val room = url.pathSegments().firstOrNull() + val publicKey = url.queryParameter("public_key") val isV2OpenGroup = !room.isNullOrEmpty() showLoader() - lifecycleScope.launch(Dispatchers.IO) { try { val (threadID, groupID) = if (isV2OpenGroup) { - val server = HttpUrl.Builder().scheme(httpUrl.scheme()).host(httpUrl.host()).apply { - if (httpUrl.port() != 80 || httpUrl.port() != 443) { - // non-standard port, add to server - this.port(httpUrl.port()) - } + val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).apply { + if (url.port() != 80 || url.port() != 443) { this.port(url.port()) } // Non-standard port; add to server }.build() val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, server.toString().removeSuffix("/"), room!!, publicKey!!) val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity) @@ -107,21 +98,19 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode threadID to groupID } else { val channel: Long = 1 - val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, properString, channel) + val group = OpenGroupUtilities.addGroup(this@JoinPublicChatActivity, stringWithExplicitScheme, channel) val threadID = GroupManager.getOpenGroupThreadID(group.id, this@JoinPublicChatActivity) val groupID = GroupUtil.getEncodedOpenGroupID(group.id.toByteArray()) threadID to groupID } MultiDeviceProtocol.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity) - withContext(Dispatchers.Main) { - // go to the new conversation and finish this one - openConversationActivity(this@JoinPublicChatActivity, threadID, Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false)) + val recipient = Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false) + openConversationActivity(this@JoinPublicChatActivity, threadID, recipient) finish() } - } catch (e: Exception) { - Log.e("JoinPublicChatActivity", "Fialed to join open group.", e) + Log.e("Loki", "Couldn't join open group.", e) withContext(Dispatchers.Main) { hideLoader() Toast.makeText(this@JoinPublicChatActivity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() @@ -175,19 +164,39 @@ private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity // region Enter Chat URL Fragment class EnterChatURLFragment : Fragment() { - private val viewModel by activityViewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return inflater.inflate(R.layout.fragment_enter_chat_url, container, false) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard + joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } + viewModel.defaultRooms.observe(viewLifecycleOwner) { state -> + defaultRoomsContainer.isVisible = state is State.Success + defaultRoomsLoader.isVisible = state is State.Loading + when (state) { + State.Loading -> { + // TODO: Show a loader + } + is State.Error -> { + // TODO: Hide the loader + } + is State.Success -> { + populateDefaultGroups(state.value) + } + } + } + } + private fun populateDefaultGroups(groups: List) { defaultRoomsGridLayout.removeAllViews() groups.forEach { defaultGroup -> val chip = layoutInflater.inflate(R.layout.default_group_chip,defaultRoomsGridLayout, false) as Chip val drawable = defaultGroup.image?.let { bytes -> - val bitmap = BitmapFactory.decodeByteArray(bytes,0,bytes.size) + val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size) RoundedBitmapDrawableFactory.create(resources,bitmap).apply { isCircular = true } @@ -200,32 +209,10 @@ class EnterChatURLFragment : Fragment() { defaultRoomsGridLayout.addView(chip) } if (groups.size and 1 != 0) { - // add a filler weight 1 view layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout) } } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard - joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } - viewModel.defaultRooms.observe(viewLifecycleOwner) { state -> - defaultRoomsParent.isVisible = state is State.Success - defaultRoomsLoader.isVisible = state is State.Loading - when (state) { - State.Loading -> { - // show a loader here probs - } - is State.Error -> { - // hide the loader and the - } - is State.Success -> { - populateDefaultGroups(state.value) - } - } - } - } - // region Convenience private fun joinPublicChatIfPossible() { val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt index a2d747ed9b..d4281a6cfb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/viewmodel/DefaultGroupsViewModel.kt @@ -20,5 +20,4 @@ class DefaultGroupsViewModel : ViewModel() { }.onStart { emit(State.Loading) }.asLiveData() - } \ No newline at end of file diff --git a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml index a4b088aac1..058ff98059 100644 --- a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml +++ b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml @@ -34,10 +34,11 @@ + + + - + + + , server: String): Promise, Exception> { + fun compactPoll(rooms: List, server: String): Promise, Exception> { val authTokenRequests = rooms.associateWith { room -> getAuthToken(room, server) } val storage = MessagingModuleConfiguration.shared.storage val requests = rooms.mapNotNull { room -> diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupV2Poller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupV2Poller.kt index 311e213592..7e40c9b2a6 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupV2Poller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupV2Poller.kt @@ -70,7 +70,7 @@ class OpenGroupV2Poller(private val openGroups: List, private val e isPollOngoing = true val server = openGroups.first().server // assume all the same server val rooms = openGroups.map { it.room } - return OpenGroupAPIV2.getCompactPoll(rooms = rooms, server).successBackground { results -> + return OpenGroupAPIV2.compactPoll(rooms = rooms, server).successBackground { results -> results.forEach { (room, results) -> val serverRoomId = "$server.$room" handleDeletedMessages(serverRoomId,results.deletions) From 133bcac17c19cab2945637640f77713d5013a87c Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 10:09:12 +1000 Subject: [PATCH 10/27] Make chips look better --- .../securesms/loki/activities/JoinPublicChatActivity.kt | 7 +++++-- .../main/res/layout-sw400dp/fragment_enter_chat_url.xml | 3 ++- app/src/main/res/layout/default_group_chip.xml | 8 +++++--- app/src/main/res/layout/fragment_enter_chat_url.xml | 3 ++- app/src/main/res/values-notnight-v21/colors.xml | 1 + app/src/main/res/values/colors.xml | 1 + 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index 37b5055ff3..bbe7bbe8e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -10,6 +10,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager +import android.widget.GridLayout import android.widget.Toast import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.view.isVisible @@ -193,8 +194,9 @@ class EnterChatURLFragment : Fragment() { private fun populateDefaultGroups(groups: List) { defaultRoomsGridLayout.removeAllViews() + defaultRoomsGridLayout.useDefaultMargins = false groups.forEach { defaultGroup -> - val chip = layoutInflater.inflate(R.layout.default_group_chip,defaultRoomsGridLayout, false) as Chip + val chip = layoutInflater.inflate(R.layout.default_group_chip, defaultRoomsGridLayout, false) as Chip val drawable = defaultGroup.image?.let { bytes -> val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size) RoundedBitmapDrawableFactory.create(resources,bitmap).apply { @@ -206,9 +208,10 @@ class EnterChatURLFragment : Fragment() { chip.setOnClickListener { (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL) } + defaultRoomsGridLayout.addView(chip) } - if (groups.size and 1 != 0) { + if ((groups.size and 1) != 0) { // This checks that the number of rooms is even layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout) } } diff --git a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml index 058ff98059..a011b2bf4b 100644 --- a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml +++ b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml @@ -33,7 +33,6 @@ @@ -50,6 +50,7 @@ diff --git a/app/src/main/res/layout/default_group_chip.xml b/app/src/main/res/layout/default_group_chip.xml index 78646ad11c..d34c896c09 100644 --- a/app/src/main/res/layout/default_group_chip.xml +++ b/app/src/main/res/layout/default_group_chip.xml @@ -5,11 +5,13 @@ xmlns:tools="http://schemas.android.com/tools" android:theme="@style/Theme.MaterialComponents.DayNight" style="?attr/chipStyle" - app:chipStartPadding="6dp" + app:chipStartPadding="4dp" + app:chipBackgroundColor="@color/open_group_chip_color" android:layout_columnWeight="1" - android:layout_marginHorizontal="2dp" tools:text="Main Group" android:ellipsize="end" tools:layout_width="wrap_content" + app:chipMinTouchTargetSize="0dp" + android:layout_margin="4dp" android:layout_width="0dp" - android:layout_height="52dp" /> \ No newline at end of file + android:layout_height="wrap_content" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_enter_chat_url.xml b/app/src/main/res/layout/fragment_enter_chat_url.xml index 0a01987af7..462c99e434 100644 --- a/app/src/main/res/layout/fragment_enter_chat_url.xml +++ b/app/src/main/res/layout/fragment_enter_chat_url.xml @@ -33,7 +33,6 @@ @@ -50,6 +50,7 @@ diff --git a/app/src/main/res/values-notnight-v21/colors.xml b/app/src/main/res/values-notnight-v21/colors.xml index 4d90ff3f87..36e4a1858d 100644 --- a/app/src/main/res/values-notnight-v21/colors.xml +++ b/app/src/main/res/values-notnight-v21/colors.xml @@ -22,6 +22,7 @@ #F5F5F5 #FCFCFC #F5F5F5 + #0D000000 #ffffff diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ecbcd77ec7..8771cdae4e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -31,6 +31,7 @@ #1B1B1B #212121 #FFCE3A + #0DFFFFFF #5ff8b0 From 88b4388e06ab38fef76daebe6e13ec299bf1ff8c Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 10:56:47 +1000 Subject: [PATCH 11/27] Clean up RetrieveProfileAvatarJob --- .../jobs/RetrieveProfileAvatarJob.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index a9c2025258..26e3280f98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.jobs; - import android.app.Application; import android.text.TextUtils; @@ -39,7 +38,7 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType private static final String TAG = RetrieveProfileAvatarJob.class.getSimpleName(); - private static final int MAX_PROFILE_SIZE_BYTES = 20 * 1024 * 1024; + private static final int MAX_PROFILE_SIZE_BYTES = 10 * 1024 * 1024; private static final String KEY_PROFILE_AVATAR = "profile_avatar"; private static final String KEY_ADDRESS = "address"; @@ -51,18 +50,17 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) { this(new Job.Parameters.Builder() - .setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize()) - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.HOURS.toMillis(1)) - .setMaxAttempts(3) - .build(), + .setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize()) + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.HOURS.toMillis(1)) + .setMaxAttempts(10) + .build(), recipient, profileAvatar); } private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) { super(parameters); - this.recipient = recipient; this.profileAvatar = profileAvatar; } @@ -70,9 +68,10 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType @Override public @NonNull Data serialize() { - return new Data.Builder().putString(KEY_PROFILE_AVATAR, profileAvatar) - .putString(KEY_ADDRESS, recipient.getAddress().serialize()) - .build(); + return new Data.Builder() + .putString(KEY_PROFILE_AVATAR, profileAvatar) + .putString(KEY_ADDRESS, recipient.getAddress().serialize()) + .build(); } @Override From ed9d1c747175cbf18fee02ace60dd2592b80832e Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 13:09:40 +1000 Subject: [PATCH 12/27] Cancel jobs when thread is deleted --- .../securesms/loki/activities/HomeActivity.kt | 2 ++ .../loki/database/SessionJobDatabase.kt | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index 3bc33c9746..ee5e22e857 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -332,6 +332,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), lifecycleScope.launch(Dispatchers.Main) { val context = this@HomeActivity as Context + DatabaseFactory.getSessionJobDatabase(this@HomeActivity).cancelPendingMessageSendJobs(threadID) + // Send a leave group message if this is an active closed group if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) { var isClosedGroup: Boolean diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt index 22c8d48f44..c5e5e102ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt @@ -71,6 +71,30 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa } } + fun cancelPendingMessageSendJobs(threadID: Long) { + val database = databaseHelper.writableDatabase + val attachmentUploadJobKeys = mutableListOf() + database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor -> + val job = jobFromCursor(cursor) as AttachmentUploadJob? + if (job != null && job.threadID == threadID.toString()) { attachmentUploadJobKeys.add(job.id!!) } + } + val messageSendJobKeys = mutableListOf() + database.getAll(sessionJobTable, "$jobType = ?", arrayOf( MessageSendJob.KEY )) { cursor -> + val job = jobFromCursor(cursor) as MessageSendJob? + if (job != null && job.message.threadID == threadID) { messageSendJobKeys.add(job.id!!) } + } + if (attachmentUploadJobKeys.isNotEmpty()) { + val attachmentUploadJobKeysAsString = attachmentUploadJobKeys.joinToString(", ") + database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)", + arrayOf( AttachmentUploadJob.KEY, attachmentUploadJobKeysAsString )) + } + if (messageSendJobKeys.isNotEmpty()) { + val messageSendJobKeysAsString = messageSendJobKeys.joinToString(", ") + database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)", + arrayOf( MessageSendJob.KEY, messageSendJobKeysAsString )) + } + } + fun isJobCanceled(job: Job): Boolean { val database = databaseHelper.readableDatabase var cursor: android.database.Cursor? = null From 5eed7a3cdd965cb3a1ad0a1189b652acc3ae3937 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 13:13:02 +1000 Subject: [PATCH 13/27] Clean --- .../securesms/loki/activities/HomeActivity.kt | 64 ++++++++----------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index ee5e22e857..9382bf83e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -24,7 +24,6 @@ import kotlinx.android.synthetic.main.activity_home.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import network.loki.messenger.R import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -315,25 +314,24 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), val threadID = thread.threadId val recipient = thread.recipient val threadDB = DatabaseFactory.getThreadDatabase(this) - val dialogMessage: String + val message: String if (recipient.isGroupRecipient) { val group = DatabaseFactory.getGroupDatabase(this).getGroup(recipient.address.toString()).orNull() if (group != null && group.admins.map { it.toString() }.contains(TextSecurePreferences.getLocalNumber(this))) { - dialogMessage = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." + message = "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." } else { - dialogMessage = resources.getString(R.string.activity_home_leave_group_dialog_message) + message = resources.getString(R.string.activity_home_leave_group_dialog_message) } } else { - dialogMessage = resources.getString(R.string.activity_home_delete_conversation_dialog_message) + message = resources.getString(R.string.activity_home_delete_conversation_dialog_message) } val dialog = AlertDialog.Builder(this) - dialog.setMessage(dialogMessage) + dialog.setMessage(message) dialog.setPositiveButton(R.string.yes) { _, _ -> lifecycleScope.launch(Dispatchers.Main) { val context = this@HomeActivity as Context - - DatabaseFactory.getSessionJobDatabase(this@HomeActivity).cancelPendingMessageSendJobs(threadID) - + // Cancel any outstanding jobs + DatabaseFactory.getSessionJobDatabase(context).cancelPendingMessageSendJobs(threadID) // Send a leave group message if this is an active closed group if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) { var isClosedGroup: Boolean @@ -352,34 +350,28 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), return@launch } } - - withContext(Dispatchers.IO) { - val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) - val openGroupV2 = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) - //TODO Move open group related logic to OpenGroupUtilities / PublicChatManager / GroupManager - if (publicChat != null) { - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - apiDB.removeLastMessageServerID(publicChat.channel, publicChat.server) - apiDB.removeLastDeletionServerID(publicChat.channel, publicChat.server) - apiDB.clearOpenGroupProfilePictureURL(publicChat.channel, publicChat.server) - - OpenGroupAPI.leave(publicChat.channel, publicChat.server) - - ApplicationContext.getInstance(context).publicChatManager - .removeChat(publicChat.server, publicChat.channel) - } else if (openGroupV2 != null) { - val apiDB = DatabaseFactory.getLokiAPIDatabase(context) - apiDB.removeLastMessageServerID(openGroupV2.room, openGroupV2.server) - apiDB.removeLastDeletionServerID(openGroupV2.room, openGroupV2.server) - - ApplicationContext.getInstance(context).publicChatManager - .removeChat(openGroupV2.server, openGroupV2.room) - } else { - threadDB.deleteConversation(threadID) - } - ApplicationContext.getInstance(context).messageNotifier.updateNotification(context) + // Delete the conversation + val v1OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID) + val v2OpenGroup = DatabaseFactory.getLokiThreadDatabase(context).getOpenGroupChat(threadID) + if (v1OpenGroup != null) { + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + apiDB.removeLastMessageServerID(v1OpenGroup.channel, v1OpenGroup.server) + apiDB.removeLastDeletionServerID(v1OpenGroup.channel, v1OpenGroup.server) + apiDB.clearOpenGroupProfilePictureURL(v1OpenGroup.channel, v1OpenGroup.server) + OpenGroupAPI.leave(v1OpenGroup.channel, v1OpenGroup.server) + ApplicationContext.getInstance(context).publicChatManager + .removeChat(v1OpenGroup.server, v1OpenGroup.channel) + } else if (v2OpenGroup != null) { + val apiDB = DatabaseFactory.getLokiAPIDatabase(context) + apiDB.removeLastMessageServerID(v2OpenGroup.room, v2OpenGroup.server) + apiDB.removeLastDeletionServerID(v2OpenGroup.room, v2OpenGroup.server) + ApplicationContext.getInstance(context).publicChatManager + .removeChat(v2OpenGroup.server, v2OpenGroup.room) + } else { + threadDB.deleteConversation(threadID) } - + // Update the badge count + ApplicationContext.getInstance(context).messageNotifier.updateNotification(context) // Notify the user val toastMessage = if (recipient.isGroupRecipient) R.string.MessageRecord_left_group else R.string.activity_home_conversation_deleted_message Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show() From 2b2756c5e75eba6f210f7eae00cf1c25d1698376 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 13:30:52 +1000 Subject: [PATCH 14/27] Update copy --- app/src/main/res/layout-sw400dp/activity_pn_mode.xml | 4 ++-- app/src/main/res/layout/activity_pn_mode.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout-sw400dp/activity_pn_mode.xml b/app/src/main/res/layout-sw400dp/activity_pn_mode.xml index e31e269a2e..fb1fc1c6ca 100644 --- a/app/src/main/res/layout-sw400dp/activity_pn_mode.xml +++ b/app/src/main/res/layout-sw400dp/activity_pn_mode.xml @@ -56,7 +56,7 @@ android:layout_marginTop="4dp" android:textSize="@dimen/very_small_font_size" android:textColor="@color/text" - android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." /> + android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers." /> + android:text="Session will occasionally check for new messages in the background." /> diff --git a/app/src/main/res/layout/activity_pn_mode.xml b/app/src/main/res/layout/activity_pn_mode.xml index 2e8f12b7f2..b86c0a3221 100644 --- a/app/src/main/res/layout/activity_pn_mode.xml +++ b/app/src/main/res/layout/activity_pn_mode.xml @@ -56,7 +56,7 @@ android:layout_marginTop="4dp" android:textSize="@dimen/very_small_font_size" android:textColor="@color/text" - android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." /> + android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers." /> + android:text="Session will occasionally check for new messages in the background." /> From c9157d33c4a50763f7dee9a88e3997c3d7206e38 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 13:31:26 +1000 Subject: [PATCH 15/27] Update copy --- app/src/main/res/xml/preferences_notifications.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/xml/preferences_notifications.xml b/app/src/main/res/xml/preferences_notifications.xml index a1b33553ac..76fff3f6d6 100644 --- a/app/src/main/res/xml/preferences_notifications.xml +++ b/app/src/main/res/xml/preferences_notifications.xml @@ -27,7 +27,7 @@ android:dependency="pref_key_enable_notifications" android:key="pref_key_use_fcm" android:title="Use Fast Mode" - android:summary="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." + android:summary="You’ll be notified of new messages reliably and immediately using Google’s notification servers." android:defaultValue="false" /> From 43c5fce526473bf8d1563c8833ec5a354b8adf37 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 10:09:12 +1000 Subject: [PATCH 16/27] Make chips look better --- .../securesms/loki/activities/JoinPublicChatActivity.kt | 8 +++++--- .../main/res/layout-sw400dp/fragment_enter_chat_url.xml | 6 ++++++ app/src/main/res/layout/default_group_chip.xml | 8 +++++--- app/src/main/res/layout/fragment_enter_chat_url.xml | 6 ++++++ app/src/main/res/values-notnight-v21/colors.xml | 1 + app/src/main/res/values/colors.xml | 1 + 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index f0490d379d..e7c018bc48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -10,6 +10,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager +import android.widget.GridLayout import android.widget.Toast import androidx.activity.viewModels import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory @@ -184,8 +185,9 @@ class EnterChatURLFragment : Fragment() { private fun populateDefaultGroups(groups: List) { defaultRoomsGridLayout.removeAllViews() + defaultRoomsGridLayout.useDefaultMargins = false groups.forEach { defaultGroup -> - val chip = layoutInflater.inflate(R.layout.default_group_chip,defaultRoomsGridLayout, false) as Chip + val chip = layoutInflater.inflate(R.layout.default_group_chip, defaultRoomsGridLayout, false) as Chip val drawable = defaultGroup.image?.let { bytes -> val bitmap = BitmapFactory.decodeByteArray(bytes,0,bytes.size) RoundedBitmapDrawableFactory.create(resources,bitmap).apply { @@ -197,10 +199,10 @@ class EnterChatURLFragment : Fragment() { chip.setOnClickListener { (requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL) } + defaultRoomsGridLayout.addView(chip) } - if (groups.size and 1 != 0) { - // add a filler weight 1 view + if ((groups.size and 1) != 0) { // This checks that the number of rooms is even layoutInflater.inflate(R.layout.grid_layout_filler, defaultRoomsGridLayout) } } diff --git a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml index a4b088aac1..db470a8785 100644 --- a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml +++ b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml @@ -33,8 +33,12 @@ >>>>>> 133bcac17 (Make chips look better) android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> @@ -42,12 +46,14 @@ android:layout_marginVertical="16dp" android:textSize="18sp" android:textStyle="bold" + android:paddingHorizontal="24dp" android:text="@string/activity_join_public_chat_join_rooms" android:layout_width="match_parent" android:layout_height="wrap_content"/> diff --git a/app/src/main/res/layout/default_group_chip.xml b/app/src/main/res/layout/default_group_chip.xml index 844898e885..e0a410ca6b 100644 --- a/app/src/main/res/layout/default_group_chip.xml +++ b/app/src/main/res/layout/default_group_chip.xml @@ -4,11 +4,13 @@ xmlns:tools="http://schemas.android.com/tools" android:theme="@style/Theme.MaterialComponents.DayNight" style="?attr/chipStyle" - app:chipStartPadding="6dp" + app:chipStartPadding="4dp" + app:chipBackgroundColor="@color/open_group_chip_color" android:layout_columnWeight="1" - android:layout_marginHorizontal="2dp" tools:text="Main Group" android:ellipsize="end" tools:layout_width="wrap_content" + app:chipMinTouchTargetSize="0dp" + android:layout_margin="4dp" android:layout_width="0dp" - android:layout_height="52dp" /> \ No newline at end of file + android:layout_height="wrap_content" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_enter_chat_url.xml b/app/src/main/res/layout/fragment_enter_chat_url.xml index 7affed157e..27dd369d71 100644 --- a/app/src/main/res/layout/fragment_enter_chat_url.xml +++ b/app/src/main/res/layout/fragment_enter_chat_url.xml @@ -33,8 +33,12 @@ >>>>>> 133bcac17 (Make chips look better) android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> @@ -42,12 +46,14 @@ android:layout_marginVertical="16dp" android:textSize="18sp" android:textStyle="bold" + android:paddingHorizontal="24dp" android:text="@string/activity_join_public_chat_join_rooms" android:layout_width="match_parent" android:layout_height="wrap_content"/> diff --git a/app/src/main/res/values-notnight-v21/colors.xml b/app/src/main/res/values-notnight-v21/colors.xml index 4d90ff3f87..36e4a1858d 100644 --- a/app/src/main/res/values-notnight-v21/colors.xml +++ b/app/src/main/res/values-notnight-v21/colors.xml @@ -22,6 +22,7 @@ #F5F5F5 #FCFCFC #F5F5F5 + #0D000000 #ffffff diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ecbcd77ec7..8771cdae4e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -31,6 +31,7 @@ #1B1B1B #212121 #FFCE3A + #0DFFFFFF #5ff8b0 From 446ff908bacbbedb8258a983d4aa5218fb1da136 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 10:56:47 +1000 Subject: [PATCH 17/27] Clean up RetrieveProfileAvatarJob --- .../jobs/RetrieveProfileAvatarJob.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java index a9c2025258..26e3280f98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileAvatarJob.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.jobs; - import android.app.Application; import android.text.TextUtils; @@ -39,7 +38,7 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType private static final String TAG = RetrieveProfileAvatarJob.class.getSimpleName(); - private static final int MAX_PROFILE_SIZE_BYTES = 20 * 1024 * 1024; + private static final int MAX_PROFILE_SIZE_BYTES = 10 * 1024 * 1024; private static final String KEY_PROFILE_AVATAR = "profile_avatar"; private static final String KEY_ADDRESS = "address"; @@ -51,18 +50,17 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) { this(new Job.Parameters.Builder() - .setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize()) - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.HOURS.toMillis(1)) - .setMaxAttempts(3) - .build(), + .setQueue("RetrieveProfileAvatarJob" + recipient.getAddress().serialize()) + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.HOURS.toMillis(1)) + .setMaxAttempts(10) + .build(), recipient, profileAvatar); } private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar) { super(parameters); - this.recipient = recipient; this.profileAvatar = profileAvatar; } @@ -70,9 +68,10 @@ public class RetrieveProfileAvatarJob extends BaseJob implements InjectableType @Override public @NonNull Data serialize() { - return new Data.Builder().putString(KEY_PROFILE_AVATAR, profileAvatar) - .putString(KEY_ADDRESS, recipient.getAddress().serialize()) - .build(); + return new Data.Builder() + .putString(KEY_PROFILE_AVATAR, profileAvatar) + .putString(KEY_ADDRESS, recipient.getAddress().serialize()) + .build(); } @Override From 72540aa7873ae407b2d9ea394e8eec8f41c80c53 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 13:09:40 +1000 Subject: [PATCH 18/27] Cancel jobs when thread is deleted --- .../securesms/loki/activities/HomeActivity.kt | 2 ++ .../loki/database/SessionJobDatabase.kt | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index 3bc33c9746..ee5e22e857 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -332,6 +332,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), lifecycleScope.launch(Dispatchers.Main) { val context = this@HomeActivity as Context + DatabaseFactory.getSessionJobDatabase(this@HomeActivity).cancelPendingMessageSendJobs(threadID) + // Send a leave group message if this is an active closed group if (recipient.address.isClosedGroup && DatabaseFactory.getGroupDatabase(context).isActive(recipient.address.toGroupString())) { var isClosedGroup: Boolean diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt index 22c8d48f44..c5e5e102ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/database/SessionJobDatabase.kt @@ -71,6 +71,30 @@ class SessionJobDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa } } + fun cancelPendingMessageSendJobs(threadID: Long) { + val database = databaseHelper.writableDatabase + val attachmentUploadJobKeys = mutableListOf() + database.getAll(sessionJobTable, "$jobType = ?", arrayOf( AttachmentUploadJob.KEY )) { cursor -> + val job = jobFromCursor(cursor) as AttachmentUploadJob? + if (job != null && job.threadID == threadID.toString()) { attachmentUploadJobKeys.add(job.id!!) } + } + val messageSendJobKeys = mutableListOf() + database.getAll(sessionJobTable, "$jobType = ?", arrayOf( MessageSendJob.KEY )) { cursor -> + val job = jobFromCursor(cursor) as MessageSendJob? + if (job != null && job.message.threadID == threadID) { messageSendJobKeys.add(job.id!!) } + } + if (attachmentUploadJobKeys.isNotEmpty()) { + val attachmentUploadJobKeysAsString = attachmentUploadJobKeys.joinToString(", ") + database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)", + arrayOf( AttachmentUploadJob.KEY, attachmentUploadJobKeysAsString )) + } + if (messageSendJobKeys.isNotEmpty()) { + val messageSendJobKeysAsString = messageSendJobKeys.joinToString(", ") + database.delete(sessionJobTable, "${Companion.jobType} = ? AND ${Companion.jobID} IN (?)", + arrayOf( MessageSendJob.KEY, messageSendJobKeysAsString )) + } + } + fun isJobCanceled(job: Job): Boolean { val database = databaseHelper.readableDatabase var cursor: android.database.Cursor? = null From 816e44a7ecb036c26190c0466df8c45a5dc6c166 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 13:30:52 +1000 Subject: [PATCH 19/27] Update copy --- app/src/main/res/layout-sw400dp/activity_pn_mode.xml | 4 ++-- app/src/main/res/layout/activity_pn_mode.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout-sw400dp/activity_pn_mode.xml b/app/src/main/res/layout-sw400dp/activity_pn_mode.xml index e31e269a2e..fb1fc1c6ca 100644 --- a/app/src/main/res/layout-sw400dp/activity_pn_mode.xml +++ b/app/src/main/res/layout-sw400dp/activity_pn_mode.xml @@ -56,7 +56,7 @@ android:layout_marginTop="4dp" android:textSize="@dimen/very_small_font_size" android:textColor="@color/text" - android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." /> + android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers." /> + android:text="Session will occasionally check for new messages in the background." /> diff --git a/app/src/main/res/layout/activity_pn_mode.xml b/app/src/main/res/layout/activity_pn_mode.xml index 2e8f12b7f2..b86c0a3221 100644 --- a/app/src/main/res/layout/activity_pn_mode.xml +++ b/app/src/main/res/layout/activity_pn_mode.xml @@ -56,7 +56,7 @@ android:layout_marginTop="4dp" android:textSize="@dimen/very_small_font_size" android:textColor="@color/text" - android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." /> + android:text="You’ll be notified of new messages reliably and immediately using Google’s notification servers." /> + android:text="Session will occasionally check for new messages in the background." /> From 9699126ac98e5b7e67f0a7bdb7820c8fbce6edd1 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 13:31:26 +1000 Subject: [PATCH 20/27] Update copy --- app/src/main/res/xml/preferences_notifications.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/xml/preferences_notifications.xml b/app/src/main/res/xml/preferences_notifications.xml index a1b33553ac..76fff3f6d6 100644 --- a/app/src/main/res/xml/preferences_notifications.xml +++ b/app/src/main/res/xml/preferences_notifications.xml @@ -27,7 +27,7 @@ android:dependency="pref_key_enable_notifications" android:key="pref_key_use_fcm" android:title="Use Fast Mode" - android:summary="You’ll be notified of new messages reliably and immediately using Google’s notification servers. The contents of your messages, and who you’re messaging, are never exposed to Google." + android:summary="You’ll be notified of new messages reliably and immediately using Google’s notification servers." android:defaultValue="false" /> From 6e5f75d472525f27dd4c8604f068a138482932a2 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 13:53:41 +1000 Subject: [PATCH 21/27] Fix build --- .../securesms/loki/activities/JoinPublicChatActivity.kt | 2 +- app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml | 5 ----- app/src/main/res/layout/fragment_enter_chat_url.xml | 5 ----- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt index e7c018bc48..39838e2c77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/JoinPublicChatActivity.kt @@ -212,7 +212,7 @@ class EnterChatURLFragment : Fragment() { chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() } viewModel.defaultRooms.observe(viewLifecycleOwner) { state -> - defaultRoomsParent.isVisible = state is State.Success + defaultRoomsContainer.isVisible = state is State.Success defaultRoomsLoader.isVisible = state is State.Loading when (state) { State.Loading -> { diff --git a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml index db470a8785..a1f71e8e2f 100644 --- a/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml +++ b/app/src/main/res/layout-sw400dp/fragment_enter_chat_url.xml @@ -33,12 +33,7 @@ >>>>>> 133bcac17 (Make chips look better) android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> diff --git a/app/src/main/res/layout/fragment_enter_chat_url.xml b/app/src/main/res/layout/fragment_enter_chat_url.xml index 27dd369d71..f1047f5a8e 100644 --- a/app/src/main/res/layout/fragment_enter_chat_url.xml +++ b/app/src/main/res/layout/fragment_enter_chat_url.xml @@ -33,12 +33,7 @@ >>>>>> 133bcac17 (Make chips look better) android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> From faa8c9443fae091136182ef7d955a79cfeff53f4 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Fri, 14 May 2021 15:17:16 +1000 Subject: [PATCH 22/27] Hardcode community guidelines --- app/src/main/AndroidManifest.xml | 4 ++ .../conversation/ConversationActivity.java | 5 ++ .../activities/OpenGroupGuidelinesActivity.kt | 37 ++++++++++ .../loki/views/OpenGroupGuidelinesView.kt | 38 ++++++++++ .../layout/activity_open_group_guidelines.xml | 36 ++++++++++ .../main/res/layout/conversation_activity.xml | 6 ++ .../res/layout/view_open_group_guidelines.xml | 72 +++++++++++++++++++ 7 files changed, 198 insertions(+) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/loki/activities/OpenGroupGuidelinesActivity.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/loki/views/OpenGroupGuidelinesView.kt create mode 100644 app/src/main/res/layout/activity_open_group_guidelines.xml create mode 100644 app/src/main/res/layout/view_open_group_guidelines.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2d8473b849..972ec30be1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -254,6 +254,10 @@ android:name="android.support.PARENT_ACTIVITY" android:value="org.thoughtcrime.securesms.loki.activities.HomeActivity" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conversation_activity.xml b/app/src/main/res/layout/conversation_activity.xml index 0b9ff3d077..ba9f7f27e1 100644 --- a/app/src/main/res/layout/conversation_activity.xml +++ b/app/src/main/res/layout/conversation_activity.xml @@ -134,6 +134,12 @@ android:background="?android:dividerHorizontal" android:elevation="1dp" /> + + + + + + + + + + + + + + + + + + +