moving the media encrypting to the crypto module and exposing as part of the service
This commit is contained in:
parent
c97e402c1a
commit
065eeef5a0
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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("=", "")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -25,4 +25,4 @@ fun interface MessageEncrypter {
|
|||
|
||||
internal object MissingMessageEncrypter : MessageEncrypter {
|
||||
override suspend fun encrypt(message: MessageEncrypter.ClearMessagePayload) = throw IllegalStateException("No encrypter instance set")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
Loading…
Reference in New Issue