From 065eeef5a0703f27d28c238acdd09f993ce705ee Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 21 Sep 2022 21:24:45 +0100 Subject: [PATCH] moving the media encrypting to the crypto module and exposing as part of the service --- .../kotlin/app/dapk/st/graph/AppModule.kt | 60 ++++++++++++------- .../dapk/st/matrix/crypto/CryptoService.kt | 27 +++++++-- .../crypto/internal/DefaultCryptoService.kt | 6 ++ .../matrix/crypto}/internal/MediaEncrypter.kt | 30 +++------- .../dapk/st/matrix/message/MediaEncrypter.kt | 30 ++++++++++ .../st/matrix/message/MessageEncrypter.kt | 2 +- .../dapk/st/matrix/message/MessageService.kt | 4 +- .../message/internal/DefaultMessageService.kt | 10 +--- .../message/internal/ImageContentReader.kt | 6 ++ .../message/internal/SendMessageUseCase.kt | 16 ++--- test-harness/src/test/kotlin/SmokeTest.kt | 2 +- .../src/test/kotlin/test/TestMatrix.kt | 60 ++++++++++++------- 12 files changed, 168 insertions(+), 85 deletions(-) rename matrix/services/{message/src/main/kotlin/app/dapk/st/matrix/message => crypto/src/main/kotlin/app/dapk/st/matrix/crypto}/internal/MediaEncrypter.kt (81%) create mode 100644 matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index d80f132..d16eb64 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -32,11 +32,8 @@ import app.dapk.st.matrix.crypto.installCryptoService import app.dapk.st.matrix.device.deviceService import app.dapk.st.matrix.device.installEncryptionService import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory -import app.dapk.st.matrix.message.MessageEncrypter -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.message.installMessageService +import app.dapk.st.matrix.message.* import app.dapk.st.matrix.message.internal.ImageContentReader -import app.dapk.st.matrix.message.messageService import app.dapk.st.matrix.push.installPushService import app.dapk.st.matrix.push.pushService import app.dapk.st.matrix.room.* @@ -272,23 +269,46 @@ internal class MatrixModules( coroutineDispatchers = coroutineDispatchers, ) val imageContentReader = AndroidImageContentReader(contentResolver) - installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler()), imageContentReader, base64) { serviceProvider -> - MessageEncrypter { message -> - val result = serviceProvider.cryptoService().encrypt( - roomId = message.roomId, - credentials = credentialsStore.credentials()!!, - messageJson = message.contents, - ) + installMessageService( + store.localEchoStore, + BackgroundWorkAdapter(workModule.workScheduler()), + imageContentReader, + messageEncrypter = { + val cryptoService = it.cryptoService() + MessageEncrypter { message -> + val result = cryptoService.encrypt( + roomId = message.roomId, + credentials = credentialsStore.credentials()!!, + messageJson = message.contents, + ) - MessageEncrypter.EncryptedMessagePayload( - result.algorithmName, - result.senderKey, - result.cipherText, - result.sessionId, - result.deviceId, - ) - } - } + MessageEncrypter.EncryptedMessagePayload( + result.algorithmName, + result.senderKey, + result.cipherText, + result.sessionId, + result.deviceId, + ) + } + }, + mediaEncrypter = { + val cryptoService = it.cryptoService() + MediaEncrypter { input -> + val result = cryptoService.encrypt(input) + MediaEncrypter.Result( + uri = result.uri, + algorithm = result.algorithm, + ext = result.ext, + keyOperations = result.keyOperations, + kty = result.kty, + k = result.k, + iv = result.iv, + hashes = result.hashes, + v = result.v, + ) + } + }, + ) val overviewStore = store.overviewStore() installRoomService( diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt index adb85c9..405a718 100644 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt +++ b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt @@ -11,10 +11,12 @@ import app.dapk.st.matrix.crypto.internal.* import app.dapk.st.matrix.device.deviceService import kotlinx.coroutines.flow.Flow import java.io.InputStream +import java.net.URI private val SERVICE_KEY = CryptoService::class interface CryptoService : MatrixService { + suspend fun encrypt(input: InputStream): Crypto.MediaEncryptionResult suspend fun encrypt(roomId: RoomId, credentials: DeviceCredentials, messageJson: JsonString): Crypto.EncryptionResult suspend fun decrypt(encryptedPayload: EncryptedMessageContent): DecryptionResult suspend fun importRoomKeys(keys: List) @@ -38,6 +40,18 @@ interface Crypto { val deviceId: DeviceId ) + data class MediaEncryptionResult( + val uri: URI, + val algorithm: String, + val ext: Boolean, + val keyOperations: List, + val kty: String, + val k: String, + val iv: String, + val hashes: Map, + val v: String, + ) + } @@ -151,7 +165,9 @@ fun MatrixServiceInstaller.installCryptoService( ) val verificationHandler = VerificationHandler(deviceService, credentialsStore, logger, JsonCanonicalizer(), olm) val roomKeyImporter = RoomKeyImporter(base64, coroutineDispatchers) - SERVICE_KEY to DefaultCryptoService(olmCrypto, verificationHandler, roomKeyImporter, logger) + val mediaEncrypter = MediaEncrypter(base64) + + SERVICE_KEY to DefaultCryptoService(olmCrypto, verificationHandler, roomKeyImporter, mediaEncrypter, logger) } } @@ -166,12 +182,13 @@ sealed interface ImportResult { data class Error(val cause: Type) : ImportResult { sealed interface Type { - data class Unknown(val cause: Throwable): Type - object NoKeysFound: Type - object UnexpectedDecryptionOutput: Type - object UnableToOpenFile: Type + data class Unknown(val cause: Throwable) : Type + object NoKeysFound : Type + object UnexpectedDecryptionOutput : Type + object UnableToOpenFile : Type } } + data class Update(val importedKeysCount: Long) : ImportResult } diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt index f5a64fe..c23cebc 100644 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt +++ b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/DefaultCryptoService.kt @@ -13,8 +13,14 @@ internal class DefaultCryptoService( private val olmCrypto: OlmCrypto, private val verificationHandler: VerificationHandler, private val roomKeyImporter: RoomKeyImporter, + private val mediaEncrypter: MediaEncrypter, private val logger: MatrixLogger, ) : CryptoService { + + override suspend fun encrypt(input: InputStream): Crypto.MediaEncryptionResult { + return mediaEncrypter.encrypt(input) + } + override suspend fun encrypt(roomId: RoomId, credentials: DeviceCredentials, messageJson: JsonString): Crypto.EncryptionResult { return olmCrypto.encryptMessage(roomId, credentials, messageJson) } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/MediaEncrypter.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MediaEncrypter.kt similarity index 81% rename from matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/MediaEncrypter.kt rename to matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MediaEncrypter.kt index 181ec2d..17ca412 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/MediaEncrypter.kt +++ b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MediaEncrypter.kt @@ -1,10 +1,12 @@ -package app.dapk.st.matrix.message.internal +package app.dapk.st.matrix.crypto.internal import app.dapk.st.core.Base64 +import app.dapk.st.matrix.crypto.Crypto import java.io.File import java.io.InputStream import java.security.MessageDigest import java.security.SecureRandom +import java.util.* import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec @@ -16,7 +18,7 @@ private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" class MediaEncrypter(private val base64: Base64) { - fun encrypt(input: InputStream, name: String): Result { + fun encrypt(input: InputStream): Crypto.MediaEncryptionResult { val secureRandom = SecureRandom() val initVectorBytes = ByteArray(16) { 0.toByte() } @@ -30,10 +32,9 @@ class MediaEncrypter(private val base64: Base64) { val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) - val outputFile = File.createTempFile("_encrypt-${name.hashCode()}", ".png") + val outputFile = File.createTempFile("_encrypt-${UUID.randomUUID()}", ".png") - val outputStream = outputFile.outputStream() - outputStream.use { s -> + outputFile.outputStream().use { s -> val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val ivParameterSpec = IvParameterSpec(initVectorBytes) @@ -60,8 +61,8 @@ class MediaEncrypter(private val base64: Base64) { s.write(encodedBytes) } - return Result( - contents = outputFile.readBytes(), + return Crypto.MediaEncryptionResult( + uri = outputFile.toURI(), algorithm = "A256CTR", ext = true, keyOperations = listOf("encrypt", "decrypt"), @@ -72,19 +73,6 @@ class MediaEncrypter(private val base64: Base64) { v = "v2" ) } - - data class Result( - val contents: ByteArray, - val algorithm: String, - val ext: Boolean, - val keyOperations: List, - val kty: String, - val k: String, - val iv: String, - val hashes: Map, - val v: String, - ) - } private fun base64ToBase64Url(base64: String): String { @@ -97,4 +85,4 @@ private fun base64ToBase64Url(base64: String): String { private fun base64ToUnpaddedBase64(base64: String): String { return base64.replace("\n".toRegex(), "") .replace("=", "") -} +} \ No newline at end of file diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt new file mode 100644 index 0000000..2f6711a --- /dev/null +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt @@ -0,0 +1,30 @@ +package app.dapk.st.matrix.message + +import java.io.File +import java.io.InputStream +import java.net.URI + +fun interface MediaEncrypter { + + suspend fun encrypt(input: InputStream): Result + + data class Result( + val uri: URI, + val algorithm: String, + val ext: Boolean, + val keyOperations: List, + val kty: String, + val k: String, + val iv: String, + val hashes: Map, + val v: String, + ) { + + fun openStream() = File(uri).outputStream() + } + +} + +internal object MissingMediaEncrypter : MediaEncrypter { + override suspend fun encrypt(input: InputStream) = throw IllegalStateException("No encrypter instance set") +} diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt index 13b67c2..d2387e8 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageEncrypter.kt @@ -25,4 +25,4 @@ fun interface MessageEncrypter { internal object MissingMessageEncrypter : MessageEncrypter { override suspend fun encrypt(message: MessageEncrypter.ClearMessagePayload) = throw IllegalStateException("No encrypter instance set") -} \ No newline at end of file +} diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt index 1fb003a..23fdb98 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt @@ -130,16 +130,16 @@ fun MatrixServiceInstaller.installMessageService( localEchoStore: LocalEchoStore, backgroundScheduler: BackgroundScheduler, imageContentReader: ImageContentReader, - base64: Base64, messageEncrypter: ServiceDepFactory = ServiceDepFactory { MissingMessageEncrypter }, + mediaEncrypter: ServiceDepFactory = ServiceDepFactory { MissingMediaEncrypter }, ) { this.install { (httpClient, _, installedServices) -> SERVICE_KEY to DefaultMessageService( httpClient, localEchoStore, backgroundScheduler, - base64, messageEncrypter.create(installedServices), + mediaEncrypter.create(installedServices), imageContentReader ) } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt index c51dc37..c6b7374 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt @@ -1,13 +1,9 @@ package app.dapk.st.matrix.message.internal -import app.dapk.st.core.Base64 import app.dapk.st.matrix.MatrixTaskRunner import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.http.MatrixHttpClient -import app.dapk.st.matrix.message.BackgroundScheduler -import app.dapk.st.matrix.message.LocalEchoStore -import app.dapk.st.matrix.message.MessageEncrypter -import app.dapk.st.matrix.message.MessageService +import app.dapk.st.matrix.message.* import kotlinx.coroutines.flow.Flow import kotlinx.serialization.json.Json import java.net.SocketException @@ -20,12 +16,12 @@ internal class DefaultMessageService( httpClient: MatrixHttpClient, private val localEchoStore: LocalEchoStore, private val backgroundScheduler: BackgroundScheduler, - base64: Base64, messageEncrypter: MessageEncrypter, + mediaEncrypter: MediaEncrypter, imageContentReader: ImageContentReader, ) : MessageService, MatrixTaskRunner { - private val sendMessageUseCase = SendMessageUseCase(httpClient, messageEncrypter, imageContentReader, base64) + private val sendMessageUseCase = SendMessageUseCase(httpClient, messageEncrypter, mediaEncrypter, imageContentReader) private val sendEventMessageUseCase = SendEventMessageUseCase(httpClient) override suspend fun canRun(task: MatrixTaskRunner.MatrixTask) = task.type == MATRIX_MESSAGE_TASK_TYPE || task.type == MATRIX_IMAGE_MESSAGE_TASK_TYPE diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt index 8395092..d4ff899 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt @@ -1,5 +1,7 @@ package app.dapk.st.matrix.message.internal +import java.io.InputStream + interface ImageContentReader { fun read(uri: String): ImageContent @@ -32,5 +34,9 @@ interface ImageContentReader { result = 31 * result + content.contentHashCode() return result } + + fun stream(): InputStream { + TODO("Not yet implemented") + } } } \ No newline at end of file diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt index d3799e1..16d8678 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt @@ -1,19 +1,19 @@ package app.dapk.st.matrix.message.internal -import app.dapk.st.core.Base64 import app.dapk.st.matrix.common.* import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest import app.dapk.st.matrix.message.ApiSendResponse +import app.dapk.st.matrix.message.MediaEncrypter import app.dapk.st.matrix.message.MessageEncrypter import app.dapk.st.matrix.message.MessageService.Message -import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream internal class SendMessageUseCase( private val httpClient: MatrixHttpClient, private val messageEncrypter: MessageEncrypter, + private val mediaEncrypter: MediaEncrypter, private val imageContentReader: ImageContentReader, - private val base64: Base64, ) { private val mapper = ApiMessageMapper() @@ -63,12 +63,12 @@ internal class SendMessageUseCase( return when (message.sendEncrypted) { true -> { - val result = MediaEncrypter(base64).encrypt( - ByteArrayInputStream(imageContent.content), - imageContent.fileName, - ) + val result = mediaEncrypter.encrypt(imageContent.stream()) + val bytes = ByteArrayOutputStream().also { + it.writeTo(result.openStream()) + }.toByteArray() - val uri = httpClient.execute(uploadRequest(result.contents, imageContent.fileName, "application/octet-stream")).contentUri + val uri = httpClient.execute(uploadRequest(bytes, imageContent.fileName, "application/octet-stream")).contentUri val content = ApiMessage.ImageMessage.ImageContent( url = null, diff --git a/test-harness/src/test/kotlin/SmokeTest.kt b/test-harness/src/test/kotlin/SmokeTest.kt index b610072..d692c96 100644 --- a/test-harness/src/test/kotlin/SmokeTest.kt +++ b/test-harness/src/test/kotlin/SmokeTest.kt @@ -75,7 +75,7 @@ class SmokeTest { @Order(6) fun `can send and receive clear image messages`() = testAfterInitialSync { alice, bob -> val testImage = loadResourceFile("test-image.png") - alice.sendImageMessage(SharedState.sharedRoom, testImage, isEncrypted = true) + alice.sendImageMessage(SharedState.sharedRoom, testImage, isEncrypted = false) bob.expectImageMessage(SharedState.sharedRoom, testImage, SharedState.alice.roomMember) } diff --git a/test-harness/src/test/kotlin/test/TestMatrix.kt b/test-harness/src/test/kotlin/test/TestMatrix.kt index 3da7355..797547c 100644 --- a/test-harness/src/test/kotlin/test/TestMatrix.kt +++ b/test-harness/src/test/kotlin/test/TestMatrix.kt @@ -17,11 +17,8 @@ import app.dapk.st.matrix.crypto.installCryptoService import app.dapk.st.matrix.device.deviceService import app.dapk.st.matrix.device.installEncryptionService import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory -import app.dapk.st.matrix.message.MessageEncrypter -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.message.installMessageService +import app.dapk.st.matrix.message.* import app.dapk.st.matrix.message.internal.ImageContentReader -import app.dapk.st.matrix.message.messageService import app.dapk.st.matrix.push.installPushService import app.dapk.st.matrix.room.RoomMessenger import app.dapk.st.matrix.room.installRoomService @@ -121,23 +118,46 @@ class TestMatrix( coroutineDispatchers = coroutineDispatchers, ) - installMessageService(storeModule.localEchoStore, InstantScheduler(it), JavaImageContentReader(), base64) { serviceProvider -> - MessageEncrypter { message -> - val result = serviceProvider.cryptoService().encrypt( - roomId = message.roomId, - credentials = storeModule.credentialsStore().credentials()!!, - messageJson = message.contents, - ) + installMessageService( + localEchoStore = storeModule.localEchoStore, + backgroundScheduler = InstantScheduler(it), + imageContentReader = JavaImageContentReader(), + messageEncrypter = { + val cryptoService = it.cryptoService() + MessageEncrypter { message -> + val result = cryptoService.encrypt( + roomId = message.roomId, + credentials = storeModule.credentialsStore().credentials()!!, + messageJson = message.contents, + ) - MessageEncrypter.EncryptedMessagePayload( - result.algorithmName, - result.senderKey, - result.cipherText, - result.sessionId, - result.deviceId, - ) - } - } + MessageEncrypter.EncryptedMessagePayload( + result.algorithmName, + result.senderKey, + result.cipherText, + result.sessionId, + result.deviceId, + ) + } + }, + mediaEncrypter = { + val cryptoService = it.cryptoService() + MediaEncrypter { input -> + val result = cryptoService.encrypt(input) + MediaEncrypter.Result( + uri = result.uri, + algorithm = result.algorithm, + ext = result.ext, + keyOperations = result.keyOperations, + kty = result.kty, + k = result.k, + iv = result.iv, + hashes = result.hashes, + v = result.v, + ) + } + }, + ) installRoomService( storeModule.memberStore(),