Merge pull request #155 from ouchadam/feature/encrypted-images

Encrypted images
This commit is contained in:
Adam Brown 2022-09-21 23:36:03 +01:00 committed by GitHub
commit a78b99fe50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 672 additions and 272 deletions

View File

@ -8,6 +8,7 @@ import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns
import app.dapk.db.DapkDb
import app.dapk.st.BuildConfig
import app.dapk.st.SharedPreferencesDelegate
@ -31,14 +32,9 @@ import app.dapk.st.matrix.crypto.cryptoService
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.device.internal.ApiMessage
import app.dapk.st.matrix.http.MatrixHttpClient
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.*
@ -64,6 +60,7 @@ import app.dapk.st.work.TaskRunnerModule
import app.dapk.st.work.WorkModule
import com.squareup.sqldelight.android.AndroidSqliteDriver
import kotlinx.coroutines.Dispatchers
import java.io.InputStream
import java.time.Clock
internal class AppModule(context: Application, logger: MatrixLogger) {
@ -80,6 +77,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
private val database = DapkDb(driver)
private val clock = Clock.systemUTC()
val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
val base64 = AndroidBase64()
val storeModule = unsafeLazy {
StoreModule(
@ -94,7 +92,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
private val workModule = WorkModule(context)
private val imageLoaderModule = ImageLoaderModule(context)
private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, context.contentResolver, buildMeta)
private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, context.contentResolver, base64, buildMeta)
val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, workModule, storeModule, context, coroutineDispatchers)
val coreAndroidModule = CoreAndroidModule(
@ -139,6 +137,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
deviceMeta,
coroutineDispatchers,
clock,
base64,
)
}
@ -154,6 +153,7 @@ internal class FeatureModules internal constructor(
deviceMeta: DeviceMeta,
coroutineDispatchers: CoroutineDispatchers,
clock: Clock,
base64: Base64,
) {
val directoryModule by unsafeLazy {
@ -181,7 +181,9 @@ internal class FeatureModules internal constructor(
matrixModules.room,
storeModule.value.credentialsStore(),
storeModule.value.roomStore(),
clock
clock,
context,
base64,
)
}
val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, matrixModules.sync, buildMeta) }
@ -232,6 +234,7 @@ internal class MatrixModules(
private val logger: MatrixLogger,
private val coroutineDispatchers: CoroutineDispatchers,
private val contentResolver: ContentResolver,
private val base64: Base64,
private val buildMeta: BuildMeta,
) {
@ -249,7 +252,6 @@ internal class MatrixModules(
installAuthService(credentialsStore)
installEncryptionService(store.knownDevicesStore())
val base64 = AndroidBase64()
val olmAccountStore = OlmPersistenceWrapper(store.olmStore(), base64)
val singletonFlows = SingletonFlows(coroutineDispatchers)
val olm = OlmWrapper(
@ -274,40 +276,47 @@ internal class MatrixModules(
coroutineDispatchers = coroutineDispatchers,
)
val imageContentReader = AndroidImageContentReader(contentResolver)
installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler()), imageContentReader) { serviceProvider ->
MessageEncrypter { message ->
val result = serviceProvider.cryptoService().encrypt(
roomId = when (message) {
is MessageService.Message.TextMessage -> message.roomId
is MessageService.Message.ImageMessage -> message.roomId
},
credentials = credentialsStore.credentials()!!,
when (message) {
is MessageService.Message.TextMessage -> JsonString(
MatrixHttpClient.jsonWithDefaults.encodeToString(
ApiMessage.TextMessage.serializer(),
ApiMessage.TextMessage(
ApiMessage.TextMessage.TextContent(
message.content.body,
message.content.type,
), message.roomId, type = EventType.ROOM_MESSAGE.value
)
)
)
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,
)
is MessageService.Message.ImageMessage -> TODO()
}
)
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,
contentLength = result.contentLength,
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(
@ -475,23 +484,27 @@ internal class DomainModules(
}
internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader {
override fun read(uri: String): ImageContentReader.ImageContent {
override fun meta(uri: String): ImageContentReader.ImageContent {
val androidUri = Uri.parse(uri)
val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeStream(fileStream, null, options)
return contentResolver.openInputStream(androidUri)?.use { stream ->
val output = stream.readBytes()
ImageContentReader.ImageContent(
height = options.outHeight,
width = options.outWidth,
size = output.size.toLong(),
mimeType = options.outMimeType,
fileName = androidUri.lastPathSegment ?: "file",
content = output
)
val fileSize = contentResolver.query(androidUri, null, null, null, null)?.use { cursor ->
cursor.moveToFirst()
val columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.getLong(columnIndex)
} ?: throw IllegalArgumentException("Could not process $uri")
return ImageContentReader.ImageContent(
height = options.outHeight,
width = options.outWidth,
size = fileSize,
mimeType = options.outMimeType,
fileName = androidUri.lastPathSegment ?: "file",
)
}
override fun inputStream(uri: String): InputStream = contentResolver.openInputStream(Uri.parse(uri))!!
}

View File

@ -3,3 +3,7 @@ plugins { id 'kotlin' }
dependencies {
implementation project(':core')
}
task generateReleaseSources {}
task compileReleaseSources {}

View File

@ -4,6 +4,7 @@ apply plugin: 'kotlin-parcelize'
dependencies {
implementation project(":matrix:services:sync")
implementation project(":matrix:services:message")
implementation project(":matrix:services:crypto")
implementation project(":matrix:services:room")
implementation project(":domains:android:compose-core")
implementation project(":domains:android:viewmodel")

View File

@ -1,7 +1,8 @@
package app.dapk.st.messenger
import android.content.Context
import android.util.Base64
import app.dapk.st.core.Base64
import app.dapk.st.matrix.crypto.MediaDecrypter
import app.dapk.st.matrix.sync.RoomEvent
import coil.ImageLoader
import coil.decode.DataSource
@ -14,25 +15,23 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.Buffer
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
private const val CRYPTO_BUFFER_SIZE = 32 * 1024
private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
class DecryptingFetcherFactory(private val context: Context, base64: Base64) : Fetcher.Factory<RoomEvent.Image> {
private val mediaDecrypter = MediaDecrypter(base64)
class DecryptingFetcherFactory(private val context: Context) : Fetcher.Factory<RoomEvent.Image> {
override fun create(data: RoomEvent.Image, options: Options, imageLoader: ImageLoader): Fetcher {
return DecryptingFetcher(data, context)
return DecryptingFetcher(data, context, mediaDecrypter)
}
}
private val http = OkHttpClient()
class DecryptingFetcher(private val data: RoomEvent.Image, private val context: Context) : Fetcher {
class DecryptingFetcher(
private val data: RoomEvent.Image,
private val context: Context,
private val mediaDecrypter: MediaDecrypter,
) : Fetcher {
override suspend fun fetch(): FetchResult {
val response = http.newCall(Request.Builder().url(data.imageMeta.url).build()).execute()
@ -44,32 +43,11 @@ class DecryptingFetcher(private val data: RoomEvent.Image, private val context:
}
private fun handleEncrypted(response: Response, keys: RoomEvent.Image.ImageMeta.Keys): Buffer {
val key = Base64.decode(keys.k.replace('-', '+').replace('_', '/'), Base64.DEFAULT)
val initVectorBytes = Base64.decode(keys.iv, Base64.DEFAULT)
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
var read: Int
val d = ByteArray(CRYPTO_BUFFER_SIZE)
var decodedBytes: ByteArray
val outputStream = Buffer()
response.body?.let {
it.byteStream().use {
read = it.read(d)
while (read != -1) {
messageDigest.update(d, 0, read)
decodedBytes = decryptCipher.update(d, 0, read)
outputStream.write(decodedBytes)
read = it.read(d)
}
return response.body?.byteStream()?.let { byteStream ->
Buffer().also { buffer ->
mediaDecrypter.decrypt(byteStream, keys.k, keys.iv).collect { buffer.write(it) }
}
}
return outputStream
} ?: Buffer()
}
}
}

View File

@ -5,19 +5,25 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import app.dapk.st.core.*
import app.dapk.st.design.components.SmallTalkTheme
import app.dapk.st.core.DapkActivity
import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.core.module
import app.dapk.st.core.viewModel
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.navigator.MessageAttachment
import kotlinx.parcelize.Parcelize
val LocalDecyptingFetcherFactory = staticCompositionLocalOf<DecryptingFetcherFactory> { throw IllegalAccessError() }
class MessengerActivity : DapkActivity() {
private val viewModel by viewModel { module<MessengerModule>().messengerViewModel() }
private val module by unsafeLazy { module<MessengerModule>() }
private val viewModel by viewModel { module.messengerViewModel() }
companion object {
@ -44,11 +50,13 @@ class MessengerActivity : DapkActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val payload = readPayload<MessagerActivityPayload>()
log(AppLogTag.ERROR_NON_FATAL, payload)
val factory = module.decryptingFetcherFactory()
setContent {
Surface(Modifier.fillMaxSize()) {
Surface(Modifier.fillMaxSize()) {
CompositionLocalProvider(LocalDecyptingFetcherFactory provides factory) {
MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator)
}
}
}
}
}

View File

@ -1,5 +1,7 @@
package app.dapk.st.messenger
import android.content.Context
import app.dapk.st.core.Base64
import app.dapk.st.core.ProvidableModule
import app.dapk.st.matrix.common.CredentialsStore
import app.dapk.st.matrix.message.MessageService
@ -15,6 +17,8 @@ class MessengerModule(
private val credentialsStore: CredentialsStore,
private val roomStore: RoomStore,
private val clock: Clock,
private val context: Context,
private val base64: Base64,
) : ProvidableModule {
internal fun messengerViewModel(): MessengerViewModel {
@ -25,4 +29,6 @@ class MessengerModule(
val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(LocalEchoMapper(MetaMapper()))
return TimelineUseCaseImpl(syncService, messageService, roomService, mergeWithLocalEchosUseCase)
}
internal fun decryptingFetcherFactory() = DecryptingFetcherFactory(context, base64)
}

View File

@ -228,7 +228,6 @@ private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
@Composable
private fun MessageImage(content: BubbleContent<RoomEvent.Image>) {
val context = LocalContext.current
val fetcherFactory = remember { DecryptingFetcherFactory(context) }
Box(modifier = Modifier.padding(start = 6.dp)) {
Box(
@ -258,7 +257,7 @@ private fun MessageImage(content: BubbleContent<RoomEvent.Image>) {
modifier = Modifier.size(content.message.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)),
painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context)
.fetcherFactory(fetcherFactory)
.fetcherFactory(LocalDecyptingFetcherFactory.current)
.data(content.message)
.build()
),
@ -407,7 +406,6 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
.defaultMinSize(minWidth = 50.dp)
) {
val context = LocalContext.current
val fetcherFactory = remember { DecryptingFetcherFactory(context) }
Column(
Modifier
.background(if (content.isNotSelf) SmallTalkTheme.extendedColors.otherBubbleReplyBackground else SmallTalkTheme.extendedColors.selfBubbleReplyBackground)
@ -438,7 +436,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
modifier = Modifier.size(replyingTo.imageMeta.scale(LocalDensity.current, LocalConfiguration.current)),
painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context)
.fetcherFactory(fetcherFactory)
.fetcherFactory(LocalDecyptingFetcherFactory.current)
.data(replyingTo)
.build()
),
@ -481,7 +479,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context)
.data(content.message)
.fetcherFactory(fetcherFactory)
.fetcherFactory(LocalDecyptingFetcherFactory.current)
.build()
),
contentDescription = null,

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,19 @@ interface Crypto {
val deviceId: DeviceId
)
data class MediaEncryptionResult(
val uri: URI,
val contentLength: Long,
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 +166,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 +183,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

@ -0,0 +1,50 @@
package app.dapk.st.matrix.crypto
import app.dapk.st.core.Base64
import java.io.InputStream
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
private const val CRYPTO_BUFFER_SIZE = 32 * 1024
private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
class MediaDecrypter(private val base64: Base64) {
fun decrypt(input: InputStream, k: String, iv: String): Collector {
val key = base64.decode(k.replace('-', '+').replace('_', '/'))
val initVectorBytes = base64.decode(iv)
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
var read: Int
val d = ByteArray(CRYPTO_BUFFER_SIZE)
var decodedBytes: ByteArray
return Collector { partial ->
input.use {
read = it.read(d)
while (read != -1) {
messageDigest.update(d, 0, read)
decodedBytes = decryptCipher.update(d, 0, read)
partial(decodedBytes)
read = it.read(d)
}
}
}
}
}
fun interface Collector {
fun collect(partial: (ByteArray) -> Unit)
}

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

@ -0,0 +1,89 @@
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
private const val CRYPTO_BUFFER_SIZE = 32 * 1024
private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
class MediaEncrypter(private val base64: Base64) {
fun encrypt(input: InputStream): Crypto.MediaEncryptionResult {
val secureRandom = SecureRandom()
val initVectorBytes = ByteArray(16) { 0.toByte() }
val ivRandomPart = ByteArray(8)
secureRandom.nextBytes(ivRandomPart)
System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size)
val key = ByteArray(32)
secureRandom.nextBytes(key)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
val outputFile = File.createTempFile("_encrypt-${UUID.randomUUID()}", ".png")
outputFile.outputStream().use { s ->
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
val data = ByteArray(CRYPTO_BUFFER_SIZE)
var read: Int
var encodedBytes: ByteArray
input.use { inputStream ->
read = inputStream.read(data)
var totalRead = read
while (read != -1) {
encodedBytes = encryptCipher.update(data, 0, read)
messageDigest.update(encodedBytes, 0, encodedBytes.size)
s.write(encodedBytes)
read = inputStream.read(data)
totalRead += read
}
}
encodedBytes = encryptCipher.doFinal()
messageDigest.update(encodedBytes, 0, encodedBytes.size)
s.write(encodedBytes)
}
return Crypto.MediaEncryptionResult(
uri = outputFile.toURI(),
contentLength = outputFile.length(),
algorithm = "A256CTR",
ext = true,
keyOperations = listOf("encrypt", "decrypt"),
kty = "oct",
k = base64ToBase64Url(base64.encode(key)),
iv = base64.encode(initVectorBytes).replace("\n", "").replace("=", ""),
hashes = mapOf("sha256" to base64ToUnpaddedBase64(base64.encode(messageDigest.digest()))),
v = "v2"
)
}
}
private fun base64ToBase64Url(base64: String): String {
return base64.replace("\n".toRegex(), "")
.replace("\\+".toRegex(), "-")
.replace('/', '_')
.replace("=", "")
}
private fun base64ToUnpaddedBase64(base64: String): String {
return base64.replace("\n".toRegex(), "")
.replace("=", "")
}

View File

@ -0,0 +1,26 @@
package app.dapk.st.matrix.device.internal
import app.dapk.st.matrix.common.MessageType
import app.dapk.st.matrix.common.RoomId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
sealed class ApiMessage {
@Serializable
@SerialName("text_message")
data class TextMessage(
@SerialName("content") val content: TextContent,
@SerialName("room_id") val roomId: RoomId,
@SerialName("type") val type: String,
) : ApiMessage() {
@Serializable
data class TextContent(
@SerialName("body") val body: String,
@SerialName("msgtype") val type: String = MessageType.TEXT.value,
)
}
}

View File

@ -6,8 +6,6 @@ import app.dapk.st.matrix.device.DeviceService.OneTimeKeys.Key.SignedCurve.Ed255
import app.dapk.st.matrix.device.KnownDeviceStore
import app.dapk.st.matrix.device.ToDevicePayload
import app.dapk.st.matrix.http.MatrixHttpClient
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import java.util.*
@ -141,22 +139,3 @@ internal class DefaultDeviceService(
}
}
@Serializable
sealed class ApiMessage {
@Serializable
@SerialName("text_message")
data class TextMessage(
@SerialName("content") val content: TextContent,
@SerialName("room_id") val roomId: RoomId,
@SerialName("type") val type: String,
) : ApiMessage() {
@Serializable
data class TextContent(
@SerialName("body") val body: String,
@SerialName("msgtype") val type: String = MessageType.TEXT.value,
)
}
}

View File

@ -2,6 +2,8 @@ plugins { id 'java-test-fixtures' }
applyMatrixServiceModule(project)
dependencies {
implementation project(":core")
kotlinFixtures(it)
testFixturesImplementation(testFixtures(project(":core")))
testFixturesImplementation(testFixtures(project(":matrix:common")))

View File

@ -0,0 +1,31 @@
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 contentLength: Long,
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).inputStream()
}
}
internal object MissingMediaEncrypter : MediaEncrypter {
override suspend fun encrypt(input: InputStream) = throw IllegalStateException("No encrypter instance set")
}

View File

@ -1,15 +1,12 @@
package app.dapk.st.matrix.message
import app.dapk.st.matrix.common.AlgorithmName
import app.dapk.st.matrix.common.CipherText
import app.dapk.st.matrix.common.DeviceId
import app.dapk.st.matrix.common.SessionId
import app.dapk.st.matrix.common.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
fun interface MessageEncrypter {
suspend fun encrypt(message: MessageService.Message): EncryptedMessagePayload
suspend fun encrypt(message: ClearMessagePayload): EncryptedMessagePayload
@Serializable
data class EncryptedMessagePayload(
@ -19,8 +16,13 @@ fun interface MessageEncrypter {
@SerialName("session_id") val sessionId: SessionId,
@SerialName("device_id") val deviceId: DeviceId
)
data class ClearMessagePayload(
val roomId: RoomId,
val contents: JsonString,
)
}
internal object MissingMessageEncrypter : MessageEncrypter {
override suspend fun encrypt(message: MessageService.Message) = throw IllegalStateException("No encrypter instance set")
}
override suspend fun encrypt(message: MessageEncrypter.ClearMessagePayload) = throw IllegalStateException("No encrypter instance set")
}

View File

@ -1,10 +1,14 @@
package app.dapk.st.matrix.message
import app.dapk.st.core.Base64
import app.dapk.st.matrix.MatrixService
import app.dapk.st.matrix.MatrixServiceInstaller
import app.dapk.st.matrix.MatrixServiceProvider
import app.dapk.st.matrix.ServiceDepFactory
import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.common.AlgorithmName
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.MessageType
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.message.internal.DefaultMessageService
import app.dapk.st.matrix.message.internal.ImageContentReader
import kotlinx.coroutines.flow.Flow
@ -67,21 +71,6 @@ interface MessageService : MatrixService {
@SerialName("uri") val uri: String,
) : Content()
@Serializable
data class ImageContent(
@SerialName("url") val url: MxUrl,
@SerialName("body") val filename: String,
@SerialName("info") val info: Info,
@SerialName("msgtype") val type: String = MessageType.IMAGE.value,
) : Content() {
@Serializable
data class Info(
@SerialName("h") val height: Int,
@SerialName("w") val width: Int,
@SerialName("size") val size: Long,
)
}
}
}
@ -142,9 +131,17 @@ fun MatrixServiceInstaller.installMessageService(
backgroundScheduler: BackgroundScheduler,
imageContentReader: ImageContentReader,
messageEncrypter: ServiceDepFactory<MessageEncrypter> = ServiceDepFactory { MissingMessageEncrypter },
mediaEncrypter: ServiceDepFactory<MediaEncrypter> = ServiceDepFactory { MissingMediaEncrypter },
) {
this.install { (httpClient, _, installedServices) ->
SERVICE_KEY to DefaultMessageService(httpClient, localEchoStore, backgroundScheduler, messageEncrypter.create(installedServices), imageContentReader)
SERVICE_KEY to DefaultMessageService(
httpClient,
localEchoStore,
backgroundScheduler,
messageEncrypter.create(installedServices),
mediaEncrypter.create(installedServices),
imageContentReader
)
}
}

View File

@ -0,0 +1,72 @@
package app.dapk.st.matrix.message.internal
import app.dapk.st.matrix.common.MessageType
import app.dapk.st.matrix.common.MxUrl
import app.dapk.st.matrix.common.RoomId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
sealed class ApiMessage {
@Serializable
@SerialName("text_message")
data class TextMessage(
@SerialName("content") val content: TextContent,
@SerialName("room_id") val roomId: RoomId,
@SerialName("type") val type: String,
) : ApiMessage() {
@Serializable
data class TextContent(
@SerialName("body") val body: String,
@SerialName("msgtype") val type: String = MessageType.TEXT.value,
) : ApiMessageContent
}
@Serializable
@SerialName("image_message")
data class ImageMessage(
@SerialName("content") val content: ImageContent,
@SerialName("room_id") val roomId: RoomId,
@SerialName("type") val type: String,
) : ApiMessage() {
@Serializable
data class ImageContent(
@SerialName("url") val url: MxUrl?,
@SerialName("body") val filename: String,
@SerialName("info") val info: Info,
@SerialName("msgtype") val type: String = MessageType.IMAGE.value,
@SerialName("file") val file: File? = null,
) : ApiMessageContent {
@Serializable
data class Info(
@SerialName("h") val height: Int,
@SerialName("w") val width: Int,
@SerialName("size") val size: Long,
)
@Serializable
data class File(
@SerialName("url") val url: MxUrl,
@SerialName("key") val key: EncryptionMeta,
@SerialName("iv") val iv: String,
@SerialName("hashes") val hashes: Map<String, String>,
@SerialName("v") val v: String
) {
@Serializable
data class EncryptionMeta(
@SerialName("alg") val algorithm: String,
@SerialName("ext") val ext: Boolean,
@SerialName("key_ops") val keyOperations: List<String>,
@SerialName("kty") val kty: String,
@SerialName("k") val k: String
)
}
}
}
}
sealed interface ApiMessageContent

View File

@ -3,10 +3,7 @@ package app.dapk.st.matrix.message.internal
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,16 +17,17 @@ internal class DefaultMessageService(
private val localEchoStore: LocalEchoStore,
private val backgroundScheduler: BackgroundScheduler,
messageEncrypter: MessageEncrypter,
mediaEncrypter: MediaEncrypter,
imageContentReader: ImageContentReader,
) : MessageService, MatrixTaskRunner {
private val sendMessageUseCase = SendMessageUseCase(httpClient, messageEncrypter, imageContentReader)
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
override suspend fun run(task: MatrixTaskRunner.MatrixTask): MatrixTaskRunner.TaskResult {
val message = when(task.type) {
val message = when (task.type) {
MATRIX_MESSAGE_TASK_TYPE -> Json.decodeFromString(MessageService.Message.TextMessage.serializer(), task.jsonPayload)
MATRIX_IMAGE_MESSAGE_TASK_TYPE -> Json.decodeFromString(MessageService.Message.ImageMessage.serializer(), task.jsonPayload)
else -> throw IllegalStateException("Unhandled task type: ${task.type}")

View File

@ -1,7 +1,10 @@
package app.dapk.st.matrix.message.internal
import java.io.InputStream
interface ImageContentReader {
fun read(uri: String): ImageContent
fun meta(uri: String): ImageContent
fun inputStream(uri: String): InputStream
data class ImageContent(
val height: Int,
@ -9,28 +12,5 @@ interface ImageContentReader {
val size: Long,
val fileName: String,
val mimeType: String,
val content: ByteArray
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ImageContent
if (height != other.height) return false
if (width != other.width) return false
if (size != other.size) return false
if (!content.contentEquals(other.content)) return false
return true
}
override fun hashCode(): Int {
var result = height
result = 31 * result + width
result = 31 * result + size.hashCode()
result = 31 * result + content.contentHashCode()
return result
}
}
)
}

View File

@ -1,62 +1,176 @@
package app.dapk.st.matrix.message.internal
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.EventType
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
import app.dapk.st.matrix.message.MessageService.Message
internal class SendMessageUseCase(
private val httpClient: MatrixHttpClient,
private val messageEncrypter: MessageEncrypter,
private val mediaEncrypter: MediaEncrypter,
private val imageContentReader: ImageContentReader,
) {
suspend fun sendMessage(message: MessageService.Message): EventId {
return when (message) {
is MessageService.Message.TextMessage -> {
val request = when (message.sendEncrypted) {
true -> {
sendRequest(
roomId = message.roomId,
eventType = EventType.ENCRYPTED,
txId = message.localId,
content = messageEncrypter.encrypt(message),
)
}
private val mapper = ApiMessageMapper()
false -> {
sendRequest(
roomId = message.roomId,
eventType = EventType.ROOM_MESSAGE,
txId = message.localId,
content = message.content,
)
}
suspend fun sendMessage(message: Message): EventId {
return with(mapper) {
when (message) {
is Message.TextMessage -> {
val request = textMessageRequest(message)
httpClient.execute(request).eventId
}
httpClient.execute(request).eventId
is Message.ImageMessage -> {
val request = imageMessageRequest(message)
httpClient.execute(request).eventId
}
}
}
}
private suspend fun ApiMessageMapper.textMessageRequest(message: Message.TextMessage): HttpRequest<ApiSendResponse> {
val contents = message.toContents()
return when (message.sendEncrypted) {
true -> sendRequest(
roomId = message.roomId,
eventType = EventType.ENCRYPTED,
txId = message.localId,
content = messageEncrypter.encrypt(
MessageEncrypter.ClearMessagePayload(
message.roomId,
contents.toMessageJson(message.roomId)
)
),
)
false -> sendRequest(
roomId = message.roomId,
eventType = EventType.ROOM_MESSAGE,
txId = message.localId,
content = contents,
)
}
}
private suspend fun ApiMessageMapper.imageMessageRequest(message: Message.ImageMessage): HttpRequest<ApiSendResponse> {
val imageMeta = imageContentReader.meta(message.content.uri)
return when (message.sendEncrypted) {
true -> {
val result = mediaEncrypter.encrypt(imageContentReader.inputStream(message.content.uri))
val uri = httpClient.execute(
uploadRequest(
result.openStream(),
result.contentLength,
imageMeta.fileName,
"application/octet-stream"
)
).contentUri
val content = ApiMessage.ImageMessage.ImageContent(
url = null,
filename = imageMeta.fileName,
file = ApiMessage.ImageMessage.ImageContent.File(
url = uri,
key = ApiMessage.ImageMessage.ImageContent.File.EncryptionMeta(
algorithm = result.algorithm,
ext = result.ext,
keyOperations = result.keyOperations,
kty = result.kty,
k = result.k,
),
iv = result.iv,
hashes = result.hashes,
v = result.v,
),
info = ApiMessage.ImageMessage.ImageContent.Info(
height = imageMeta.height,
width = imageMeta.width,
size = imageMeta.size
)
)
val json = JsonString(
MatrixHttpClient.jsonWithDefaults.encodeToString(
ApiMessage.ImageMessage.serializer(),
ApiMessage.ImageMessage(
content = content,
roomId = message.roomId,
type = EventType.ROOM_MESSAGE.value,
)
)
)
sendRequest(
roomId = message.roomId,
eventType = EventType.ENCRYPTED,
txId = message.localId,
content = messageEncrypter.encrypt(MessageEncrypter.ClearMessagePayload(message.roomId, json)),
)
}
is MessageService.Message.ImageMessage -> {
val imageContent = imageContentReader.read(message.content.uri)
val uri = httpClient.execute(uploadRequest(imageContent.content, imageContent.fileName, imageContent.mimeType)).contentUri
val request = sendRequest(
false -> {
val uri = httpClient.execute(
uploadRequest(
imageContentReader.inputStream(message.content.uri),
imageMeta.size,
imageMeta.fileName,
imageMeta.mimeType
)
).contentUri
sendRequest(
roomId = message.roomId,
eventType = EventType.ROOM_MESSAGE,
txId = message.localId,
content = MessageService.Message.Content.ImageContent(
content = ApiMessage.ImageMessage.ImageContent(
url = uri,
filename = imageContent.fileName,
MessageService.Message.Content.ImageContent.Info(
height = imageContent.height,
width = imageContent.width,
size = imageContent.size
filename = imageMeta.fileName,
ApiMessage.ImageMessage.ImageContent.Info(
height = imageMeta.height,
width = imageMeta.width,
size = imageMeta.size
)
),
)
httpClient.execute(request).eventId
}
}
}
}
class ApiMessageMapper {
fun Message.TextMessage.toContents() = ApiMessage.TextMessage.TextContent(
this.content.body,
this.content.type,
)
fun ApiMessage.TextMessage.TextContent.toMessageJson(roomId: RoomId) = JsonString(
MatrixHttpClient.jsonWithDefaults.encodeToString(
ApiMessage.TextMessage.serializer(),
ApiMessage.TextMessage(
content = this,
roomId = roomId,
type = EventType.ROOM_MESSAGE.value
)
)
)
fun Message.ImageMessage.toContents(uri: MxUrl, image: ImageContentReader.ImageContent) = ApiMessage.ImageMessage.ImageContent(
url = uri,
filename = image.fileName,
ApiMessage.ImageMessage.ImageContent.Info(
height = image.height,
width = image.width,
size = image.size
)
)
}

View File

@ -9,18 +9,20 @@ import app.dapk.st.matrix.message.ApiSendResponse
import app.dapk.st.matrix.message.ApiUploadResponse
import app.dapk.st.matrix.message.MessageEncrypter
import app.dapk.st.matrix.message.MessageService.EventMessage
import app.dapk.st.matrix.message.MessageService.Message
import io.ktor.content.*
import app.dapk.st.matrix.message.internal.ApiMessage.ImageMessage
import app.dapk.st.matrix.message.internal.ApiMessage.TextMessage
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.utils.io.jvm.javaio.*
import java.io.InputStream
import java.util.*
internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, content: Message.Content) = httpRequest<ApiSendResponse>(
internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, content: ApiMessageContent) = httpRequest<ApiSendResponse>(
path = "_matrix/client/r0/rooms/${roomId.value}/send/${eventType.value}/${txId}",
method = MatrixHttpClient.Method.PUT,
body = when (content) {
is Message.Content.TextContent -> jsonBody(Message.Content.TextContent.serializer(), content, MatrixHttpClient.jsonWithDefaults)
is Message.Content.ImageContent -> jsonBody(Message.Content.ImageContent.serializer(), content, MatrixHttpClient.jsonWithDefaults)
is Message.Content.ApiImageContent -> throw IllegalArgumentException()
is TextMessage.TextContent -> jsonBody(TextMessage.TextContent.serializer(), content, MatrixHttpClient.jsonWithDefaults)
is ImageMessage.ImageContent -> jsonBody(ImageMessage.ImageContent.serializer(), content, MatrixHttpClient.jsonWithDefaults)
}
)
@ -38,12 +40,15 @@ internal fun sendRequest(roomId: RoomId, eventType: EventType, content: EventMes
}
)
internal fun uploadRequest(body: ByteArray, filename: String, contentType: String) = httpRequest<ApiUploadResponse>(
internal fun uploadRequest(stream: InputStream, contentLength: Long, filename: String, contentType: String) = httpRequest<ApiUploadResponse>(
path = "_matrix/media/r0/upload/?filename=$filename",
headers = listOf("Content-Type" to contentType),
method = MatrixHttpClient.Method.POST,
body = ByteArrayContent(body, ContentType.parse(contentType)),
body = ChannelWriterContent(
body = { stream.copyTo(this) },
contentType = ContentType.parse(contentType),
contentLength = contentLength,
),
)
fun txId() = "local.${UUID.randomUUID()}"

View File

@ -81,6 +81,14 @@ class SmokeTest {
@Test
@Order(7)
fun `can send and receive encrypted image messages`() = testAfterInitialSync { alice, bob ->
val testImage = loadResourceFile("test-image2.png")
alice.sendImageMessage(SharedState.sharedRoom, testImage, isEncrypted = true)
bob.expectImageMessage(SharedState.sharedRoom, testImage, SharedState.alice.roomMember)
}
@Test
@Order(8)
fun `can request and verify devices`() = testAfterInitialSync { alice, bob ->
alice.client.cryptoService().verificationAction(Verification.Action.Request(bob.userId(), bob.deviceId()))
alice.client.cryptoService().verificationState().automaticVerification(alice).expectAsync { it == Verification.State.Done }

View File

@ -7,6 +7,7 @@ import TestUser
import app.dapk.st.core.extensions.ifNull
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.crypto.MediaDecrypter
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.message.messageService
import app.dapk.st.matrix.sync.RoomEvent
@ -22,6 +23,7 @@ import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.fail
import org.amshove.kluent.shouldBeEqualTo
import java.io.ByteArrayOutputStream
import java.io.File
import java.math.BigInteger
import java.security.MessageDigest
@ -145,10 +147,20 @@ class MatrixTestScope(private val testScope: TestScope) {
this.client.syncService().room(roomId)
.map {
it.events.filterIsInstance<RoomEvent.Image>().map {
println("found: ${it.imageMeta.url}")
val output = File(image.parentFile.absolutePath, "output.png")
HttpClient().request(it.imageMeta.url).bodyAsChannel().copyAndClose(output.writeChannel())
output.readBytes().md5Hash() to it.author
val md5Hash = when (val keys = it.imageMeta.keys) {
null -> output.readBytes().md5Hash()
else -> {
val byteStream = ByteArrayOutputStream()
MediaDecrypter(this.base64).decrypt(output.inputStream(), keys.k, keys.iv).collect {
byteStream.write(it)
}
byteStream.toByteArray().md5Hash()
}
}
md5Hash to it.author
}.firstOrNull()
}
.assert(image.readBytes().md5Hash() to author)

View File

@ -16,14 +16,9 @@ import app.dapk.st.matrix.crypto.cryptoService
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.device.internal.ApiMessage
import app.dapk.st.matrix.http.MatrixHttpClient
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
@ -35,7 +30,6 @@ import app.dapk.st.olm.DeviceKeyFactory
import app.dapk.st.olm.OlmPersistenceWrapper
import app.dapk.st.olm.OlmWrapper
import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.amshove.kluent.fail
import test.impl.InMemoryDatabase
@ -88,6 +82,7 @@ class TestMatrix(
},
coroutineDispatchers = coroutineDispatchers
)
val base64 = JavaBase64()
val client = MatrixClient(
KtorMatrixHttpClientFactory(
@ -100,7 +95,6 @@ class TestMatrix(
installAuthService(storeModule.credentialsStore())
installEncryptionService(storeModule.knownDevicesStore())
val base64 = JavaBase64()
val olmAccountStore = OlmPersistenceWrapper(storeModule.olmStore(), base64)
val olm = OlmWrapper(
olmStore = olmAccountStore,
@ -124,39 +118,47 @@ class TestMatrix(
coroutineDispatchers = coroutineDispatchers,
)
installMessageService(storeModule.localEchoStore, InstantScheduler(it), JavaImageContentReader()) { serviceProvider ->
MessageEncrypter { message ->
val result = serviceProvider.cryptoService().encrypt(
roomId = when (message) {
is MessageService.Message.TextMessage -> message.roomId
is MessageService.Message.ImageMessage -> message.roomId
},
credentials = storeModule.credentialsStore().credentials()!!,
when (message) {
is MessageService.Message.TextMessage -> JsonString(
MatrixHttpClient.jsonWithDefaults.encodeToString(
ApiMessage.TextMessage(
ApiMessage.TextMessage.TextContent(
message.content.body,
message.content.type,
), message.roomId, type = EventType.ROOM_MESSAGE.value
)
)
)
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,
)
is MessageService.Message.ImageMessage -> TODO()
}
)
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,
contentLength = result.contentLength,
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(),
@ -338,7 +340,7 @@ class JavaBase64 : Base64 {
class JavaImageContentReader : ImageContentReader {
override fun read(uri: String): ImageContentReader.ImageContent {
override fun meta(uri: String): ImageContentReader.ImageContent {
val file = File(uri)
val size = file.length()
val image = ImageIO.read(file)
@ -348,8 +350,9 @@ class JavaImageContentReader : ImageContentReader {
size = size,
mimeType = "image/${file.extension}",
fileName = file.name,
content = file.readBytes()
)
}
override fun inputStream(uri: String) = File(uri).inputStream()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB