moving the media encrypting to the crypto module and exposing as part of the service

This commit is contained in:
Adam Brown 2022-09-21 21:24:45 +01:00 committed by Adam Brown
parent c97e402c1a
commit 065eeef5a0
12 changed files with 168 additions and 85 deletions

View File

@ -32,11 +32,8 @@ import app.dapk.st.matrix.crypto.installCryptoService
import app.dapk.st.matrix.device.deviceService import app.dapk.st.matrix.device.deviceService
import app.dapk.st.matrix.device.installEncryptionService import app.dapk.st.matrix.device.installEncryptionService
import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
import app.dapk.st.matrix.message.MessageEncrypter import app.dapk.st.matrix.message.*
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.message.installMessageService
import app.dapk.st.matrix.message.internal.ImageContentReader 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.installPushService
import app.dapk.st.matrix.push.pushService import app.dapk.st.matrix.push.pushService
import app.dapk.st.matrix.room.* import app.dapk.st.matrix.room.*
@ -272,23 +269,46 @@ internal class MatrixModules(
coroutineDispatchers = coroutineDispatchers, coroutineDispatchers = coroutineDispatchers,
) )
val imageContentReader = AndroidImageContentReader(contentResolver) val imageContentReader = AndroidImageContentReader(contentResolver)
installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler()), imageContentReader, base64) { serviceProvider -> installMessageService(
MessageEncrypter { message -> store.localEchoStore,
val result = serviceProvider.cryptoService().encrypt( BackgroundWorkAdapter(workModule.workScheduler()),
roomId = message.roomId, imageContentReader,
credentials = credentialsStore.credentials()!!, messageEncrypter = {
messageJson = message.contents, val cryptoService = it.cryptoService()
) MessageEncrypter { message ->
val result = cryptoService.encrypt(
roomId = message.roomId,
credentials = credentialsStore.credentials()!!,
messageJson = message.contents,
)
MessageEncrypter.EncryptedMessagePayload( MessageEncrypter.EncryptedMessagePayload(
result.algorithmName, result.algorithmName,
result.senderKey, result.senderKey,
result.cipherText, result.cipherText,
result.sessionId, result.sessionId,
result.deviceId, 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() val overviewStore = store.overviewStore()
installRoomService( installRoomService(

View File

@ -11,10 +11,12 @@ import app.dapk.st.matrix.crypto.internal.*
import app.dapk.st.matrix.device.deviceService import app.dapk.st.matrix.device.deviceService
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.io.InputStream import java.io.InputStream
import java.net.URI
private val SERVICE_KEY = CryptoService::class private val SERVICE_KEY = CryptoService::class
interface CryptoService : MatrixService { interface CryptoService : MatrixService {
suspend fun encrypt(input: InputStream): Crypto.MediaEncryptionResult
suspend fun encrypt(roomId: RoomId, credentials: DeviceCredentials, messageJson: JsonString): Crypto.EncryptionResult suspend fun encrypt(roomId: RoomId, credentials: DeviceCredentials, messageJson: JsonString): Crypto.EncryptionResult
suspend fun decrypt(encryptedPayload: EncryptedMessageContent): DecryptionResult suspend fun decrypt(encryptedPayload: EncryptedMessageContent): DecryptionResult
suspend fun importRoomKeys(keys: List<SharedRoomKey>) suspend fun importRoomKeys(keys: List<SharedRoomKey>)
@ -38,6 +40,18 @@ interface Crypto {
val deviceId: DeviceId val deviceId: DeviceId
) )
data class MediaEncryptionResult(
val uri: URI,
val algorithm: String,
val ext: Boolean,
val keyOperations: List<String>,
val kty: String,
val k: String,
val iv: String,
val hashes: Map<String, String>,
val v: String,
)
} }
@ -151,7 +165,9 @@ fun MatrixServiceInstaller.installCryptoService(
) )
val verificationHandler = VerificationHandler(deviceService, credentialsStore, logger, JsonCanonicalizer(), olm) val verificationHandler = VerificationHandler(deviceService, credentialsStore, logger, JsonCanonicalizer(), olm)
val roomKeyImporter = RoomKeyImporter(base64, coroutineDispatchers) 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 { data class Error(val cause: Type) : ImportResult {
sealed interface Type { sealed interface Type {
data class Unknown(val cause: Throwable): Type data class Unknown(val cause: Throwable) : Type
object NoKeysFound: Type object NoKeysFound : Type
object UnexpectedDecryptionOutput: Type object UnexpectedDecryptionOutput : Type
object UnableToOpenFile: Type object UnableToOpenFile : Type
} }
} }
data class Update(val importedKeysCount: Long) : ImportResult data class Update(val importedKeysCount: Long) : ImportResult
} }

View File

@ -13,8 +13,14 @@ internal class DefaultCryptoService(
private val olmCrypto: OlmCrypto, private val olmCrypto: OlmCrypto,
private val verificationHandler: VerificationHandler, private val verificationHandler: VerificationHandler,
private val roomKeyImporter: RoomKeyImporter, private val roomKeyImporter: RoomKeyImporter,
private val mediaEncrypter: MediaEncrypter,
private val logger: MatrixLogger, private val logger: MatrixLogger,
) : CryptoService { ) : 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 { override suspend fun encrypt(roomId: RoomId, credentials: DeviceCredentials, messageJson: JsonString): Crypto.EncryptionResult {
return olmCrypto.encryptMessage(roomId, credentials, messageJson) return olmCrypto.encryptMessage(roomId, credentials, messageJson)
} }

View File

@ -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.core.Base64
import app.dapk.st.matrix.crypto.Crypto
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import java.util.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
@ -16,7 +18,7 @@ private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
class MediaEncrypter(private val base64: Base64) { class MediaEncrypter(private val base64: Base64) {
fun encrypt(input: InputStream, name: String): Result { fun encrypt(input: InputStream): Crypto.MediaEncryptionResult {
val secureRandom = SecureRandom() val secureRandom = SecureRandom()
val initVectorBytes = ByteArray(16) { 0.toByte() } val initVectorBytes = ByteArray(16) { 0.toByte() }
@ -30,10 +32,9 @@ class MediaEncrypter(private val base64: Base64) {
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) 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() outputFile.outputStream().use { s ->
outputStream.use { s ->
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes) val ivParameterSpec = IvParameterSpec(initVectorBytes)
@ -60,8 +61,8 @@ class MediaEncrypter(private val base64: Base64) {
s.write(encodedBytes) s.write(encodedBytes)
} }
return Result( return Crypto.MediaEncryptionResult(
contents = outputFile.readBytes(), uri = outputFile.toURI(),
algorithm = "A256CTR", algorithm = "A256CTR",
ext = true, ext = true,
keyOperations = listOf("encrypt", "decrypt"), keyOperations = listOf("encrypt", "decrypt"),
@ -72,19 +73,6 @@ class MediaEncrypter(private val base64: Base64) {
v = "v2" v = "v2"
) )
} }
data class Result(
val contents: ByteArray,
val algorithm: String,
val ext: Boolean,
val keyOperations: List<String>,
val kty: String,
val k: String,
val iv: String,
val hashes: Map<String, String>,
val v: String,
)
} }
private fun base64ToBase64Url(base64: String): String { private fun base64ToBase64Url(base64: String): String {
@ -97,4 +85,4 @@ private fun base64ToBase64Url(base64: String): String {
private fun base64ToUnpaddedBase64(base64: String): String { private fun base64ToUnpaddedBase64(base64: String): String {
return base64.replace("\n".toRegex(), "") return base64.replace("\n".toRegex(), "")
.replace("=", "") .replace("=", "")
} }

View File

@ -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<String>,
val kty: String,
val k: String,
val iv: String,
val hashes: Map<String, String>,
val v: String,
) {
fun openStream() = File(uri).outputStream()
}
}
internal object MissingMediaEncrypter : MediaEncrypter {
override suspend fun encrypt(input: InputStream) = throw IllegalStateException("No encrypter instance set")
}

View File

@ -25,4 +25,4 @@ fun interface MessageEncrypter {
internal object MissingMessageEncrypter : MessageEncrypter { internal object MissingMessageEncrypter : MessageEncrypter {
override suspend fun encrypt(message: MessageEncrypter.ClearMessagePayload) = throw IllegalStateException("No encrypter instance set") override suspend fun encrypt(message: MessageEncrypter.ClearMessagePayload) = throw IllegalStateException("No encrypter instance set")
} }

View File

@ -130,16 +130,16 @@ fun MatrixServiceInstaller.installMessageService(
localEchoStore: LocalEchoStore, localEchoStore: LocalEchoStore,
backgroundScheduler: BackgroundScheduler, backgroundScheduler: BackgroundScheduler,
imageContentReader: ImageContentReader, imageContentReader: ImageContentReader,
base64: Base64,
messageEncrypter: ServiceDepFactory<MessageEncrypter> = ServiceDepFactory { MissingMessageEncrypter }, messageEncrypter: ServiceDepFactory<MessageEncrypter> = ServiceDepFactory { MissingMessageEncrypter },
mediaEncrypter: ServiceDepFactory<MediaEncrypter> = ServiceDepFactory { MissingMediaEncrypter },
) { ) {
this.install { (httpClient, _, installedServices) -> this.install { (httpClient, _, installedServices) ->
SERVICE_KEY to DefaultMessageService( SERVICE_KEY to DefaultMessageService(
httpClient, httpClient,
localEchoStore, localEchoStore,
backgroundScheduler, backgroundScheduler,
base64,
messageEncrypter.create(installedServices), messageEncrypter.create(installedServices),
mediaEncrypter.create(installedServices),
imageContentReader imageContentReader
) )
} }

View File

@ -1,13 +1,9 @@
package app.dapk.st.matrix.message.internal package app.dapk.st.matrix.message.internal
import app.dapk.st.core.Base64
import app.dapk.st.matrix.MatrixTaskRunner import app.dapk.st.matrix.MatrixTaskRunner
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.http.MatrixHttpClient
import app.dapk.st.matrix.message.BackgroundScheduler import app.dapk.st.matrix.message.*
import app.dapk.st.matrix.message.LocalEchoStore
import app.dapk.st.matrix.message.MessageEncrypter
import app.dapk.st.matrix.message.MessageService
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.net.SocketException import java.net.SocketException
@ -20,12 +16,12 @@ internal class DefaultMessageService(
httpClient: MatrixHttpClient, httpClient: MatrixHttpClient,
private val localEchoStore: LocalEchoStore, private val localEchoStore: LocalEchoStore,
private val backgroundScheduler: BackgroundScheduler, private val backgroundScheduler: BackgroundScheduler,
base64: Base64,
messageEncrypter: MessageEncrypter, messageEncrypter: MessageEncrypter,
mediaEncrypter: MediaEncrypter,
imageContentReader: ImageContentReader, imageContentReader: ImageContentReader,
) : MessageService, MatrixTaskRunner { ) : MessageService, MatrixTaskRunner {
private val sendMessageUseCase = SendMessageUseCase(httpClient, messageEncrypter, imageContentReader, base64) private val sendMessageUseCase = SendMessageUseCase(httpClient, messageEncrypter, mediaEncrypter, imageContentReader)
private val sendEventMessageUseCase = SendEventMessageUseCase(httpClient) 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 override suspend fun canRun(task: MatrixTaskRunner.MatrixTask) = task.type == MATRIX_MESSAGE_TASK_TYPE || task.type == MATRIX_IMAGE_MESSAGE_TASK_TYPE

View File

@ -1,5 +1,7 @@
package app.dapk.st.matrix.message.internal package app.dapk.st.matrix.message.internal
import java.io.InputStream
interface ImageContentReader { interface ImageContentReader {
fun read(uri: String): ImageContent fun read(uri: String): ImageContent
@ -32,5 +34,9 @@ interface ImageContentReader {
result = 31 * result + content.contentHashCode() result = 31 * result + content.contentHashCode()
return result return result
} }
fun stream(): InputStream {
TODO("Not yet implemented")
}
} }
} }

View File

@ -1,19 +1,19 @@
package app.dapk.st.matrix.message.internal package app.dapk.st.matrix.message.internal
import app.dapk.st.core.Base64
import app.dapk.st.matrix.common.* import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.http.MatrixHttpClient
import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest
import app.dapk.st.matrix.message.ApiSendResponse 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.MessageEncrypter
import app.dapk.st.matrix.message.MessageService.Message import app.dapk.st.matrix.message.MessageService.Message
import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream
internal class SendMessageUseCase( internal class SendMessageUseCase(
private val httpClient: MatrixHttpClient, private val httpClient: MatrixHttpClient,
private val messageEncrypter: MessageEncrypter, private val messageEncrypter: MessageEncrypter,
private val mediaEncrypter: MediaEncrypter,
private val imageContentReader: ImageContentReader, private val imageContentReader: ImageContentReader,
private val base64: Base64,
) { ) {
private val mapper = ApiMessageMapper() private val mapper = ApiMessageMapper()
@ -63,12 +63,12 @@ internal class SendMessageUseCase(
return when (message.sendEncrypted) { return when (message.sendEncrypted) {
true -> { true -> {
val result = MediaEncrypter(base64).encrypt( val result = mediaEncrypter.encrypt(imageContent.stream())
ByteArrayInputStream(imageContent.content), val bytes = ByteArrayOutputStream().also {
imageContent.fileName, 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( val content = ApiMessage.ImageMessage.ImageContent(
url = null, url = null,

View File

@ -75,7 +75,7 @@ class SmokeTest {
@Order(6) @Order(6)
fun `can send and receive clear image messages`() = testAfterInitialSync { alice, bob -> fun `can send and receive clear image messages`() = testAfterInitialSync { alice, bob ->
val testImage = loadResourceFile("test-image.png") 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) bob.expectImageMessage(SharedState.sharedRoom, testImage, SharedState.alice.roomMember)
} }

View File

@ -17,11 +17,8 @@ import app.dapk.st.matrix.crypto.installCryptoService
import app.dapk.st.matrix.device.deviceService import app.dapk.st.matrix.device.deviceService
import app.dapk.st.matrix.device.installEncryptionService import app.dapk.st.matrix.device.installEncryptionService
import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
import app.dapk.st.matrix.message.MessageEncrypter import app.dapk.st.matrix.message.*
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.message.installMessageService
import app.dapk.st.matrix.message.internal.ImageContentReader 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.installPushService
import app.dapk.st.matrix.room.RoomMessenger import app.dapk.st.matrix.room.RoomMessenger
import app.dapk.st.matrix.room.installRoomService import app.dapk.st.matrix.room.installRoomService
@ -121,23 +118,46 @@ class TestMatrix(
coroutineDispatchers = coroutineDispatchers, coroutineDispatchers = coroutineDispatchers,
) )
installMessageService(storeModule.localEchoStore, InstantScheduler(it), JavaImageContentReader(), base64) { serviceProvider -> installMessageService(
MessageEncrypter { message -> localEchoStore = storeModule.localEchoStore,
val result = serviceProvider.cryptoService().encrypt( backgroundScheduler = InstantScheduler(it),
roomId = message.roomId, imageContentReader = JavaImageContentReader(),
credentials = storeModule.credentialsStore().credentials()!!, messageEncrypter = {
messageJson = message.contents, val cryptoService = it.cryptoService()
) MessageEncrypter { message ->
val result = cryptoService.encrypt(
roomId = message.roomId,
credentials = storeModule.credentialsStore().credentials()!!,
messageJson = message.contents,
)
MessageEncrypter.EncryptedMessagePayload( MessageEncrypter.EncryptedMessagePayload(
result.algorithmName, result.algorithmName,
result.senderKey, result.senderKey,
result.cipherText, result.cipherText,
result.sessionId, result.sessionId,
result.deviceId, 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( installRoomService(
storeModule.memberStore(), storeModule.memberStore(),