Decrypt video file

This commit is contained in:
Benoit Marty 2019-07-08 17:07:21 +02:00
parent 1b82ed5abb
commit 12bd85e0a9
11 changed files with 280 additions and 49 deletions

View File

@ -26,12 +26,14 @@ import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
import im.vector.matrix.android.internal.crypto.NewSessionListener
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import java.io.File

interface CryptoService {

@ -103,6 +105,13 @@ interface CryptoService {

fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>)

/**
* Decrypt a file.
* Result will be a decrypted file, stored in the cache folder. id parameter will be used to create a sub folder to avoid name collision.
* You can pass the eventId
*/
fun decryptFile(id: String, filename: String, url: String, elementToDecrypt: ElementToDecrypt, callback: MatrixCallback<File>)

fun getEncryptionAlgorithm(roomId: String): String?

fun shouldEncryptForInvitedMembers(roomId: String): Boolean

View File

@ -48,6 +48,7 @@ import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAct
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
@ -78,6 +79,7 @@ import im.vector.matrix.android.internal.util.fetchCopied
import kotlinx.coroutines.*
import org.matrix.olm.OlmManager
import timber.log.Timber
import java.io.File
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@ -112,6 +114,8 @@ internal class CryptoManager @Inject constructor(
private val keysBackup: KeysBackup,
//
private val objectSigner: ObjectSigner,
// File decryptor
private val fileDecryptor: FileDecryptor,
//
private val oneTimeKeysUploader: OneTimeKeysUploader,
//
@ -607,6 +611,10 @@ internal class CryptoManager @Inject constructor(
}
}

override fun decryptFile(id: String, filename: String, url: String, elementToDecrypt: ElementToDecrypt, callback: MatrixCallback<File>) {
fileDecryptor.decryptFile(id, filename, url, elementToDecrypt, callback)
}

/**
* Decrypt an event
*

View File

@ -0,0 +1,99 @@
/*
* 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.matrix.android.internal.crypto

import android.content.Context
import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5
import im.vector.matrix.android.internal.util.writeToFile
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import timber.log.Timber
import java.io.File
import java.io.IOException
import javax.inject.Inject

@SessionScope
internal class FileDecryptor @Inject constructor(private val context: Context,
private val sessionParams: SessionParams,
private val contentUrlResolver: ContentUrlResolver,
private val coroutineDispatchers: MatrixCoroutineDispatchers) {

val okHttpClient = OkHttpClient()

fun decryptFile(id: String,
fileName: String,
url: String,
elementToDecrypt: ElementToDecrypt,
callback: MatrixCallback<File>) {
GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) {
Try {
// Create dir tree:
// <cache>/DF/<md5(userId)>/<md5(id)>/
val tmpFolderRoot = File(context.cacheDir, "DF")
val tmpFolderUser = File(tmpFolderRoot, sessionParams.credentials.userId.md5())
val tmpFolder = File(tmpFolderUser, id.md5())

if (!tmpFolder.exists()) {
tmpFolder.mkdirs()
}

File(tmpFolder, fileName)
}.map { destFile ->
if (!destFile.exists()) {
Try {
Timber.v("## decrypt file")

val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null")

val request = Request.Builder()
.url(resolvedUrl)
.build()

val response = okHttpClient.newCall(request).execute()
val inputStream = response.body()?.byteStream()
Timber.v("Response size ${response.body()?.contentLength()} - Stream available: ${inputStream?.available()}")
if (!response.isSuccessful) {
throw IOException()
}

MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error")
}
.map { inputStream ->
writeToFile(inputStream, destFile)
}
}

destFile
}
}
.foldToCallback(callback)
}
}
}

View File

@ -16,7 +16,6 @@

package im.vector.matrix.android.internal.crypto.attachments

import android.text.TextUtils
import android.util.Base64
import arrow.core.Try
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
@ -198,7 +197,7 @@ object MXEncryptedAttachments {

val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))

if (!TextUtils.equals(elementToDecrypt.sha256, currentDigestValue)) {
if (elementToDecrypt.sha256 != currentDigestValue) {
Timber.e("## decryptAttachment() : Digest value mismatch")
outStream.close()
return null

View File

@ -0,0 +1,37 @@
/*
* 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.matrix.android.internal.util

import androidx.annotation.WorkerThread
import okio.Okio
import java.io.File
import java.io.InputStream

/**
* Save an input stream to a file with Okio
*/
@WorkerThread
fun writeToFile(inputStream: InputStream, outputFile: File) {
val source = Okio.buffer(Okio.source(inputStream))
val sink = Okio.buffer(Okio.sink(outputFile))

source.use { input ->
sink.use { output ->
output.writeAll(input)
}
}
}

View File

@ -23,6 +23,7 @@ import arrow.core.Try
import okio.Okio
import timber.log.Timber
import java.io.File
import java.io.InputStream

/**
* Save a string to a file with Okio

View File

@ -29,14 +29,7 @@ import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
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
@ -87,9 +80,9 @@ class MessageItemFactory @Inject constructor(

val messageContent: MessageContent =
event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.getClearContent().toModel()
?: //Malformed content, we should echo something on screen
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))
?: event.root.getClearContent().toModel()
?: //Malformed content, we should echo something on screen
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message))

if (messageContent.relatesTo?.type == RelationType.REPLACE) {
// ignore replace event, the targeted id is already edited
@ -99,16 +92,16 @@ class MessageItemFactory @Inject constructor(
// val ev = all.toModel<Event>()
return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent,
informationData,
event.annotations?.editSummary,
highlight,
callback)
informationData,
event.annotations?.editSummary,
highlight,
callback)
is MessageTextContent -> buildTextMessageItem(event.sendState,
messageContent,
informationData,
event.annotations?.editSummary,
highlight,
callback
messageContent,
informationData,
event.annotations?.editSummary,
highlight,
callback
)
is MessageImageContent -> buildImageMessageItem(messageContent, informationData, highlight, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback)
@ -142,7 +135,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}

@ -165,7 +158,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
.clickListener(
DebouncedClickListener(View.OnClickListener { _ ->
@ -218,7 +211,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}

@ -239,8 +232,10 @@ class MessageItemFactory @Inject constructor(
)

val videoData = VideoContentRenderer.Data(
eventId = informationData.eventId,
filename = messageContent.body,
videoUrl = messageContent.url,
url = messageContent.encryptedFileInfo?.url ?: messageContent.url,
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
thumbnailMediaData = thumbnailData
)

@ -262,7 +257,7 @@ class MessageItemFactory @Inject constructor(
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}

@ -302,7 +297,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}

@ -334,9 +329,9 @@ class MessageItemFactory @Inject constructor(
//nop
}
},
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
editStart,
editEnd,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
return spannable
}

@ -372,7 +367,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}

@ -408,7 +403,7 @@ class MessageItemFactory @Inject constructor(
}))
.longClickListener { view ->
return@longClickListener callback?.onEventLongClicked(informationData, messageContent, view)
?: false
?: false
}
}


View File

@ -100,7 +100,6 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
return
}

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

View File

@ -18,26 +18,87 @@ package im.vector.riotx.features.media

import android.os.Parcelable
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.VideoView
import androidx.core.view.isVisible
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.error.ErrorFormatter
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
import java.io.File
import javax.inject.Inject

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

// TODO DECRYPT_FILE Encrypted data
@Parcelize
data class Data(
val eventId: String,
val filename: String,
val videoUrl: String?,
val url: String?,
val elementToDecrypt: ElementToDecrypt?,
val thumbnailMediaData: ImageContentRenderer.Data
) : Parcelable

fun render(data: Data, thumbnailView: ImageView, videoView: VideoView) {
fun render(data: Data,
thumbnailView: ImageView,
loadingView: ProgressBar,
videoView: VideoView,
errorView: TextView) {
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
val resolvedUrl = contentUrlResolver.resolveFullSize(data.videoUrl)
videoView.setVideoPath(resolvedUrl)
videoView.start()

if (data.elementToDecrypt != null) {
Timber.v("Decrypt video")
videoView.isVisible = false

if (data.url == null) {
loadingView.isVisible = false
errorView.isVisible = true
errorView.setText(R.string.unknown_error)
} else {
thumbnailView.isVisible = true
loadingView.isVisible = true

activeSessionHolder.getActiveSession()
.decryptFile(data.eventId,
data.filename,
data.url,
data.elementToDecrypt,
object : MatrixCallback<File> {
override fun onSuccess(data: File) {
thumbnailView.isVisible = false
loadingView.isVisible = false
videoView.isVisible = true

videoView.setVideoPath(data.path)
videoView.start()
}

override fun onFailure(failure: Throwable) {
loadingView.isVisible = false
errorView.isVisible = true
errorView.text = errorFormatter.toHumanReadable(failure)
}
})
}
} else {
thumbnailView.isVisible = false
loadingView.isVisible = false

val resolvedUrl = contentUrlResolver.resolveFullSize(data.url)

if (resolvedUrl == null) {
errorView.isVisible = true
errorView.setText(R.string.unknown_error)
} else {
videoView.setVideoPath(resolvedUrl)
videoView.start()
}
}
}

}

View File

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

class VideoMediaViewerActivity : VectorBaseActivity() {

@Inject lateinit var imageContentRenderer: ImageContentRenderer
@Inject lateinit var videoContentRenderer: VideoContentRenderer

override fun injectWith(injector: ScreenComponent) {
@ -38,12 +39,10 @@ class VideoMediaViewerActivity : VectorBaseActivity() {
super.onCreate(savedInstanceState)
setContentView(im.vector.riotx.R.layout.activity_video_media_viewer)
val mediaData = intent.getParcelableExtra<VideoContentRenderer.Data>(EXTRA_MEDIA_DATA)
if (mediaData.videoUrl.isNullOrEmpty()) {
finish()
} else {
configureToolbar(videoMediaViewerToolbar, mediaData)
videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerVideoView)
}

configureToolbar(videoMediaViewerToolbar, mediaData)
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView)
}

private fun configureToolbar(toolbar: Toolbar, mediaData: VideoContentRenderer.Data) {

View File

@ -15,6 +15,7 @@
-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@ -32,12 +33,35 @@
<ImageView
android:id="@+id/videoMediaViewerThumbnailView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible" />

<ProgressBar
android:id="@+id/videoMediaViewerLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />

<VideoView
android:id="@+id/videoMediaViewerVideoView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:visibility="gone" />

<TextView
android:id="@+id/videoMediaViewerErrorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="16dp"
android:textColor="@color/riotx_notice"
android:textSize="16sp"
android:visibility="gone"
tools:text="Error"
tools:visibility="visible" />

</FrameLayout>