diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/DecryptingFetcher.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/DecryptingFetcher.kt new file mode 100644 index 0000000..16e11d6 --- /dev/null +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/DecryptingFetcher.kt @@ -0,0 +1,64 @@ +package app.dapk.st.messenger + +import android.util.Base64 +import app.dapk.st.matrix.sync.RoomEvent +import coil.bitmap.BitmapPool +import coil.decode.DataSource +import coil.decode.Options +import coil.fetch.FetchResult +import coil.fetch.Fetcher +import coil.fetch.SourceResult +import coil.size.Size +import okhttp3.OkHttpClient +import okhttp3.Request +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 DecryptingFetcher : Fetcher { + + private val http = OkHttpClient() + + override suspend fun fetch(pool: BitmapPool, data: RoomEvent.Image, size: Size, options: Options): FetchResult { + val keys = data.imageMeta.keys!! + 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 response = http.newCall(Request.Builder().url(data.imageMeta.url).build()).execute() + 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 SourceResult(outputStream, null, DataSource.NETWORK) + } + + override fun key(data: RoomEvent.Image) = data.imageMeta.url + +} \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt index 4eb5dab..52fbff7 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt @@ -9,10 +9,10 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Surface import androidx.compose.ui.Modifier -import app.dapk.st.design.components.SmallTalkTheme import app.dapk.st.core.DapkActivity import app.dapk.st.core.module import app.dapk.st.core.viewModel +import app.dapk.st.design.components.SmallTalkTheme import app.dapk.st.matrix.common.RoomId import kotlinx.parcelize.Parcelize diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index 16ef053..97b4a58 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -1,5 +1,6 @@ package app.dapk.st.messenger +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* @@ -179,16 +180,9 @@ private fun LazyItemScope.Image(self: UserId, message: RoomEvent.Image, wasPrevi isNotSelf = false, wasPreviousMessageSameSender = wasPreviousMessageSameSender ) { - Text(message.imageMeta.url) - androidx.compose.foundation.Image( - painter = rememberImagePainter( - data = message.imageMeta.url, - ), - contentDescription = null, - modifier = Modifier - .size(100.dp) - .align(Alignment.Center) - ) + Box { + MessageImage(message) + } } } } @@ -200,25 +194,29 @@ private fun LazyItemScope.Image(self: UserId, message: RoomEvent.Image, wasPrevi isNotSelf = true, wasPreviousMessageSameSender = wasPreviousMessageSameSender ) { - Text(message.imageMeta.url) - androidx.compose.foundation.Image( - painter = rememberImagePainter( - data = message.imageMeta.url, - builder = { - - } - ), - contentDescription = null, - modifier = Modifier - .size(100.dp) - .align(Alignment.Center) - ) + Box { + MessageImage(message) + } } } } } } +@Composable +private fun MessageImage(message: RoomEvent.Image) { + val width = with(LocalDensity.current) { message.imageMeta.width.toDp() } + val height = with(LocalDensity.current) { message.imageMeta.height.toDp() } + + Image( + modifier = Modifier.size(width, height), + painter = rememberImagePainter( + data = message, + builder = { fetcher(DecryptingFetcher()) } + ), + contentDescription = null, + ) +} @Composable private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMessageSameSender: Boolean) { diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt index 8021b96..dc2ba44 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt @@ -93,7 +93,18 @@ sealed class RoomEvent { @SerialName("width") val width: Int, @SerialName("height") val height: Int, @SerialName("url") val url: String, - ) + @SerialName("keys") val keys: Keys?, + ) { + + @Serializable + data class Keys( + @SerialName("k") val k: String, + @SerialName("iv") val iv: String, + @SerialName("v") val v: String, + @SerialName("hashes") val hashes: Map, + ) + + } } } diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt index 0136ac2..39ed572 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt @@ -459,7 +459,8 @@ internal sealed class ApiTimelineEvent { @Serializable data class Image( - @SerialName("file") val file: File, + @SerialName("url") val url: MxUrl? = null, + @SerialName("file") val file: File? = null, @SerialName("info") val info: Info, @SerialName("m.relates_to") override val relation: Relation? = null, @SerialName("msgtype") val messageType: String = "m.image", @@ -468,14 +469,24 @@ internal sealed class ApiTimelineEvent { @Serializable data class File( @SerialName("url") val url: MxUrl, - ) + @SerialName("iv") val iv: String, + @SerialName("v") val v: String, + @SerialName("hashes") val hashes: Map, + @SerialName("key") val key: Key, + ) { + + @Serializable + data class Key( + @SerialName("k") val k: String, + ) + + } @Serializable internal data class Info( @SerialName("h") val height: Int, @SerialName("w") val width: Int, ) - } @Serializable @@ -553,10 +564,7 @@ internal object ApiTimelineMessageContentDeserializer : KSerializer ApiTimelineEvent.TimelineMessage.Content.Text.serializer().deserialize(decoder) - "m.image" -> when (element.jsonObject["file"]) { - null -> ApiTimelineEvent.TimelineMessage.Content.Ignored - else -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder) - } + "m.image" -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder) else -> { println(element) ApiTimelineEvent.TimelineMessage.Content.Ignored diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt index 418dfb1..9ab0984 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt @@ -37,7 +37,8 @@ internal class RoomEventsDecrypter( imageMeta = RoomEvent.Image.ImageMeta( width = content.info.width, height = content.info.height, - url = content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: "" + url = content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer), + keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) } ), encryptedContent = null, ) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt index de508d1..01b08c3 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventFactory.kt @@ -47,7 +47,8 @@ internal class RoomEventFactory( return RoomEvent.Image.ImageMeta( content.info.width, content.info.height, - content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) + content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer), + keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) } ) } }