Feature/aris/issue 465 scrub exif data (#4248)
Implement ImageExifTagRemover to scrub user sensitive data while sending original size photos - Return a not scrubbed file when there is an exception while scrubbing the jpeg file - Improve error handling on image compression
This commit is contained in:
parent
2a47acc68a
commit
aea22201c3
|
@ -0,0 +1 @@
|
|||
Uppon sharing image compression fails, return the original image
|
|
@ -0,0 +1 @@
|
|||
Scrub user sensitive data like gps location from images when sending on original quality
|
|
@ -130,6 +130,9 @@ ext.libs = [
|
|||
'emojiMaterial' : "com.vanniktech:emoji-material:$vanniktechEmoji",
|
||||
'emojiGoogle' : "com.vanniktech:emoji-google:$vanniktechEmoji"
|
||||
],
|
||||
apache:[
|
||||
'commonsImaging' : "org.apache.sanselan:sanselan:0.97-incubator"
|
||||
],
|
||||
tests : [
|
||||
'kluent' : "org.amshove.kluent:kluent-android:1.68",
|
||||
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",
|
||||
|
|
|
@ -154,6 +154,9 @@ dependencies {
|
|||
// Video compression
|
||||
implementation 'com.otaliastudios:transcoder:0.10.4'
|
||||
|
||||
// Exif data handling
|
||||
implementation libs.apache.commonsImaging
|
||||
|
||||
// Phone number https://github.com/google/libphonenumber
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.35'
|
||||
|
||||
|
|
|
@ -20,22 +20,23 @@ import android.graphics.Bitmap
|
|||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.internal.util.TemporaryFileCreator
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ImageCompressor @Inject constructor(
|
||||
private val temporaryFileCreator: TemporaryFileCreator
|
||||
private val temporaryFileCreator: TemporaryFileCreator,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) {
|
||||
suspend fun compress(
|
||||
imageFile: File,
|
||||
desiredWidth: Int,
|
||||
desiredHeight: Int,
|
||||
desiredQuality: Int = 80): File {
|
||||
return withContext(Dispatchers.IO) {
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
val compressedBitmap = BitmapFactory.Options().run {
|
||||
inJustDecodeBounds = true
|
||||
decodeBitmap(imageFile, this)
|
||||
|
@ -52,6 +53,8 @@ internal class ImageCompressor @Inject constructor(
|
|||
destinationFile.outputStream().use {
|
||||
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it)
|
||||
}
|
||||
}.onFailure {
|
||||
return@withContext imageFile
|
||||
}
|
||||
|
||||
destinationFile
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.session.content
|
||||
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.sanselan.Sanselan
|
||||
import org.apache.sanselan.formats.jpeg.JpegImageMetadata
|
||||
import org.apache.sanselan.formats.jpeg.exifRewrite.ExifRewriter
|
||||
import org.apache.sanselan.formats.tiff.constants.ExifTagConstants
|
||||
import org.apache.sanselan.formats.tiff.constants.GPSTagConstants
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.internal.util.TemporaryFileCreator
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This class is responsible for removing Exif tags from image files
|
||||
*/
|
||||
|
||||
internal class ImageExifTagRemover @Inject constructor(
|
||||
private val temporaryFileCreator: TemporaryFileCreator,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) {
|
||||
|
||||
/**
|
||||
* Remove sensitive exif tags from a jpeg image file.
|
||||
* Scrubbing exif tags like GPS location and user comments
|
||||
* @param jpegImageFile The image file to be scrubbed
|
||||
* @return the new scrubbed image file, or the original file if the operation failed
|
||||
*/
|
||||
suspend fun removeSensitiveJpegExifTags(jpegImageFile: File): File = withContext(coroutineDispatchers.io) {
|
||||
val outputSet = tryOrNull("Unable to read JpegImageMetadata") {
|
||||
(Sanselan.getMetadata(jpegImageFile) as? JpegImageMetadata)?.exif?.outputSet
|
||||
} ?: return@withContext jpegImageFile
|
||||
|
||||
tryOrNull("Unable to remove ExifData") {
|
||||
outputSet.removeField(ExifTagConstants.EXIF_TAG_GPSINFO)
|
||||
outputSet.removeField(ExifTagConstants.EXIF_TAG_SUBJECT_LOCATION_1)
|
||||
outputSet.removeField(ExifTagConstants.EXIF_TAG_SUBJECT_LOCATION_2)
|
||||
outputSet.removeField(ExifTagConstants.EXIF_TAG_USER_COMMENT)
|
||||
outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_ALTITUDE)
|
||||
outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_ALTITUDE_REF)
|
||||
outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LONGITUDE)
|
||||
outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LONGITUDE_REF)
|
||||
outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LONGITUDE)
|
||||
outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LONGITUDE_REF)
|
||||
outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LATITUDE)
|
||||
outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LATITUDE_REF)
|
||||
outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LATITUDE)
|
||||
outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LATITUDE_REF)
|
||||
} ?: return@withContext jpegImageFile
|
||||
|
||||
val scrubbedFile = temporaryFileCreator.create()
|
||||
return@withContext runCatching {
|
||||
FileOutputStream(scrubbedFile).use { fos ->
|
||||
val outputStream = BufferedOutputStream(fos)
|
||||
ExifRewriter().updateExifMetadataLossless(jpegImageFile, outputStream, outputSet)
|
||||
}
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
scrubbedFile
|
||||
},
|
||||
onFailure = {
|
||||
scrubbedFile.delete()
|
||||
jpegImageFile
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -64,7 +64,7 @@ private data class NewAttachmentAttributes(
|
|||
* Possible next worker : Always [MultipleEventSendingDispatcherWorker]
|
||||
*/
|
||||
internal class UploadContentWorker(val context: Context, params: WorkerParameters) :
|
||||
SessionSafeCoroutineWorker<UploadContentWorker.Params>(context, params, Params::class.java) {
|
||||
SessionSafeCoroutineWorker<UploadContentWorker.Params>(context, params, Params::class.java) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class Params(
|
||||
|
@ -81,6 +81,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||
@Inject lateinit var fileService: DefaultFileService
|
||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||
@Inject lateinit var imageCompressor: ImageCompressor
|
||||
@Inject lateinit var imageExitTagRemover: ImageExifTagRemover
|
||||
@Inject lateinit var videoCompressor: VideoCompressor
|
||||
@Inject lateinit var thumbnailExtractor: ThumbnailExtractor
|
||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||
|
@ -114,7 +115,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||
}
|
||||
|
||||
val attachment = params.attachment
|
||||
val filesToDelete = mutableListOf<File>()
|
||||
val filesToDelete = hashSetOf<File>()
|
||||
|
||||
return try {
|
||||
val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
|
||||
|
@ -219,6 +220,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (attachment.type == ContentAttachmentData.Type.IMAGE && !params.compressBeforeSending) {
|
||||
fileToUpload = imageExitTagRemover.removeSensitiveJpegExifTags(workingFile)
|
||||
.also { filesToDelete.add(it) }
|
||||
newAttachmentAttributes = newAttachmentAttributes.copy(newFileSize = fileToUpload.length())
|
||||
} else {
|
||||
fileToUpload = workingFile
|
||||
// Fix: OpenableColumns.SIZE may return -1 or 0
|
||||
|
|
Loading…
Reference in New Issue