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
parent f6ef073689
commit 9d6e72303a
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.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(

View File

@ -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<SharedRoomKey>)
@ -38,6 +40,18 @@ interface Crypto {
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 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
}

View File

@ -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)
}

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.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<String>,
val kty: String,
val k: String,
val iv: String,
val hashes: Map<String, String>,
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("=", "")
}
}

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 {
override suspend fun encrypt(message: MessageEncrypter.ClearMessagePayload) = throw IllegalStateException("No encrypter instance set")
}
}

View File

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

View File

@ -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

View File

@ -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")
}
}
}

View File

@ -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,

View File

@ -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)
}

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.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(),