From 6f7b4a6e4383858511c6862294bb0f7f0fcbca14 Mon Sep 17 00:00:00 2001 From: "Collin B. Stuart" Date: Wed, 3 Jan 2018 12:35:36 -0500 Subject: [PATCH] Strip media metadata. - removes non-orientation metadata from image and video attachments - option to disable the feature // FREEBIE --- .../PrivacySettingsTableViewController.m | 16 +++++ .../translations/en.lproj/Localizable.strings | 12 ++++ .../attachments/SignalAttachment.swift | 68 ++++++++++++++++++- SignalMessaging/utils/OWSPreferences.h | 3 + SignalMessaging/utils/OWSPreferences.m | 12 ++++ 5 files changed, 109 insertions(+), 2 deletions(-) diff --git a/Signal/src/ViewControllers/PrivacySettingsTableViewController.m b/Signal/src/ViewControllers/PrivacySettingsTableViewController.m index fd0b442cc..a9ebb6f94 100644 --- a/Signal/src/ViewControllers/PrivacySettingsTableViewController.m +++ b/Signal/src/ViewControllers/PrivacySettingsTableViewController.m @@ -67,6 +67,15 @@ NS_ASSUME_NONNULL_BEGIN target:weakSelf selector:@selector(didToggleScreenSecuritySwitch:)]]; [contents addSection:screenSecuritySection]; + + OWSTableSection *removeMetadataSection = [OWSTableSection new]; + removeMetadataSection.headerTitle = NSLocalizedString(@"SETTINGS_REMOVE_METADATA_TITLE", @"Remove metadata section header"); + removeMetadataSection.footerTitle = NSLocalizedString(@"SETTINGS_REMOVE_METADATA_DETAIL", @"Remove metadata section footer"); + [removeMetadataSection addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_REMOVE_METADATA", @"Remove metadata table cell label") + isOn:[Environment.preferences isRemoveMetadataEnabled] + target:weakSelf + selector:@selector(didToggleRemoveMetadataSwitch:)]]; + [contents addSection:removeMetadataSection]; // Allow calls to connect directly vs. using TURN exclusively OWSTableSection *callingSection = [OWSTableSection new]; @@ -192,6 +201,13 @@ NS_ASSUME_NONNULL_BEGIN [Environment.preferences setScreenSecurity:enabled]; } +- (void)didToggleRemoveMetadataSwitch:(UISwitch *)sender +{ + BOOL enabled = sender.isOn; + DDLogInfo(@"%@ toggled remove metadata: %@", self.logTag, enabled ? @"ON" : @"OFF"); + [Environment.preferences setIsRemoveMetadataEnabled:enabled]; +} + - (void)didToggleReadReceiptsSwitch:(UISwitch *)sender { BOOL enabled = sender.isOn; diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 3c3f3d8d3..827de08e1 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -118,6 +118,9 @@ /* Attachment error message for video attachments which could not be converted to MP4 */ "ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image."; + /* Attachment error message for image attachments which cannot be parsed */ "ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Image attachment could not be parsed."; @@ -1690,6 +1693,15 @@ /* No comment provided by engineer. */ "SETTINGS_SCREEN_SECURITY_DETAIL" = "Prevent Signal previews from appearing in the app switcher."; +/* Remove metadata table view header label. */ +"SETTINGS_REMOVE_METADATA_TITLE" = "Metadata"; + +/* Label for the remove metadata setting. */ +"SETTINGS_REMOVE_METADATA" = "Remove Media Metadata"; + +/* Footer label explanation of the remove metadata setting. */ +"SETTINGS_REMOVE_METADATA_DETAIL" = "Removes user-identifying metadata and GPS information when sending image and video messages."; + /* Settings table section footer. */ "SETTINGS_SECTION_CALL_KIT_DESCRIPTION" = "iOS Call Integration shows Signal calls on your lock screen and in the system's call history. You may optionally show your contact's name and number. If iCloud is enabled, this call history will be shared with Apple."; diff --git a/SignalMessaging/attachments/SignalAttachment.swift b/SignalMessaging/attachments/SignalAttachment.swift index 383510605..c51d5c8ba 100644 --- a/SignalMessaging/attachments/SignalAttachment.swift +++ b/SignalMessaging/attachments/SignalAttachment.swift @@ -15,6 +15,7 @@ enum SignalAttachmentError: Error { case couldNotParseImage case couldNotConvertToJpeg case couldNotConvertToMpeg4 + case couldNotRemoveMetadata case invalidFileFormat } @@ -53,6 +54,8 @@ extension SignalAttachmentError: LocalizedError { return NSLocalizedString("ATTACHMENT_ERROR_INVALID_FILE_FORMAT", comment: "Attachment error message for attachments with an invalid file format") case .couldNotConvertToMpeg4: return NSLocalizedString("ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4", comment: "Attachment error message for video attachments which could not be converted to MP4") + case .couldNotRemoveMetadata: + return NSLocalizedString("ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA", comment: "Attachment error message for image attachments in which metadata could not be removed") } } } @@ -625,8 +628,13 @@ public class SignalAttachment: NSObject { } if isValidOutput { - Logger.verbose("\(TAG) Sending raw \(attachment.mimeType)") - return attachment + if Environment.preferences().isRemoveMetadataEnabled() { + Logger.verbose("\(TAG) Rewriting attachment with metadata removed \(attachment.mimeType)") + return removeImageMetadata(attachment : attachment) + } else { + Logger.verbose("\(TAG) Sending raw \(attachment.mimeType)") + return attachment + } } else { Logger.verbose("\(TAG) Compressing attachment as image/jpeg, \(dataSource.dataLength()) bytes") return compressImageAsJPEG(image: image, attachment: attachment, filename: dataSource.sourceFilename, imageQuality: imageQuality) @@ -802,6 +810,59 @@ public class SignalAttachment: NSObject { return 0.5 } } + + private class func removeImageMetadata(attachment: SignalAttachment) -> SignalAttachment { + + guard let source = CGImageSourceCreateWithData(attachment.data as CFData, nil) else { + let attachment = SignalAttachment(dataSource : DataSourceValue.emptyDataSource(), dataUTI: attachment.dataUTI) + attachment.error = .missingData + return attachment + } + + guard let type = CGImageSourceGetType(source) else { + let attachment = SignalAttachment(dataSource : DataSourceValue.emptyDataSource(), dataUTI: attachment.dataUTI) + attachment.error = .invalidFileFormat + return attachment + } + + let count = CGImageSourceGetCount(source) + let mutableData = NSMutableData() + guard let destination = CGImageDestinationCreateWithData(mutableData as CFMutableData, type, count, nil) else { + attachment.error = .couldNotRemoveMetadata + return attachment + } + + let removeMetadataProperties : [String : AnyObject] = + [ + kCGImagePropertyExifDictionary as String : kCFNull, + kCGImagePropertyExifAuxDictionary as String : kCFNull, + kCGImagePropertyGPSDictionary as String : kCFNull, + kCGImagePropertyTIFFDictionary as String : kCFNull, + kCGImagePropertyJFIFDictionary as String : kCFNull, + kCGImagePropertyPNGDictionary as String : kCFNull, + kCGImagePropertyIPTCDictionary as String : kCFNull, + kCGImagePropertyMakerAppleDictionary as String : kCFNull + ] + + for index in 0...count-1 { + CGImageDestinationAddImageFromSource(destination, source, index, removeMetadataProperties as CFDictionary) + } + + if CGImageDestinationFinalize(destination) { + guard let dataSource = DataSourceValue.dataSource(with:mutableData as Data, utiType:attachment.dataUTI) else { + attachment.error = .couldNotRemoveMetadata + return attachment + } + + let strippedAttachment = SignalAttachment(dataSource : dataSource, dataUTI: attachment.dataUTI) + return strippedAttachment + + } else { + Logger.verbose("\(TAG) CGImageDestinationFinalize failed") + attachment.error = .couldNotRemoveMetadata + return attachment + } + } // MARK: Video Attachments @@ -863,6 +924,9 @@ public class SignalAttachment: NSObject { exportSession.shouldOptimizeForNetworkUse = true exportSession.outputFileType = AVFileTypeMPEG4 + if Environment.preferences().isRemoveMetadataEnabled() { + exportSession.metadataItemFilter = AVMetadataItemFilter.forSharing() + } let exportURL = videoTempPath.appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4") exportSession.outputURL = exportURL diff --git a/SignalMessaging/utils/OWSPreferences.h b/SignalMessaging/utils/OWSPreferences.h index fe3a9a333..504ae3fd7 100644 --- a/SignalMessaging/utils/OWSPreferences.h +++ b/SignalMessaging/utils/OWSPreferences.h @@ -44,6 +44,9 @@ extern NSString *const OWSPreferencesKeyEnableDebugLog; - (BOOL)screenSecurityIsEnabled; - (void)setScreenSecurity:(BOOL)flag; +- (BOOL)isRemoveMetadataEnabled; +- (void)setIsRemoveMetadataEnabled:(BOOL)enabled; + - (NotificationType)notificationPreviewType; - (void)setNotificationPreviewType:(NotificationType)type; - (NSString *)nameForNotificationPreviewType:(NotificationType)notificationType; diff --git a/SignalMessaging/utils/OWSPreferences.m b/SignalMessaging/utils/OWSPreferences.m index 73f44058b..90eee4df9 100644 --- a/SignalMessaging/utils/OWSPreferences.m +++ b/SignalMessaging/utils/OWSPreferences.m @@ -24,6 +24,7 @@ NSString *const OWSPreferencesKeyLastRecordedVoipToken = @"LastRecordedVoipToken NSString *const OWSPreferencesKeyCallKitEnabled = @"CallKitEnabled"; NSString *const OWSPreferencesKeyCallKitPrivacyEnabled = @"CallKitPrivacyEnabled"; NSString *const OWSPreferencesKeyCallsHideIPAddress = @"CallsHideIPAddress"; +NSString *const OWSPreferencesKeyRemoveMetadata = @"Remove Metadata Key"; NSString *const OWSPreferencesKeyHasDeclinedNoContactsView = @"hasDeclinedNoContactsView"; NSString *const OWSPreferencesKeyIOSUpgradeNagDate = @"iOSUpgradeNagDate"; NSString *const OWSPreferencesKey_IsReadyForAppExtensions = @"isReadyForAppExtensions_5"; @@ -116,6 +117,17 @@ NSString *const OWSPreferencesKeySystemCallLogEnabled = @"OWSPreferencesKeySyste [self setValueForKey:OWSPreferencesKeyScreenSecurity toValue:@(flag)]; } +- (BOOL)isRemoveMetadataEnabled +{ + NSNumber *preference = [self tryGetValueForKey:OWSPreferencesKeyRemoveMetadata]; + return preference ? [preference boolValue] : YES; +} + +- (void)setIsRemoveMetadataEnabled:(BOOL)enabled +{ + [self setValueForKey:OWSPreferencesKeyRemoveMetadata toValue:@(enabled)]; +} + - (BOOL)getHasSentAMessage { NSNumber *preference = [self tryGetValueForKey:OWSPreferencesKeyHasSentAMessage];