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",
|
'emojiMaterial' : "com.vanniktech:emoji-material:$vanniktechEmoji",
|
||||||
'emojiGoogle' : "com.vanniktech:emoji-google:$vanniktechEmoji"
|
'emojiGoogle' : "com.vanniktech:emoji-google:$vanniktechEmoji"
|
||||||
],
|
],
|
||||||
|
apache:[
|
||||||
|
'commonsImaging' : "org.apache.sanselan:sanselan:0.97-incubator"
|
||||||
|
],
|
||||||
tests : [
|
tests : [
|
||||||
'kluent' : "org.amshove.kluent:kluent-android:1.68",
|
'kluent' : "org.amshove.kluent:kluent-android:1.68",
|
||||||
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",
|
'timberJunitRule' : "net.lachlanmckee:timber-junit-rule:1.0.1",
|
||||||
|
|
|
@ -154,6 +154,9 @@ dependencies {
|
||||||
// Video compression
|
// Video compression
|
||||||
implementation 'com.otaliastudios:transcoder:0.10.4'
|
implementation 'com.otaliastudios:transcoder:0.10.4'
|
||||||
|
|
||||||
|
// Exif data handling
|
||||||
|
implementation libs.apache.commonsImaging
|
||||||
|
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.35'
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.35'
|
||||||
|
|
||||||
|
|
|
@ -20,22 +20,23 @@ import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||||
import org.matrix.android.sdk.internal.util.TemporaryFileCreator
|
import org.matrix.android.sdk.internal.util.TemporaryFileCreator
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class ImageCompressor @Inject constructor(
|
internal class ImageCompressor @Inject constructor(
|
||||||
private val temporaryFileCreator: TemporaryFileCreator
|
private val temporaryFileCreator: TemporaryFileCreator,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||||
) {
|
) {
|
||||||
suspend fun compress(
|
suspend fun compress(
|
||||||
imageFile: File,
|
imageFile: File,
|
||||||
desiredWidth: Int,
|
desiredWidth: Int,
|
||||||
desiredHeight: Int,
|
desiredHeight: Int,
|
||||||
desiredQuality: Int = 80): File {
|
desiredQuality: Int = 80): File {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(coroutineDispatchers.io) {
|
||||||
val compressedBitmap = BitmapFactory.Options().run {
|
val compressedBitmap = BitmapFactory.Options().run {
|
||||||
inJustDecodeBounds = true
|
inJustDecodeBounds = true
|
||||||
decodeBitmap(imageFile, this)
|
decodeBitmap(imageFile, this)
|
||||||
|
@ -52,6 +53,8 @@ internal class ImageCompressor @Inject constructor(
|
||||||
destinationFile.outputStream().use {
|
destinationFile.outputStream().use {
|
||||||
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it)
|
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it)
|
||||||
}
|
}
|
||||||
|
}.onFailure {
|
||||||
|
return@withContext imageFile
|
||||||
}
|
}
|
||||||
|
|
||||||
destinationFile
|
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]
|
* Possible next worker : Always [MultipleEventSendingDispatcherWorker]
|
||||||
*/
|
*/
|
||||||
internal class UploadContentWorker(val context: Context, params: WorkerParameters) :
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class Params(
|
internal data class Params(
|
||||||
|
@ -81,6 +81,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
@Inject lateinit var fileService: DefaultFileService
|
@Inject lateinit var fileService: DefaultFileService
|
||||||
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
@Inject lateinit var cancelSendTracker: CancelSendTracker
|
||||||
@Inject lateinit var imageCompressor: ImageCompressor
|
@Inject lateinit var imageCompressor: ImageCompressor
|
||||||
|
@Inject lateinit var imageExitTagRemover: ImageExifTagRemover
|
||||||
@Inject lateinit var videoCompressor: VideoCompressor
|
@Inject lateinit var videoCompressor: VideoCompressor
|
||||||
@Inject lateinit var thumbnailExtractor: ThumbnailExtractor
|
@Inject lateinit var thumbnailExtractor: ThumbnailExtractor
|
||||||
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
@Inject lateinit var localEchoRepository: LocalEchoRepository
|
||||||
|
@ -114,7 +115,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
||||||
}
|
}
|
||||||
|
|
||||||
val attachment = params.attachment
|
val attachment = params.attachment
|
||||||
val filesToDelete = mutableListOf<File>()
|
val filesToDelete = hashSetOf<File>()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
|
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 {
|
} else {
|
||||||
fileToUpload = workingFile
|
fileToUpload = workingFile
|
||||||
// Fix: OpenableColumns.SIZE may return -1 or 0
|
// Fix: OpenableColumns.SIZE may return -1 or 0
|
||||||
|
|
Loading…
Reference in New Issue