Decrypt Attachment - WIP

This commit is contained in:
Benoit Marty 2019-06-24 22:33:30 +02:00
parent 707a4712fc
commit b54ca5a8a0
12 changed files with 302 additions and 48 deletions

View File

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

View File

@ -14,7 +14,7 @@
* limitations under the License. * 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.text.TextUtils
import android.util.Base64 import android.util.Base64
@ -142,24 +142,27 @@ object MXEncryptedAttachments {
* @return the decrypted attachment stream * @return the decrypted attachment stream
*/ */
fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? { 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 // sanity checks
if (null == attachmentStream || null == encryptedFileInfo) { if (null == attachmentStream || elementToDecrypt == null) {
Timber.e("## decryptAttachment() : null parameters") Timber.e("## decryptAttachment() : null stream")
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")
return null return null
} }


@ -177,8 +180,8 @@ object MXEncryptedAttachments {
val outStream = ByteArrayOutputStream() val outStream = ByteArrayOutputStream()


try { try {
val key = Base64.decode(base64UrlToBase64(encryptedFileInfo.key!!.k), Base64.DEFAULT) val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT)
val initVectorBytes = Base64.decode(encryptedFileInfo.iv, Base64.DEFAULT) val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT)


val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
@ -205,7 +208,7 @@ object MXEncryptedAttachments {


val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) 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") Timber.e("## decryptAttachment() : Digest value mismatch")
outStream.close() outStream.close()
return null return null

View File

@ -33,7 +33,7 @@ data class EncryptedFileInfo(
* Not documented * Not documented
*/ */
@Json(name = "mimetype") @Json(name = "mimetype")
var mimetype: String, var mimetype: String? = null,


/** /**
* Required. A JSON Web Key object. * 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. * Required. The Initialisation Vector used by AES-CTR, encoded as unpadded base64.
*/ */
@Json(name = "iv") @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. * 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". * Clients should support the SHA-256 hash, which uses the key "sha256".
*/ */
@Json(name = "hashes") @Json(name = "hashes")
var hashes: Map<String, String>, var hashes: Map<String, String>? = null,


/** /**
* Required. Version of the encrypted attachments protocol. Must be "v2". * Required. Version of the encrypted attachments protocol. Must be "v2".
*/ */
@Json(name = "v") @Json(name = "v")
var v: String? = null 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
}
}

View File

@ -24,7 +24,7 @@ data class EncryptedFileKey(
* Required. Algorithm. Must be "A256CTR". * Required. Algorithm. Must be "A256CTR".
*/ */
@Json(name = "alg") @Json(name = "alg")
var alg: String, var alg: String? = null,


/** /**
* Required. Extractable. Must be true. This is a W3C extension. * 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". * Required. Key operations. Must at least contain "encrypt" and "decrypt".
*/ */
@Json(name = "key_ops") @Json(name = "key_ops")
var key_ops: List<String>, var key_ops: List<String>? = null,


/** /**
* Required. Key type. Must be "oct". * Required. Key type. Must be "oct".
*/ */
@Json(name = "kty") @Json(name = "kty")
var kty: String, var kty: String? = null,


/** /**
* Required. The key, encoded as urlsafe unpadded base64. * Required. The key, encoded as urlsafe unpadded base64.
*/ */
@Json(name = "k") @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
}
}



View File

@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * 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 * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -14,21 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */


package im.vector.riotredesign.core.glide; package im.vector.riotredesign.core.glide


import android.content.Context; import com.bumptech.glide.load.Option
import android.util.Log; import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt


import com.bumptech.glide.GlideBuilder; const val ElementToDecryptOptionKey = "im.vector.riotx.core.glide.ElementToDecrypt"
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;


@GlideModule
public final class MyAppGlideModule extends AppGlideModule {


@Override val ELEMENT_TO_DECRYPT = Option.memory(
public void applyOptions(Context context, GlideBuilder builder) { ElementToDecryptOptionKey, ElementToDecrypt("", "", ""))
builder.setLogLevel(Log.ERROR);
}


}

View File

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

View File

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

override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<InputStream, InputStream> {
return VectorGlideModelLoader()
}

override fun teardown() {
// Is there something to do here?
}

}

class VectorGlideModelLoader : ModelLoader<InputStream, InputStream> {
override fun handles(model: InputStream): Boolean {
// Always handle
return true
}

override fun buildLoadData(model: InputStream, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream>? {
return ModelLoader.LoadData(ObjectKey(model), VectorGlideDataFetcher(model, options.get(ELEMENT_TO_DECRYPT)))
}
}

class VectorGlideDataFetcher(private val inputStream: InputStream,
private val elementToDecrypt: ElementToDecrypt?) : DataFetcher<InputStream> {
override fun getDataClass(): Class<InputStream> {
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<in InputStream>) {
if (elementToDecrypt?.k?.isNotBlank() == true) {
// Encrypted stream
callback.onDataReady(MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt))
} else {
// Not encrypted stream
callback.onDataReady(inputStream)
}
}
}

View File

@ -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.model.message.*
import im.vector.matrix.android.api.session.room.send.SendState 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.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.riotredesign.EmojiCompatFontProvider import im.vector.riotredesign.EmojiCompatFontProvider
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
@ -179,7 +180,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data( val data = ImageContentRenderer.Data(
filename = messageContent.body, filename = messageContent.body,
url = messageContent.url, url = messageContent.encryptedFileInfo?.url ?: messageContent.url,
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
height = messageContent.info?.height, height = messageContent.info?.height,
maxHeight = maxHeight, maxHeight = maxHeight,
width = messageContent.info?.width, width = messageContent.info?.width,
@ -220,7 +222,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body, filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailUrl, url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height, height = messageContent.videoInfo?.height,
maxHeight = maxHeight, maxHeight = maxHeight,
width = messageContent.videoInfo?.width, width = messageContent.videoInfo?.width,

View File

@ -20,11 +20,13 @@ import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import android.widget.ImageView import android.widget.ImageView
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.github.piasy.biv.view.BigImageView 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.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.di.ActiveSessionHolder
import im.vector.riotredesign.core.glide.ELEMENT_TO_DECRYPT
import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -37,6 +39,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
data class Data( data class Data(
val filename: String, val filename: String,
val url: String?, val url: String?,
val elementToDecrypt: ElementToDecrypt?,
val height: Int?, val height: Int?,
val maxHeight: Int, val maxHeight: Int,
val width: Int?, val width: Int?,
@ -70,6 +73,15 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
GlideApp GlideApp
.with(imageView) .with(imageView)
.load(resolvedUrl) .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() .dontAnimate()
.transform(RoundedCorners(dpToPx(8, imageView.context))) .transform(RoundedCorners(dpToPx(8, imageView.context)))
.thumbnail(0.3f) .thumbnail(0.3f)
@ -81,6 +93,8 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val fullSize = contentUrlResolver.resolveFullSize(data.url) val fullSize = contentUrlResolver.resolveFullSize(data.url)
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)

// TODO DECRYPT_FILE Decrypt file
imageView.showImage( imageView.showImage(
Uri.parse(thumbnail), Uri.parse(thumbnail),
Uri.parse(fullSize) Uri.parse(fullSize)

View File

@ -26,6 +26,7 @@ import javax.inject.Inject


class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){ class VideoContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){


// TODO DECRYPT_FILE Encrypted data
@Parcelize @Parcelize
data class Data( data class Data(
val filename: String, val filename: String,

View File

@ -32,6 +32,7 @@ import androidx.core.view.isVisible
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory import androidx.preference.PreferenceCategory
import com.bumptech.glide.Glide
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.vector.riotredesign.R 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.MainActivity
import im.vector.riotredesign.features.themes.ThemeUtils import im.vector.riotredesign.features.themes.ThemeUtils
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker 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.lang.ref.WeakReference
import java.util.* import java.util.*


@ -197,6 +202,18 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {


it.onPreferenceClickListener = Preference.OnPreferenceClickListener { it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
notImplemented() 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 /* TODO
displayLoadingView() displayLoadingView()



View File

@ -119,7 +119,7 @@ class VectorSettingsPreferencesFragment : VectorSettingsBaseFragment() {
context?.let { context: Context -> context?.let { context: Context ->
AlertDialog.Builder(context) AlertDialog.Builder(context)
.setSingleChoiceItems(R.array.media_saving_choice, .setSingleChoiceItems(R.array.media_saving_choice,
PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n -> PreferencesManager.getSelectedMediasSavingPeriod(activity)) { d, n ->
PreferencesManager.setSelectedMediasSavingPeriod(activity, n) PreferencesManager.setSelectedMediasSavingPeriod(activity, n)
d.cancel() d.cancel()