diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/ElementToDecrypt.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/ElementToDecrypt.kt new file mode 100644 index 00000000..9f1bf708 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/ElementToDecrypt.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.attachments + +import android.os.Parcelable +import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo +import kotlinx.android.parcel.Parcelize + + +fun EncryptedFileInfo.toElementToDecrypt(): ElementToDecrypt? { + // Check the validity of some fields + if (isValid()) { + return ElementToDecrypt( + iv = this.iv!!, + k = this.key!!.k!!, + sha256 = this.hashes!!["sha256"] ?: error("") + ) + } + + return null +} + + +/** + * Represent data to decode an attachment + */ +@Parcelize +data class ElementToDecrypt( + val iv: String, + val k: String, + val sha256: String +) : Parcelable \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt similarity index 90% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEncryptedAttachments.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt index 47b84af1..9c98d58c 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package im.vector.matrix.android.internal.crypto +package im.vector.matrix.android.internal.crypto.attachments import android.text.TextUtils import android.util.Base64 @@ -142,24 +142,27 @@ object MXEncryptedAttachments { * @return the decrypted attachment stream */ fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? { + if (encryptedFileInfo?.isValid() != true) { + Timber.e("## decryptAttachment() : some fields are not defined, or invalid key fields") + return null + } + + val elementToDecrypt = encryptedFileInfo.toElementToDecrypt() + + return decryptAttachment(attachmentStream, elementToDecrypt) + } + + /** + * Decrypt an attachment + * + * @param attachmentStream the attachment stream + * @param elementToDecrypt the elementToDecrypt info + * @return the decrypted attachment stream + */ + fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?): InputStream? { // sanity checks - if (null == attachmentStream || null == encryptedFileInfo) { - Timber.e("## decryptAttachment() : null parameters") - return null - } - - if (TextUtils.isEmpty(encryptedFileInfo.iv) - || null == encryptedFileInfo.key - || null == encryptedFileInfo.hashes - || !encryptedFileInfo.hashes.containsKey("sha256")) { - Timber.e("## decryptAttachment() : some fields are not defined") - return null - } - - if (!TextUtils.equals(encryptedFileInfo.key!!.alg, "A256CTR") - || !TextUtils.equals(encryptedFileInfo.key!!.kty, "oct") - || TextUtils.isEmpty(encryptedFileInfo.key!!.k)) { - Timber.e("## decryptAttachment() : invalid key fields") + if (null == attachmentStream || elementToDecrypt == null) { + Timber.e("## decryptAttachment() : null stream") return null } @@ -177,8 +180,8 @@ object MXEncryptedAttachments { val outStream = ByteArrayOutputStream() try { - val key = Base64.decode(base64UrlToBase64(encryptedFileInfo.key!!.k), Base64.DEFAULT) - val initVectorBytes = Base64.decode(encryptedFileInfo.iv, Base64.DEFAULT) + val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) + val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT) val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) @@ -205,7 +208,7 @@ object MXEncryptedAttachments { val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) - if (!TextUtils.equals(encryptedFileInfo.hashes["sha256"], currentDigestValue)) { + if (!TextUtils.equals(elementToDecrypt.sha256, currentDigestValue)) { Timber.e("## decryptAttachment() : Digest value mismatch") outStream.close() return null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt index f483ba39..5e09b20c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt @@ -33,7 +33,7 @@ data class EncryptedFileInfo( * Not documented */ @Json(name = "mimetype") - var mimetype: String, + var mimetype: String? = null, /** * Required. A JSON Web Key object. @@ -45,18 +45,45 @@ data class EncryptedFileInfo( * Required. The Initialisation Vector used by AES-CTR, encoded as unpadded base64. */ @Json(name = "iv") - var iv: String, + var iv: String? = null, /** * Required. A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64. * Clients should support the SHA-256 hash, which uses the key "sha256". */ @Json(name = "hashes") - var hashes: Map, + var hashes: Map? = null, /** * Required. Version of the encrypted attachments protocol. Must be "v2". */ @Json(name = "v") var v: String? = null -) +) { + /** + * Check what the spec tells us + */ + fun isValid(): Boolean { + if (url.isNullOrBlank()) { + return false + } + + if (key?.isValid() != true) { + return false + } + + if (iv.isNullOrBlank()) { + return false + } + + if (hashes?.containsKey("sha256") != true) { + return false + } + + if (v != "v2") { + return false + } + + return true + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt index 3ea603c0..3cf1e308 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt @@ -24,7 +24,7 @@ data class EncryptedFileKey( * Required. Algorithm. Must be "A256CTR". */ @Json(name = "alg") - var alg: String, + var alg: String? = null, /** * Required. Extractable. Must be true. This is a W3C extension. @@ -36,18 +36,45 @@ data class EncryptedFileKey( * Required. Key operations. Must at least contain "encrypt" and "decrypt". */ @Json(name = "key_ops") - var key_ops: List, + var key_ops: List? = null, /** * Required. Key type. Must be "oct". */ @Json(name = "kty") - var kty: String, + var kty: String? = null, /** * Required. The key, encoded as urlsafe unpadded base64. */ @Json(name = "k") - var k: String -) + var k: String? = null +) { + /** + * Check what the spec tells us + */ + fun isValid(): Boolean { + if (alg != "A256CTR") { + return false + } + + if (ext != true) { + return false + } + + if (key_ops?.contains("encrypt") != true || key_ops?.contains("decrypt") != true) { + return false + } + + if (kty != "oct") { + return false + } + + if (k.isNullOrBlank()) { + return false + } + + return true + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java b/vector/src/main/java/im/vector/riotredesign/core/glide/ElementToDecryptOption.kt similarity index 53% rename from vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java rename to vector/src/main/java/im/vector/riotredesign/core/glide/ElementToDecryptOption.kt index 08fed579..79805029 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java +++ b/vector/src/main/java/im/vector/riotredesign/core/glide/ElementToDecryptOption.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,21 +14,14 @@ * limitations under the License. */ -package im.vector.riotredesign.core.glide; +package im.vector.riotredesign.core.glide -import android.content.Context; -import android.util.Log; +import com.bumptech.glide.load.Option +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt -import com.bumptech.glide.GlideBuilder; -import com.bumptech.glide.annotation.GlideModule; -import com.bumptech.glide.module.AppGlideModule; +const val ElementToDecryptOptionKey = "im.vector.riotx.core.glide.ElementToDecrypt" -@GlideModule -public final class MyAppGlideModule extends AppGlideModule { - @Override - public void applyOptions(Context context, GlideBuilder builder) { - builder.setLogLevel(Log.ERROR); - } +val ELEMENT_TO_DECRYPT = Option.memory( + ElementToDecryptOptionKey, ElementToDecrypt("", "", "")) -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.kt b/vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.kt new file mode 100644 index 00000000..8937ad48 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.glide + +import android.content.Context +import android.util.Log + +import com.bumptech.glide.Glide +import com.bumptech.glide.GlideBuilder +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule +import java.io.InputStream + +@GlideModule +class MyAppGlideModule : AppGlideModule() { + + override fun applyOptions(context: Context, builder: GlideBuilder) { + builder.setLogLevel(Log.ERROR) + } + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + // FIXME This does not work + registry.append(InputStream::class.java, InputStream::class.java, VectorGlideModelLoaderFactory()) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/glide/VectorGlideModelLoader.kt b/vector/src/main/java/im/vector/riotredesign/core/glide/VectorGlideModelLoader.kt new file mode 100644 index 00000000..8556739d --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/glide/VectorGlideModelLoader.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.glide + +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.signature.ObjectKey +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt +import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments +import java.io.InputStream +import com.bumptech.glide.load.engine.Resource as Resource1 + +class VectorGlideModelLoaderFactory : ModelLoaderFactory { + + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + return VectorGlideModelLoader() + } + + override fun teardown() { + // Is there something to do here? + } + +} + +class VectorGlideModelLoader : ModelLoader { + override fun handles(model: InputStream): Boolean { + // Always handle + return true + } + + override fun buildLoadData(model: InputStream, width: Int, height: Int, options: Options): ModelLoader.LoadData? { + return ModelLoader.LoadData(ObjectKey(model), VectorGlideDataFetcher(model, options.get(ELEMENT_TO_DECRYPT))) + } +} + +class VectorGlideDataFetcher(private val inputStream: InputStream, + private val elementToDecrypt: ElementToDecrypt?) : DataFetcher { + override fun getDataClass(): Class { + return InputStream::class.java + } + + override fun cleanup() { + // ? + } + + override fun getDataSource(): DataSource { + // ? + return DataSource.REMOTE + } + + override fun cancel() { + // ? + } + + override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { + if (elementToDecrypt?.k?.isNotBlank() == true) { + // Encrypted stream + callback.onDataReady(MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)) + } else { + // Not encrypted stream + callback.onDataReady(inputStream) + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 7eda1dac..4bb900d1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.riotredesign.EmojiCompatFontProvider import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.VectorEpoxyModel @@ -179,7 +180,8 @@ class MessageItemFactory @Inject constructor( val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val data = ImageContentRenderer.Data( filename = messageContent.body, - url = messageContent.url, + url = messageContent.encryptedFileInfo?.url ?: messageContent.url, + elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), height = messageContent.info?.height, maxHeight = maxHeight, width = messageContent.info?.width, @@ -220,7 +222,8 @@ class MessageItemFactory @Inject constructor( val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val thumbnailData = ImageContentRenderer.Data( filename = messageContent.body, - url = messageContent.videoInfo?.thumbnailUrl, + url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, + elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = maxHeight, width = messageContent.videoInfo?.width, diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt index 1e50f90b..5fcb443b 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt @@ -20,11 +20,13 @@ import android.net.Uri import android.os.Parcelable import android.widget.ImageView import androidx.exifinterface.media.ExifInterface +import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.github.piasy.biv.view.BigImageView -import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.session.content.ContentUrlResolver +import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.riotredesign.core.di.ActiveSessionHolder +import im.vector.riotredesign.core.glide.ELEMENT_TO_DECRYPT import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx import kotlinx.android.parcel.Parcelize @@ -37,6 +39,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: data class Data( val filename: String, val url: String?, + val elementToDecrypt: ElementToDecrypt?, val height: Int?, val maxHeight: Int, val width: Int?, @@ -70,6 +73,15 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: GlideApp .with(imageView) .load(resolvedUrl) + .apply { + // Give element to decrypt to Glide + if (data.elementToDecrypt != null) { + set(ELEMENT_TO_DECRYPT, data.elementToDecrypt) + // And disable cache + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + } + } .dontAnimate() .transform(RoundedCorners(dpToPx(8, imageView.context))) .thumbnail(0.3f) @@ -81,6 +93,8 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val fullSize = contentUrlResolver.resolveFullSize(data.url) val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) + + // TODO DECRYPT_FILE Decrypt file imageView.showImage( Uri.parse(thumbnail), Uri.parse(fullSize) diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt index 8fd4b43e..2eb5f062 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt @@ -26,6 +26,7 @@ import javax.inject.Inject class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){ + // TODO DECRYPT_FILE Encrypted data @Parcelize data class Data( val filename: String, diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsGeneralFragment.kt index f059ef62..aa7ce304 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsGeneralFragment.kt @@ -32,6 +32,7 @@ import androidx.core.view.isVisible import androidx.preference.EditTextPreference import androidx.preference.Preference import androidx.preference.PreferenceCategory +import com.bumptech.glide.Glide import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import im.vector.riotredesign.R @@ -46,6 +47,10 @@ import im.vector.riotredesign.core.utils.toast import im.vector.riotredesign.features.MainActivity import im.vector.riotredesign.features.themes.ThemeUtils import im.vector.riotredesign.features.workers.signout.SignOutUiWorker +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.lang.ref.WeakReference import java.util.* @@ -197,6 +202,18 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { it.onPreferenceClickListener = Preference.OnPreferenceClickListener { notImplemented() + + // TODO DECRYPT_FILE Quick implementation of clear cache, finish this + GlobalScope.launch(Dispatchers.Main) { + // On UI Thread + Glide.get(requireContext()).clearMemory() + + withContext(Dispatchers.IO) { + // On BG thread + Glide.get(requireContext()).clearDiskCache() + } + } + /* TODO displayLoadingView() diff --git a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt index a77f0f73..0a82ef9f 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/settings/VectorSettingsPreferencesFragment.kt @@ -119,7 +119,7 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() { context?.let { context: Context -> AlertDialog.Builder(context) .setSingleChoiceItems(R.array.media_saving_choice, - PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n -> + PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n -> PreferencesManager.setSelectedMediasSavingPeriod(activity, n) d.cancel()