From a4a862f078aaad64c1500d9553e8554912b422f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Fri, 13 Dec 2024 10:41:34 +0100 Subject: [PATCH 01/17] Confidential View and Screenshot Protection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This update introduces screenshot protection for confidentially marked views in the app and the share sheet extension. When a screenshot is taken, confidential views will not display their content in the screenshot. To discourage taking photos of confidential views, these views now include a watermark displaying the account’s email and username. Screenshot protection can be enabled via an MDM parameter. Additionally, to prevent sharing confidential data when screenshot protection or confidential view marking is enabled, the following features are disabled: - File Provider access - Shortcuts Intents - System sharing dialog actions - Copying files - Text recognition on images This behavior can be overridden using an MDM parameter; however, overriding it is not recommended. --- ownCloud Intents/IntentSettings.swift | 5 + ownCloud.xcodeproj/project.pbxproj | 40 ++++- .../AppRootViewController+ItemActions.swift | 1 + .../Actions/EditDocumentViewController.swift | 1 + ownCloud/Client/ClientActivityCell.swift | 2 + ownCloud/UI Elements/ImageScrollView.swift | 2 + .../Confidential/ConfidentalManager.swift | 101 ++++++++++++ .../Confidential/ConfidentialManager.h | 42 +++++ .../Confidential/ConfidentialManager.m | 115 +++++++++++++ .../OCFileProviderSettings.m | 5 + ownCloudAppFramework/ownCloudApp.h | 2 + ownCloudAppShared/Client/Actions/Action.swift | 3 + .../Cells/DriveListCell.swift | 17 +- .../Cells/UniversalItemListCell.swift | 1 + .../CollectionViewController.swift | 6 +- .../Client/Sharing/ShareViewController.swift | 13 +- .../ClientItemViewController.swift | 4 +- .../ClientLocationPopupButton.swift | 4 +- .../OCLocation+Breadcrumbs.swift | 4 +- .../ClientLocationPicker.swift | 3 + .../String+Extension.swift | 13 ++ .../SDK Extensions/OCAction+UIAction.swift | 4 +- .../UIKit Extension/UIView+Extension.swift | 24 +++ .../BrowserNavigationViewController.swift | 11 +- .../ConfidentialContentView.swift | 154 ++++++++++++++++++ .../Confidential/SecureTextField.swift | 57 +++++++ .../User Interface/More/MoreViewHeader.swift | 24 ++- 27 files changed, 637 insertions(+), 21 deletions(-) create mode 100644 ownCloudAppFramework/Confidential/ConfidentalManager.swift create mode 100644 ownCloudAppFramework/Confidential/ConfidentialManager.h create mode 100644 ownCloudAppFramework/Confidential/ConfidentialManager.m create mode 100644 ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift create mode 100644 ownCloudAppShared/User Interface/Confidential/SecureTextField.swift diff --git a/ownCloud Intents/IntentSettings.swift b/ownCloud Intents/IntentSettings.swift index 99457bdee..434af6cb4 100644 --- a/ownCloud Intents/IntentSettings.swift +++ b/ownCloud Intents/IntentSettings.swift @@ -17,6 +17,7 @@ */ import ownCloudApp +import ownCloudAppShared import ownCloudSDK class IntentSettings: NSObject { @@ -29,6 +30,10 @@ class IntentSettings: NSObject { } var isEnabled : Bool { + if !ConfidentialManager.shared.allowOverwriteConfidentialMDMSettings { + return false + } + return (self.classSetting(forOCClassSettingsKey: .shortcutsEnabled) as? Bool) ?? true } diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index 581bb85fa..bbdec175c 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -93,6 +93,10 @@ 39CC8AE6228C12100020253B /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CC8AE5228C12100020253B /* Array+Extension.swift */; }; 39CD755423D8392D00193950 /* EditDocumentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CD755323D8392D00193950 /* EditDocumentViewController.swift */; }; 39D06BEC229BE8D8000D7FC9 /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D06BEB229BE8D8000D7FC9 /* SettingsSection.swift */; }; + 39D091AD2D073498001329DF /* ConfidentialContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D091AC2D073492001329DF /* ConfidentialContentView.swift */; }; + 39D091AF2D073593001329DF /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D091AE2D07358C001329DF /* SecureTextField.swift */; }; + 39D091B42D079640001329DF /* ConfidentialManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 39D091B32D07963B001329DF /* ConfidentialManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 39D091B62D079646001329DF /* ConfidentialManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 39D091B52D079644001329DF /* ConfidentialManager.m */; }; 39DC7CD025C2E1570001E08C /* DocumentActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39DC7CCF25C2E1570001E08C /* DocumentActionViewController.swift */; }; 39DC7CD325C2E1570001E08C /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 39DC7CD125C2E1570001E08C /* MainInterface.storyboard */; }; 39DC7CD725C2E1570001E08C /* ownCloud File Provider UI.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 39DC7CCD25C2E1570001E08C /* ownCloud File Provider UI.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -1091,6 +1095,10 @@ 39CD755123D787E400193950 /* DocumentEditingAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentEditingAction.swift; sourceTree = ""; }; 39CD755323D8392D00193950 /* EditDocumentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditDocumentViewController.swift; sourceTree = ""; }; 39D06BEB229BE8D8000D7FC9 /* SettingsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; }; + 39D091AC2D073492001329DF /* ConfidentialContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfidentialContentView.swift; sourceTree = ""; }; + 39D091AE2D07358C001329DF /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = ""; }; + 39D091B32D07963B001329DF /* ConfidentialManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConfidentialManager.h; sourceTree = ""; }; + 39D091B52D079644001329DF /* ConfidentialManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ConfidentialManager.m; sourceTree = ""; }; 39D8397F25A7219100FAA0A8 /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sq; path = sq.lproj/Intents.strings; sourceTree = ""; }; 39D8398925A7219300FAA0A8 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Intents.strings; sourceTree = ""; }; 39D8399325A7219600FAA0A8 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Intents.strings; sourceTree = ""; }; @@ -2114,6 +2122,24 @@ path = ownCloud/Resources/Theming; sourceTree = SOURCE_ROOT; }; + 39D091AB2D07347F001329DF /* Confidential */ = { + isa = PBXGroup; + children = ( + 39D091AE2D07358C001329DF /* SecureTextField.swift */, + 39D091AC2D073492001329DF /* ConfidentialContentView.swift */, + ); + path = Confidential; + sourceTree = ""; + }; + 39D091B02D0788D1001329DF /* Confidential */ = { + isa = PBXGroup; + children = ( + 39D091B52D079644001329DF /* ConfidentialManager.m */, + 39D091B32D07963B001329DF /* ConfidentialManager.h */, + ); + path = Confidential; + sourceTree = ""; + }; 39DC7CCE25C2E1570001E08C /* ownCloud File Provider UI */ = { isa = PBXGroup; children = ( @@ -2246,6 +2272,7 @@ DC0A35A024C1091400FB58FC /* UserInterfaceContext.swift */, DC2A68D629D4E93300BFF393 /* SharedKeyCommands.swift */, DCE4E43624C19A3B0051722F /* More */, + 39D091AB2D07347F001329DF /* Confidential */, DC0A359024C0E46800FB58FC /* Cursor Support */, DC0A355124C0E1EF00FB58FC /* Theme */, DC298C9B2934D3FA009FA87F /* Alert View */, @@ -3126,6 +3153,7 @@ DCC0855D2293F1FD008CC05C /* ownCloudAppFramework */ = { isa = PBXGroup; children = ( + 39D091B02D0788D1001329DF /* Confidential */, DCC0855E2293F1FD008CC05C /* ownCloudApp.h */, DC774E5A22F44E2A000B11A1 /* Display Settings */, DC6A0E4F26EA9E2B0076B533 /* AppLock Settings */, @@ -3778,6 +3806,7 @@ DCC5E4472326564F002E5B84 /* NSObject+AnnotatedProperties.h in Headers */, DC66F3AB23965C9C00CF4812 /* OCLicenseAppStoreReceiptInAppPurchase.h in Headers */, DC049156258C00C400DEDC27 /* OCFileProviderServiceStandby.h in Headers */, + 39D091B42D079640001329DF /* ConfidentialManager.h in Headers */, 3912D8AD29958BF400EDCB9A /* OCThemeValues.h in Headers */, DCFEFE972368D099009A142F /* OCLicenseEnvironment.h in Headers */, DC6A0E5426EA9E740076B533 /* AppLockSettings.h in Headers */, @@ -4096,7 +4125,7 @@ }; DCC0855B2293F1FD008CC05C = { CreatedOnToolsVersion = 10.2.1; - LastSwiftMigration = 1100; + LastSwiftMigration = 1600; ProvisioningStyle = Automatic; }; DCC085632293F1FD008CC05C = { @@ -4722,6 +4751,7 @@ DCB6B1ED292B963300D27573 /* AccountConnection+ItemActions.swift in Sources */, DCB6B200292CF2FE00D27573 /* NavigationRevocationAction.swift in Sources */, DC0A355424C0E2C200FB58FC /* SortBar.swift in Sources */, + 39D091AF2D073593001329DF /* SecureTextField.swift in Sources */, DC9B4FC3293F453C0037F8F8 /* EmbeddingViewController.swift in Sources */, DC298C972934D354009FA87F /* IssuesCardViewController.swift in Sources */, DCA2EDE2279B16F1001F04E6 /* ResourceSourceItemIcons.swift in Sources */, @@ -4771,6 +4801,7 @@ DCFC9ED3280023BB005D9144 /* CollectionViewCellProvider+StandardImplementations.swift in Sources */, DCB6B20A292E296800D27573 /* CollectionSidebarAction.swift in Sources */, DC0A358D24C0E44B00FB58FC /* ThemedAlertController.swift in Sources */, + 39D091AD2D073498001329DF /* ConfidentialContentView.swift in Sources */, DC24E10F28B7D2B9002E4F5B /* PopupButtonController.swift in Sources */, DCE4E43C24C19B660051722F /* FrameViewController.swift in Sources */, DC0A35A124C1091400FB58FC /* UserInterfaceContext.swift in Sources */, @@ -4946,6 +4977,7 @@ DCF575F02796CE38003BEBBA /* OCViewHost.m in Sources */, DCB330D729F07ED600BFF393 /* UIImage+ViewProvider.m in Sources */, DCB458EE2604A7D4006A02AB /* OCQueryCondition+SearchSegmenter.m in Sources */, + 39D091B62D079646001329DF /* ConfidentialManager.m in Sources */, DC080CF3238C92480044C5D2 /* OCLicenseAppStoreItem.m in Sources */, DCFEFE982368D099009A142F /* OCLicenseEnvironment.m in Sources */, DC049157258C00C400DEDC27 /* OCFileProviderServiceStandby.m in Sources */, @@ -5365,7 +5397,6 @@ 233BDEBA204FEFE500C06732 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ownCloud/ownCloud.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; @@ -5403,7 +5434,6 @@ 233BDEBB204FEFE500C06732 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ownCloud/ownCloud.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; @@ -5441,7 +5471,6 @@ 233BDEBD204FEFE600C06732 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 9B5WD74GWJ; @@ -5461,7 +5490,6 @@ 233BDEBE204FEFE600C06732 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 9B5WD74GWJ; @@ -5866,6 +5894,7 @@ SKIP_INSTALL = YES; STRINGS_FILE_OUTPUT_ENCODING = "UTF-16"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -5908,6 +5937,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; STRINGS_FILE_OUTPUT_ENCODING = "UTF-16"; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; diff --git a/ownCloud/App Controllers/AppRootViewController+ItemActions.swift b/ownCloud/App Controllers/AppRootViewController+ItemActions.swift index b1c2597bf..cd284ea96 100644 --- a/ownCloud/App Controllers/AppRootViewController+ItemActions.swift +++ b/ownCloud/App Controllers/AppRootViewController+ItemActions.swift @@ -31,6 +31,7 @@ extension AppRootViewController: ViewItemAction { let itemViewController = DisplayHostViewController(clientContext: context, selectedItem: item, queryDataSource: queryDatasource) itemViewController.hidesBottomBarWhenPushed = true itemViewController.progressSummarizer = context.progressSummarizer + itemViewController.view.secureView(core: context.core) return itemViewController } diff --git a/ownCloud/Client/Actions/EditDocumentViewController.swift b/ownCloud/Client/Actions/EditDocumentViewController.swift index ef4b8efa0..56b83f623 100644 --- a/ownCloud/Client/Actions/EditDocumentViewController.swift +++ b/ownCloud/Client/Actions/EditDocumentViewController.swift @@ -121,6 +121,7 @@ class EditDocumentViewController: QLPreviewController, Themeable { } } timer!.resume() + self.view.secureView(core: core) } @objc func enableEditingMode() { diff --git a/ownCloud/Client/ClientActivityCell.swift b/ownCloud/Client/ClientActivityCell.swift index ee7a90b3b..4a303de1f 100644 --- a/ownCloud/Client/ClientActivityCell.swift +++ b/ownCloud/Client/ClientActivityCell.swift @@ -95,6 +95,8 @@ class ClientActivityCell: ThemeTableViewCell { messageButton.topAnchor.constraint(equalTo: statusCircle.topAnchor), messageButton.bottomAnchor.constraint(equalTo: statusCircle.bottomAnchor) ]) + + self.secureView(core: core) } // MARK: - Message support diff --git a/ownCloud/UI Elements/ImageScrollView.swift b/ownCloud/UI Elements/ImageScrollView.swift index 999a6cbef..3b8028a99 100644 --- a/ownCloud/UI Elements/ImageScrollView.swift +++ b/ownCloud/UI Elements/ImageScrollView.swift @@ -19,6 +19,7 @@ import UIKit import VisionKit import ownCloudSDK +import ownCloudApp import ownCloudAppShared class ImageScrollView: UIScrollView { @@ -196,6 +197,7 @@ extension ImageScrollView { } var imageInteractionsAllowed: Bool { + guard ConfidentialManager.shared.allowOverwriteConfidentialMDMSettings else { return false } return Action.classSetting(forOCClassSettingsKey: .allowImageInteractions) as? Bool ?? true } } diff --git a/ownCloudAppFramework/Confidential/ConfidentalManager.swift b/ownCloudAppFramework/Confidential/ConfidentalManager.swift new file mode 100644 index 000000000..f64c233bd --- /dev/null +++ b/ownCloudAppFramework/Confidential/ConfidentalManager.swift @@ -0,0 +1,101 @@ +// +// ConfidentalManager.swift +// ownCloud +// +// Created by Matthias Hühne on 09.12.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import ownCloudSDK + +public class ConfidentalManager : NSObject { + public static var shared: ConfidentalManager = ConfidentalManager() +} + +public extension OCClassSettingsIdentifier { + static let confidential = OCClassSettingsIdentifier("confidential") +} + +public extension OCClassSettingsKey { + static let allowScreenshots = OCClassSettingsKey("allow-screenshots") + static let markConfidentalViews = OCClassSettingsKey("mark-confidental-views") + static let allowOverwriteConfidentalMDMSettings = OCClassSettingsKey("allow-overwrite-confidental-mdm-settings") +} + +extension ConfidentalManager : OCClassSettingsSupport { + public static let classSettingsIdentifier : OCClassSettingsIdentifier = .confidential + + + public var allowScreenshots: Bool { + return ConfidentalManager.classSetting(forOCClassSettingsKey: .allowScreenshots) as? Bool ?? true + } + + public var markConfidentalViews: Bool { + return ConfidentalManager.classSetting(forOCClassSettingsKey: .markConfidentalViews) as? Bool ?? true + } + + public var allowOverwriteConfidentalMDMSettings: Bool { + return (self.confidentalSettingsEnabled && (ConfidentalManager.classSetting(forOCClassSettingsKey: .allowOverwriteConfidentalMDMSettings) as? Bool ?? true)) + } + + public var confidentalSettingsEnabled: Bool { + if self.allowScreenshots || self.markConfidentalViews { + return true + } + + return false + } + + public var disallowedActions: [String]? { + if confidentalSettingsEnabled, !allowOverwriteConfidentalMDMSettings { + return ["com.owncloud.action.openin", "com.owncloud.action.copy", "com.owncloud.action.collaborate", "action.allow-image-interactions"] + } + + return nil + } + + public static func defaultSettings(forIdentifier identifier: OCClassSettingsIdentifier) -> [OCClassSettingsKey : Any]? { + if identifier == .confidential { + return [ + .allowScreenshots : false, + .markConfidentalViews : true, + .allowOverwriteConfidentalMDMSettings : false + ] + } + + return nil + } + + public static func classSettingsMetadata() -> [OCClassSettingsKey : [OCClassSettingsMetadataKey : Any]]? { + return [ + .allowScreenshots : [ + .type : OCClassSettingsMetadataType.boolean, + .description : "Controls whether screenshots are allowed or not. If not allowed confidental views will be marked as sensitive and are not visible in screenshots.", + .category : "Confidental", + .status : OCClassSettingsKeyStatus.debugOnly + ], + .markConfidentalViews : [ + .type : OCClassSettingsMetadataType.boolean, + .description : "Controls if views which contains sensitive content contains a watermark or not.", + .category : "Confidental", + .status : OCClassSettingsKeyStatus.debugOnly + ], + .allowOverwriteConfidentalMDMSettings : [ + .type : OCClassSettingsMetadataType.boolean, + .description : "Controls if confidental related MDM settings can be overwritten.", + .category : "Confidental", + .status : OCClassSettingsKeyStatus.debugOnly + ] + ] + } +} diff --git a/ownCloudAppFramework/Confidential/ConfidentialManager.h b/ownCloudAppFramework/Confidential/ConfidentialManager.h new file mode 100644 index 000000000..d9e08488b --- /dev/null +++ b/ownCloudAppFramework/Confidential/ConfidentialManager.h @@ -0,0 +1,42 @@ +// +// ConfidentialManager.h +// ownCloud +// +// Created by Matthias Hühne on 09.12.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ConfidentialManager : NSObject + +@property(class,strong,nonatomic,readonly) ConfidentialManager *sharedConfidentialManager; + +@property (assign, readonly) BOOL allowScreenshots; +@property (assign, readonly) BOOL markConfidentialViews; +@property (assign, readonly) BOOL allowOverwriteConfidentialMDMSettings; +@property (assign, readonly) BOOL confidentialSettingsEnabled; +@property (nonatomic, readonly, nullable) NSArray *disallowedActions; + +@end + +extern OCClassSettingsIdentifier OCClassSettingsIdentifierConfidential; + +extern OCClassSettingsKey OCClassSettingsKeyAllowScreenshots; +extern OCClassSettingsKey OCClassSettingsKeyMarkConfidentialViews; +extern OCClassSettingsKey OCClassSettingsKeyAllowOverwriteConfidentialMDMSettings; + +NS_ASSUME_NONNULL_END diff --git a/ownCloudAppFramework/Confidential/ConfidentialManager.m b/ownCloudAppFramework/Confidential/ConfidentialManager.m new file mode 100644 index 000000000..97063101c --- /dev/null +++ b/ownCloudAppFramework/Confidential/ConfidentialManager.m @@ -0,0 +1,115 @@ +// +// ConfidentialManager.m +// ownCloud +// +// Created by Matthias Hühne on 09.12.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +#import "ConfidentialManager.h" + +@implementation ConfidentialManager + ++ (instancetype)sharedConfidentialManager +{ + static dispatch_once_t onceToken; + static ConfidentialManager *sharedInstance; + + dispatch_once(&onceToken, ^{ + sharedInstance = [ConfidentialManager new]; + }); + + return (sharedInstance); +} + +#pragma mark - Class settings + ++ (OCClassSettingsIdentifier)classSettingsIdentifier +{ + return (OCClassSettingsIdentifierConfidential); +} + +- (BOOL)allowScreenshots { + id value = [ConfidentialManager classSettingForOCClassSettingsKey:OCClassSettingsKeyAllowScreenshots]; + return value ? [value boolValue] : YES; +} + +- (BOOL)markConfidentialViews { + id value = [ConfidentialManager classSettingForOCClassSettingsKey:OCClassSettingsKeyMarkConfidentialViews]; + return value ? [value boolValue] : YES; +} + +- (BOOL)allowOverwriteConfidentialMDMSettings { + BOOL confidentialSettingsEnabled = self.confidentialSettingsEnabled; + id value = [ConfidentialManager classSettingForOCClassSettingsKey:OCClassSettingsKeyAllowOverwriteConfidentialMDMSettings]; + return confidentialSettingsEnabled && (value ? [value boolValue] : YES); +} + +- (BOOL)confidentialSettingsEnabled { + return self.allowScreenshots || self.markConfidentialViews; +} + +- (NSArray *)disallowedActions { + if (self.confidentialSettingsEnabled && !self.allowOverwriteConfidentialMDMSettings) { + return @[ + @"com.owncloud.action.openin", + @"com.owncloud.action.copy", + @"action.allow-image-interactions" + ]; + } + return nil; +} + ++ (NSDictionary *)defaultSettingsForIdentifier:(OCClassSettingsIdentifier)identifier +{ + if ([identifier isEqual:OCClassSettingsIdentifierConfidential]) { + return @{ + OCClassSettingsKeyAllowScreenshots : @NO, + OCClassSettingsKeyMarkConfidentialViews : @YES, + OCClassSettingsKeyAllowOverwriteConfidentialMDMSettings : @NO + }; + } + return nil; +} + ++ (OCClassSettingsMetadataCollection)classSettingsMetadata +{ + return (@{ + OCClassSettingsKeyAllowScreenshots : @{ + @"type" : @"boolean", + @"description" : @"Controls whether screenshots are allowed or not. If not allowed confidential views will be marked as sensitive and are not visible in screenshots.", + @"category" : @"Confidential", + @"status" : @"debugOnly" + }, + OCClassSettingsKeyMarkConfidentialViews : @{ + @"type" : @"boolean", + @"description" : @"Controls if views which contains sensitive content contains a watermark or not.", + @"category" : @"Confidential", + @"status" : @"debugOnly" + }, + OCClassSettingsKeyAllowOverwriteConfidentialMDMSettings : @{ + @"type" : @"boolean", + @"description" : @"Controls if confidential related MDM settings can be overwritten.", + @"category" : @"Confidential", + @"status" : @"debugOnly" + } + }); +} + +@end + +OCClassSettingsIdentifier OCClassSettingsIdentifierConfidential = @"confidential"; + +OCClassSettingsKey OCClassSettingsKeyAllowScreenshots = @"allow-screenshots"; +OCClassSettingsKey OCClassSettingsKeyMarkConfidentialViews = @"mark-confidential-views"; +OCClassSettingsKey OCClassSettingsKeyAllowOverwriteConfidentialMDMSettings = @"allow-overwrite-confidential-mdm-settings"; diff --git a/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m b/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m index 88869cd39..7def15b3c 100644 --- a/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m +++ b/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m @@ -17,6 +17,7 @@ */ #import "OCFileProviderSettings.h" +#import "ownCloudApp/ConfidentialManager.h" @implementation OCFileProviderSettings @@ -46,6 +47,10 @@ + (OCClassSettingsMetadataCollection)classSettingsMetadata + (BOOL)browseable { + if ([[ConfidentialManager sharedConfidentialManager] allowOverwriteConfidentialMDMSettings] == false) { + return false; + } + return ([([self classSettingForOCClassSettingsKey:OCClassSettingsKeyFileProviderBrowseable]) boolValue]); } diff --git a/ownCloudAppFramework/ownCloudApp.h b/ownCloudAppFramework/ownCloudApp.h index 03b3192e1..20a706bbc 100644 --- a/ownCloudAppFramework/ownCloudApp.h +++ b/ownCloudAppFramework/ownCloudApp.h @@ -95,3 +95,5 @@ FOUNDATION_EXPORT const unsigned char ownCloudAppVersionString[]; #import #import + +#import diff --git a/ownCloudAppShared/Client/Actions/Action.swift b/ownCloudAppShared/Client/Actions/Action.swift index d0145360d..dd4c0099d 100644 --- a/ownCloudAppShared/Client/Actions/Action.swift +++ b/ownCloudAppShared/Client/Actions/Action.swift @@ -311,6 +311,9 @@ open class Action : NSObject { disallowedActions.contains(actionIdentifier.rawValue) { return .noMatch } + if let disallowedActions = ConfidentialManager.shared.disallowedActions, disallowedActions.contains(actionIdentifier.rawValue) { + return .noMatch + } } if self.applicablePosition(forContext: actionContext) == .none { diff --git a/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift index 15fa44bac..4fa84e2f3 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift @@ -139,6 +139,10 @@ extension DriveListCell { if let coverImageRequest = coverImageRequest { resourceManager?.start(coverImageRequest) } + + if let clientItemViewController = collectionItemRef.ocCellConfiguration?.hostViewController as? CollectionViewController { + cell.secureView(core: clientItemViewController.clientContext?.core) + } cell.accessories = [ .disclosureIndicator() ] } @@ -168,6 +172,10 @@ extension DriveListCell { cell.collectionItemRef = collectionItemRef cell.collectionViewController = collectionItemRef.ocCellConfiguration?.hostViewController + + if let clientItemViewController = collectionItemRef.ocCellConfiguration?.hostViewController as? CollectionViewController { + cell.secureView(core: clientItemViewController.clientContext?.core) + } if let coverImageRequest = coverImageRequest { resourceManager?.start(coverImageRequest) @@ -213,6 +221,10 @@ extension DriveListCell { cell.collectionItemRef = collectionItemRef cell.collectionViewController = collectionItemRef.ocCellConfiguration?.hostViewController + + if let clientItemViewController = collectionItemRef.ocCellConfiguration?.hostViewController as? CollectionViewController { + cell.secureView(core: clientItemViewController.clientContext?.core) + } if let coverImageRequest = coverImageRequest { resourceManager?.start(coverImageRequest) @@ -247,13 +259,12 @@ extension DriveListCell { var content = cell.defaultContentConfiguration() - content.text = title + content.text = title?.redacted() content.image = icon - + cell.backgroundConfiguration = UIBackgroundConfiguration.listSidebarCell() cell.contentConfiguration = content cell.applyThemeCollection(theme: Theme.shared, collection: Theme.shared.activeCollection, event: .initial) - cell.accessibilityTraits = .button } diff --git a/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift index a84dd300e..89d253bda 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift @@ -654,6 +654,7 @@ open class UniversalItemListCell: ThemeableCollectionViewListCell { } self.accessibilityLabel = accessibilityLabelContent + self.secureView(core: clientContext?.core) } } diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift index dd404df75..a745eb40c 100644 --- a/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift @@ -167,10 +167,14 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, public override func loadView() { super.loadView() + let secureView = SecureTextField().secureContainerView + + view.embed(toFillWith: secureView, enclosingAnchors: compressForKeyboard ? view.safeAreaWithKeyboardAnchorSet : view.safeAreaAnchorSet) + if usesStackViewRoot { createStackView() if let stackView { - view.embed(toFillWith: stackView, enclosingAnchors: compressForKeyboard ? view.safeAreaWithKeyboardAnchorSet : view.safeAreaAnchorSet) + secureView.embed(toFillWith: stackView, enclosingAnchors: compressForKeyboard ? view.safeAreaWithKeyboardAnchorSet : view.safeAreaAnchorSet) } } } diff --git a/ownCloudAppShared/Client/Sharing/ShareViewController.swift b/ownCloudAppShared/Client/Sharing/ShareViewController.swift index f663a80fb..920a77698 100644 --- a/ownCloudAppShared/Client/Sharing/ShareViewController.swift +++ b/ownCloudAppShared/Client/Sharing/ShareViewController.swift @@ -259,7 +259,18 @@ open class ShareViewController: CollectionViewController, SearchViewControllerDe open override func viewDidLoad() { super.viewDidLoad() - + + let secureView = SecureTextField().secureContainerView + secureView.addSubview(collectionView) + view.addSubview(secureView) + + NSLayoutConstraint.activate([ + secureView.topAnchor.constraint(equalTo: view.topAnchor), + secureView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + secureView.leadingAnchor.constraint(equalTo: view.leadingAnchor).with(priority: .defaultHigh), + secureView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + // Disable dragging of items, so keyboard control does // not include "Drag Item" in the accessibility actions // invoked with Tab + Z diff --git a/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift b/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift index 2bba79f2d..4c3a27c17 100644 --- a/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift +++ b/ownCloudAppShared/Client/View Controllers/ClientItemViewController.swift @@ -725,9 +725,9 @@ open class ClientItemViewController: CollectionViewController, SortBarDelegate, if navigationLocation == nil || !useNavigationLocationBreadcrumbDropdown || navigationLocation?.isRoot == true { if let navigationTitle { - self.navigationTitle = navigationTitle + self.navigationTitle = navigationTitle.redacted() } else { - self.navigationTitle = navigationItem.title + self.navigationTitle = navigationItem.title?.redacted() } } } diff --git a/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/ClientLocationPopupButton.swift b/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/ClientLocationPopupButton.swift index 03ffaa528..14a4f2904 100644 --- a/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/ClientLocationPopupButton.swift +++ b/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/ClientLocationPopupButton.swift @@ -48,7 +48,7 @@ open class ClientLocationPopupButton: ThemeCSSButton { if let clientContext = self?.clientContext, let breadcrumbs = breadcrumbLocation?.breadcrumbs(in: clientContext, includeServerName: false, action: .reveal).reversed() { for crumbAction in breadcrumbs { - menuItems.append(crumbAction.uiAction()) + menuItems.append(crumbAction.uiAction(redacted: true)) } } @@ -66,7 +66,7 @@ open class ClientLocationPopupButton: ThemeCSSButton { } func updateButton() { - let title = location?.displayName(in: clientContext) ?? "-" + let title = location?.displayName(in: clientContext).redacted() ?? "-" let attributedTitle = AttributedString(NSAttributedString(string: title, attributes: [.font : UIFont.systemFont(ofSize: UIFont.buttonFontSize, weight: .semibold)])) let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 0.7 * UIFont.buttonFontSize) let chevronBackgroundColor = Theme.shared.activeThemeCSS.getColor(.fill, selectors: [.popupButton, .icon], for: self) ?? .lightGray diff --git a/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/OCLocation+Breadcrumbs.swift b/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/OCLocation+Breadcrumbs.swift index 6ad2b90eb..d44c09138 100644 --- a/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/OCLocation+Breadcrumbs.swift +++ b/ownCloudAppShared/Client/View Controllers/Location Breadcrumbs/OCLocation+Breadcrumbs.swift @@ -218,8 +218,10 @@ extension OCLocation { seperatorSegment.insets.trailing = 0 segments.append(seperatorSegment) } + + let title = (breadcrumb.properties[.location] as? OCLocation) != nil ? breadcrumb.title.redacted() : breadcrumb.title - let segment = SegmentViewItem(with: breadcrumb.icon, title: breadcrumb.title, style: .plain, titleTextStyle: .footnote) + let segment = SegmentViewItem(with: breadcrumb.icon, title: title, style: .plain, titleTextStyle: .footnote) if let segmentConfigurator { segmentConfigurator(breadcrumb, segment) diff --git a/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift index 9626eb977..d2112883e 100644 --- a/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift +++ b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift @@ -172,6 +172,9 @@ public class ClientLocationPicker : NSObject { } allowedLocationFilter = { (targetLocation, context) in + OnMainThread { + self.headerView?.secureView(core: context?.core) + } // Disallow all paths as target that are parent of any of the items if itemParentLocations.contains(targetLocation) { return false diff --git a/ownCloudAppShared/Foundation Extensions/String+Extension.swift b/ownCloudAppShared/Foundation Extensions/String+Extension.swift index 4e200ccc4..ba9734b79 100644 --- a/ownCloudAppShared/Foundation Extensions/String+Extension.swift +++ b/ownCloudAppShared/Foundation Extensions/String+Extension.swift @@ -19,6 +19,7 @@ import Foundation import UIKit import ownCloudSDK +import ownCloudApp private let _sharedAppBundle = Bundle(identifier: "com.owncloud.ownCloudAppShared") @@ -74,4 +75,16 @@ extension String { return ceil(boundingBox.width) } + + /// Redacts the string with a specified character. + /// - Parameter replacement: The character to use for redaction (default is `•`). + /// - Returns: A new string where each character is replaced with the redaction character. + public func redacted(after visibleCount: Int = 3, with replacement: Character = "•") -> String { + guard ConfidentialManager.shared.markConfidentialViews else { return self } + + guard self.count > visibleCount else { return self } + let visiblePart = self.prefix(visibleCount) // First `visibleCount` characters + let redactedPart = String(repeating: replacement, count: self.count - visibleCount) + return visiblePart + redactedPart + } } diff --git a/ownCloudAppShared/SDK Extensions/OCAction+UIAction.swift b/ownCloudAppShared/SDK Extensions/OCAction+UIAction.swift index a716e1a51..fc9d6d683 100644 --- a/ownCloudAppShared/SDK Extensions/OCAction+UIAction.swift +++ b/ownCloudAppShared/SDK Extensions/OCAction+UIAction.swift @@ -20,8 +20,8 @@ import UIKit import ownCloudSDK public extension OCAction { - func uiAction(with options: [OCActionRunOptionKey : Any]? = nil) -> UIAction { - return UIAction(title: title, image: icon, attributes: [], handler: { action in + func uiAction(with options: [OCActionRunOptionKey : Any]? = nil, redacted: Bool = false) -> UIAction { + return UIAction(title: redacted ? title.redacted() : title, image: icon, attributes: [], handler: { action in self.run(options: options) }) } diff --git a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift index b281fd66f..8c0553b8a 100644 --- a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift @@ -17,6 +17,7 @@ */ import UIKit +import ownCloudSDK public extension UIView { // MARK: - Animation @@ -76,4 +77,27 @@ public extension UIView { return hostViewController } + + func secureView(core: OCCore?) { + let overlayView = ConfidentialContentView() + overlayView.text = core?.bookmark.user?.emailAddress ?? "Confidential View" + overlayView.subtext = core?.bookmark.userName ?? "Confidential View" + overlayView.backgroundColor = .clear + overlayView.translatesAutoresizingMaskIntoConstraints = false + + if self.frame.height <= 200 { + overlayView.angle = 20 + self.insertSubview(overlayView, aboveSubview: self) + } else { + overlayView.angle = 45 + self.addSubview(overlayView) + } + + NSLayoutConstraint.activate([ + overlayView.topAnchor.constraint(equalTo: self.topAnchor), + overlayView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + overlayView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + overlayView.trailingAnchor.constraint(equalTo: self.trailingAnchor) + ]) + } } diff --git a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift index 65e537bd4..eff95c909 100644 --- a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift @@ -70,7 +70,10 @@ open class BrowserNavigationViewController: EmbeddingViewController, Themeable, contentContainerView.cssSelector = .content contentContainerView.translatesAutoresizingMaskIntoConstraints = false contentContainerView.focusGroupIdentifier = "com.owncloud.content" - view.addSubview(contentContainerView) + + let secureView = SecureTextField().secureContainerView + secureView.addSubview(contentContainerView) + view.addSubview(secureView) navigationView.translatesAutoresizingMaskIntoConstraints = false navigationView.delegate = self @@ -89,6 +92,12 @@ open class BrowserNavigationViewController: EmbeddingViewController, Themeable, navigationBarTopConstraint = navigationBarTopConstraint(for: navigationBarHidden) NSLayoutConstraint.activate([ + + secureView.topAnchor.constraint(equalTo: view.topAnchor), + secureView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + secureView.leadingAnchor.constraint(equalTo: view.leadingAnchor).with(priority: .defaultHigh), + secureView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + contentContainerView.topAnchor.constraint(equalTo: view.topAnchor), contentContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), contentContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor).with(priority: .defaultHigh), // Allow for flexibility without having to remove this constraint. It will be overridden by constraints with higher priority (default is .required) when necessary diff --git a/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift b/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift new file mode 100644 index 000000000..1573ee2f2 --- /dev/null +++ b/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift @@ -0,0 +1,154 @@ +// +// ConfidentialContentView.swift +// ownCloud +// +// Created by Matthias Hühne on 09.12.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ + +import ownCloudSDK +import ownCloudApp + +public class ConfidentialContentView: UIView { + + var text: String = "Confidential Content" { + didSet { + setNeedsDisplay() + } + } + var subtext: String = "Confidential Content" { + didSet { + setNeedsDisplay() + } + } + var textColor: UIColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.button], for: nil)?.withAlphaComponent(0.7) ?? .red { + didSet { + setNeedsDisplay() + } + } + var font: UIFont = UIFont.systemFont(ofSize: 14) { + didSet { + setNeedsDisplay() + } + } + var angle: CGFloat = 45 { + didSet { + setNeedsDisplay() + } + } + var columnSpacing: CGFloat = 50 { + didSet { + setNeedsDisplay() + } + } + var lineSpacing: CGFloat = 40 { + didSet { + setNeedsDisplay() + } + } + + private var drawn: Bool = false + + override init(frame: CGRect) { + super.init(frame: frame) + setupOrientationObserver() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupOrientationObserver() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + private func setupOrientationObserver() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleOrientationChange), + name: UIDevice.orientationDidChangeNotification, + object: nil + ) + } + + @objc private func handleOrientationChange() { + drawn = false + setNeedsDisplay() + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for subview in subviews.reversed() { + let subviewPoint = convert(point, to: subview) + if let hitView = subview.hitTest(subviewPoint, with: event) { + return hitView + } + } + return nil // Allow touches to pass through + } + + public override func draw(_ rect: CGRect) { + guard ConfidentialManager.shared.markConfidentialViews, let context = UIGraphicsGetCurrentContext() else { return } + + drawn = true + + context.saveGState() + + let radians = angle * .pi / 180 + context.rotate(by: radians) + + let textAttributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: textColor + ] + let subtextAttributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: textColor + ] + + let textSize = text.size(withAttributes: textAttributes) + let subtextSize = subtext.size(withAttributes: subtextAttributes) + + let stepX = textSize.width + columnSpacing + let stepY = textSize.height + lineSpacing + + let stepSubtextX = subtextSize.width + columnSpacing + let stepSubTextY = subtextSize.height + lineSpacing + + let rotatedDiagonal = sqrt(rect.width * rect.width + rect.height * rect.height) + + let startX = -rotatedDiagonal + let startY = -rotatedDiagonal + let endX = rotatedDiagonal + let endY = rotatedDiagonal + + var y = startY + while y <= endY { + var x = startX + var col = 0 + while x <= endX { + if col % 2 == 0 { + text.draw(at: CGPoint(x: x, y: y), withAttributes: textAttributes) + x += stepX + } else { + subtext.draw(at: CGPoint(x: x, y: y + (stepSubTextY / 2)), withAttributes: subtextAttributes) + x += stepSubtextX + } + col += 1 + } + y += stepY + } + + context.restoreGState() + } +} diff --git a/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift b/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift new file mode 100644 index 000000000..af60fba21 --- /dev/null +++ b/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift @@ -0,0 +1,57 @@ +// +// SecureTextField.swift +// ownCloud +// +// Created by Matthias Hühne on 09.12.24. +// Copyright © 2024 ownCloud GmbH. All rights reserved. +// + +/* + * Copyright (C) 2024, ownCloud GmbH. + * + * This code is covered by the GNU Public License Version 3. + * + * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ + * You should have received a copy of this license along with this program. If not, see . + * + */ +import UIKit +import ownCloudApp + +class SecureTextField : UITextField { + + override init(frame: CGRect) { + super.init(frame: .zero) + self.isSecureTextEntry = true + self.translatesAutoresizingMaskIntoConstraints = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var secureContainerView: UIView { + guard !ConfidentialManager.shared.allowScreenshots else { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + } + + if let secureView = self.subviews.filter({ subview in + type(of: subview).description().contains("CanvasView") + }).first { + secureView.translatesAutoresizingMaskIntoConstraints = false + secureView.isUserInteractionEnabled = true + return secureView + } + + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + } + + override var canBecomeFirstResponder: Bool { false } + override func becomeFirstResponder() -> Bool { false } +} diff --git a/ownCloudAppShared/User Interface/More/MoreViewHeader.swift b/ownCloudAppShared/User Interface/More/MoreViewHeader.swift index 9aaea4bd3..8c12d00a4 100644 --- a/ownCloudAppShared/User Interface/More/MoreViewHeader.swift +++ b/ownCloudAppShared/User Interface/More/MoreViewHeader.swift @@ -86,6 +86,12 @@ open class MoreViewHeader: UIView { private func render() { cssSelectors = [.more, .header] + + let secureView = SecureTextField().secureContainerView + let contentContainerView = UIView() + contentContainerView.translatesAutoresizingMaskIntoConstraints = false + secureView.addSubview(contentContainerView) + self.addSubview(secureView) titleLabel.translatesAutoresizingMaskIntoConstraints = false detailLabel.translatesAutoresizingMaskIntoConstraints = false @@ -104,8 +110,18 @@ open class MoreViewHeader: UIView { titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) detailLabel.setContentCompressionResistancePriority(.required, for: .vertical) labelContainerView.setContentCompressionResistancePriority(.required, for: .vertical) - + + NSLayoutConstraint.activate([ + secureView.topAnchor.constraint(equalTo: self.topAnchor), + secureView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + secureView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + secureView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + + contentContainerView.topAnchor.constraint(equalTo: self.topAnchor), + contentContainerView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + contentContainerView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + titleLabel.leadingAnchor.constraint(equalTo: labelContainerView.leadingAnchor), titleLabel.trailingAnchor.constraint(equalTo: labelContainerView.trailingAnchor), titleLabel.topAnchor.constraint(equalTo: labelContainerView.topAnchor), @@ -116,8 +132,8 @@ open class MoreViewHeader: UIView { detailLabel.bottomAnchor.constraint(equalTo: labelContainerView.bottomAnchor) ]) - self.addSubview(iconView) - self.addSubview(labelContainerView) + contentContainerView.addSubview(iconView) + contentContainerView.addSubview(labelContainerView) NSLayoutConstraint.activate([ iconView.widthAnchor.constraint(equalToConstant: thumbnailSize.width), @@ -211,6 +227,8 @@ open class MoreViewHeader: UIView { core?.vault.resourceManager?.start(iconRequest) titleLabel.numberOfLines = 0 + + self.secureView(core: core) } public func updateHeader(title: String, subtitle: String) { From 3c41ebeed2c45a1a3bf819d22df4f13f59a15a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Fri, 13 Dec 2024 17:31:35 +0100 Subject: [PATCH 02/17] - Fixed metadata values for MDM documentation generation - Adjusted watermark angle for improved alignment - Updated GitHub Action workflow: - Fetch the latest available OS version - Retrieve iPhone simulator device for the selected OS version --- .github/workflows/build-and-analyze.yml | 35 +++++++++++++++---- .../Confidential/ConfidentialManager.m | 18 +++++----- .../UIKit Extension/UIView+Extension.swift | 2 +- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-and-analyze.yml b/.github/workflows/build-and-analyze.yml index ea3e67e0a..06ef5258b 100644 --- a/.github/workflows/build-and-analyze.yml +++ b/.github/workflows/build-and-analyze.yml @@ -27,11 +27,34 @@ jobs: scheme: ${{ 'ownCloud' }} platform: ${{ 'iOS Simulator' }} run: | - # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) - device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` - if [ $scheme = default ]; then scheme=$(cat default); fi - if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi - file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` - xcodebuild clean build analyze -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" | xcpretty && exit ${PIPESTATUS[0]} + set -e # Exit immediately on any error + # Get the latest iOS version + latest_ios_version=$(xcrun simctl list devices | grep -oE '^-- iOS [0-9.]+' | awk '{print $3}' | sort -r | head -1) + # Get the first available iPhone for that iOS version + device=$(xcrun simctl list devices | grep -A 1 "^-- iOS $latest_ios_version" | grep -oE 'iPhone [^()]*' | head -1) + + # Set scheme if it's default + if [ "$scheme" = "default" ]; then + scheme=$(cat default) + fi + + # Find the Xcode workspace or project + if ls *.xcworkspace >/dev/null 2>&1; then + filetype_parameter="workspace" + file_to_build=$(find . -maxdepth 1 -name "*.xcworkspace" | head -1) + elif ls *.xcodeproj >/dev/null 2>&1; then + filetype_parameter="project" + file_to_build=$(find . -maxdepth 1 -name "*.xcodeproj" | head -1) + else + echo "Error: No .xcworkspace or .xcodeproj found!" + exit 1 + fi + + # Clean up the file name + file_to_build=$(echo "$file_to_build" | awk '{$1=$1;print}') + + # Run xcodebuild with xcpretty + xcodebuild clean build analyze -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=iOS Simulator,name=$device,OS=$latest_ios_version" | xcpretty + exit ${PIPESTATUS[0]} diff --git a/ownCloudAppFramework/Confidential/ConfidentialManager.m b/ownCloudAppFramework/Confidential/ConfidentialManager.m index 97063101c..546cb8459 100644 --- a/ownCloudAppFramework/Confidential/ConfidentialManager.m +++ b/ownCloudAppFramework/Confidential/ConfidentialManager.m @@ -86,22 +86,22 @@ + (OCClassSettingsMetadataCollection)classSettingsMetadata { return (@{ OCClassSettingsKeyAllowScreenshots : @{ - @"type" : @"boolean", + @"type" : OCClassSettingsMetadataTypeBoolean, @"description" : @"Controls whether screenshots are allowed or not. If not allowed confidential views will be marked as sensitive and are not visible in screenshots.", - @"category" : @"Confidential", - @"status" : @"debugOnly" + @"status" : OCClassSettingsKeyStatusAdvanced, + @"category" : @"Confidential" }, OCClassSettingsKeyMarkConfidentialViews : @{ - @"type" : @"boolean", + @"type" : OCClassSettingsMetadataTypeBoolean, @"description" : @"Controls if views which contains sensitive content contains a watermark or not.", - @"category" : @"Confidential", - @"status" : @"debugOnly" + @"status" : OCClassSettingsKeyStatusAdvanced, + @"category" : @"Confidential" }, OCClassSettingsKeyAllowOverwriteConfidentialMDMSettings : @{ - @"type" : @"boolean", + @"type" : OCClassSettingsMetadataTypeBoolean, @"description" : @"Controls if confidential related MDM settings can be overwritten.", - @"category" : @"Confidential", - @"status" : @"debugOnly" + @"status" : OCClassSettingsKeyStatusAdvanced, + @"category" : @"Confidential" } }); } diff --git a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift index 8c0553b8a..112554b03 100644 --- a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift @@ -86,7 +86,7 @@ public extension UIView { overlayView.translatesAutoresizingMaskIntoConstraints = false if self.frame.height <= 200 { - overlayView.angle = 20 + overlayView.angle = 15 self.insertSubview(overlayView, aboveSubview: self) } else { overlayView.angle = 45 From 7e1b727fb08db1e9c13ebcda221743e4617d289d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Fri, 13 Dec 2024 17:46:54 +0100 Subject: [PATCH 03/17] Added architecture parameter to xcodebuild --- .github/workflows/build-and-analyze.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-analyze.yml b/.github/workflows/build-and-analyze.yml index 06ef5258b..dd1c6273e 100644 --- a/.github/workflows/build-and-analyze.yml +++ b/.github/workflows/build-and-analyze.yml @@ -56,5 +56,5 @@ jobs: file_to_build=$(echo "$file_to_build" | awk '{$1=$1;print}') # Run xcodebuild with xcpretty - xcodebuild clean build analyze -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=iOS Simulator,name=$device,OS=$latest_ios_version" | xcpretty + xcodebuild clean build analyze -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=iOS Simulator,name=$device,OS=$latest_ios_version" ARCHS=x86_64 | xcpretty exit ${PIPESTATUS[0]} From cadff6c297d040309f6fab433decb1503c314b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Mon, 16 Dec 2024 14:56:02 +0100 Subject: [PATCH 04/17] feat: update confidential MDM settings and UI enhancements - Updated default values for confidential MDM settings - Adjusted rotation angle for confidential text - Added a subtitle to small confidential views and modified text color opacity --- ownCloudAppFramework/Confidential/ConfidentialManager.m | 4 ++-- ownCloudAppShared/UIKit Extension/UIView+Extension.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ownCloudAppFramework/Confidential/ConfidentialManager.m b/ownCloudAppFramework/Confidential/ConfidentialManager.m index 546cb8459..4c1c64e28 100644 --- a/ownCloudAppFramework/Confidential/ConfidentialManager.m +++ b/ownCloudAppFramework/Confidential/ConfidentialManager.m @@ -74,8 +74,8 @@ - (BOOL)confidentialSettingsEnabled { { if ([identifier isEqual:OCClassSettingsIdentifierConfidential]) { return @{ - OCClassSettingsKeyAllowScreenshots : @NO, - OCClassSettingsKeyMarkConfidentialViews : @YES, + OCClassSettingsKeyAllowScreenshots : @YES, + OCClassSettingsKeyMarkConfidentialViews : @NO, OCClassSettingsKeyAllowOverwriteConfidentialMDMSettings : @NO }; } diff --git a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift index 112554b03..47f233caf 100644 --- a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift @@ -86,7 +86,7 @@ public extension UIView { overlayView.translatesAutoresizingMaskIntoConstraints = false if self.frame.height <= 200 { - overlayView.angle = 15 + overlayView.angle = 10 self.insertSubview(overlayView, aboveSubview: self) } else { overlayView.angle = 45 From 16b8446efa72966a7d69f75fa17b8bb85168cd55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Mon, 16 Dec 2024 15:52:48 +0100 Subject: [PATCH 05/17] fix: handle accessory views and include missing file - Preserved accessory views by ensuring watermarks are not applied to them - Added missing file from the last commit --- .../Cells/UniversalItemListCell.swift | 2 +- .../ConfidentialContentView.swift | 37 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift index 89d253bda..4ab45ff2f 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell.swift @@ -654,7 +654,7 @@ open class UniversalItemListCell: ThemeableCollectionViewListCell { } self.accessibilityLabel = accessibilityLabelContent - self.secureView(core: clientContext?.core) + contentView.secureView(core: clientContext?.core) } } diff --git a/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift b/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift index 1573ee2f2..a3de0dbe2 100644 --- a/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift +++ b/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift @@ -31,7 +31,12 @@ public class ConfidentialContentView: UIView { setNeedsDisplay() } } - var textColor: UIColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.button], for: nil)?.withAlphaComponent(0.7) ?? .red { + var textColor: UIColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.button], for: nil)?.withAlphaComponent(0.8) ?? .red { + didSet { + setNeedsDisplay() + } + } + var subtitleTextColor: UIColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.button], for: nil)?.withAlphaComponent(0.4) ?? .red { didSet { setNeedsDisplay() } @@ -41,6 +46,11 @@ public class ConfidentialContentView: UIView { setNeedsDisplay() } } + var subtitleFont: UIFont = UIFont.systemFont(ofSize: 8) { + didSet { + setNeedsDisplay() + } + } var angle: CGFloat = 45 { didSet { setNeedsDisplay() @@ -56,6 +66,11 @@ public class ConfidentialContentView: UIView { setNeedsDisplay() } } + var marginY: CGFloat = 10 { + didSet { + setNeedsDisplay() + } + } private var drawn: Bool = false @@ -128,9 +143,9 @@ public class ConfidentialContentView: UIView { let rotatedDiagonal = sqrt(rect.width * rect.width + rect.height * rect.height) let startX = -rotatedDiagonal - let startY = -rotatedDiagonal + let startY = -rotatedDiagonal + marginY let endX = rotatedDiagonal - let endY = rotatedDiagonal + let endY = rotatedDiagonal - marginY var y = startY while y <= endY { @@ -150,5 +165,21 @@ public class ConfidentialContentView: UIView { } context.restoreGState() + + if angle < 45.0 { + let combinedText = "\(subtext) - \(text)" + let combinedTextAttributes: [NSAttributedString.Key: Any] = [ + .font: subtitleFont, + .foregroundColor: subtitleTextColor + ] + let combinedTextSize = combinedText.size(withAttributes: combinedTextAttributes) + + var x = CGFloat(0) + let subtextY = rect.height - combinedTextSize.height - 2 + while x < rect.width { + combinedText.draw(at: CGPoint(x: x, y: subtextY), withAttributes: combinedTextAttributes) + x += combinedTextSize.width + columnSpacing + } + } } } From f9873041b5de4740689c34f0bd2761dfd553d127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Thu, 16 Jan 2025 14:34:56 +0100 Subject: [PATCH 06/17] Fixed code review findings --- .../Confidential/ConfidentalManager.swift | 101 ------------------ .../Confidential/ConfidentialManager.h | 2 +- .../Confidential/ConfidentialManager.m | 13 ++- ownCloudAppShared/Client/Actions/Action.swift | 2 +- .../Cells/DriveListCell.swift | 12 +-- .../Client/Sharing/ShareViewController.swift | 12 +-- .../ClientLocationPicker.swift | 2 +- .../UIKit Extension/UIView+Extension.swift | 23 ---- .../BrowserNavigationViewController.swift | 10 +- .../ConfidentialContentView.swift | 51 ++++++++- .../User Interface/Theme/CSS/ThemeCSS.swift | 4 + .../Theme/ThemeCollection.swift | 6 +- 12 files changed, 73 insertions(+), 165 deletions(-) delete mode 100644 ownCloudAppFramework/Confidential/ConfidentalManager.swift diff --git a/ownCloudAppFramework/Confidential/ConfidentalManager.swift b/ownCloudAppFramework/Confidential/ConfidentalManager.swift deleted file mode 100644 index f64c233bd..000000000 --- a/ownCloudAppFramework/Confidential/ConfidentalManager.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// ConfidentalManager.swift -// ownCloud -// -// Created by Matthias Hühne on 09.12.24. -// Copyright © 2024 ownCloud GmbH. All rights reserved. -// - -/* - * Copyright (C) 2024, ownCloud GmbH. - * - * This code is covered by the GNU Public License Version 3. - * - * For distribution utilizing Apple mechanisms please see https://owncloud.org/contribute/iOS-license-exception/ - * You should have received a copy of this license along with this program. If not, see . - * - */ - -import ownCloudSDK - -public class ConfidentalManager : NSObject { - public static var shared: ConfidentalManager = ConfidentalManager() -} - -public extension OCClassSettingsIdentifier { - static let confidential = OCClassSettingsIdentifier("confidential") -} - -public extension OCClassSettingsKey { - static let allowScreenshots = OCClassSettingsKey("allow-screenshots") - static let markConfidentalViews = OCClassSettingsKey("mark-confidental-views") - static let allowOverwriteConfidentalMDMSettings = OCClassSettingsKey("allow-overwrite-confidental-mdm-settings") -} - -extension ConfidentalManager : OCClassSettingsSupport { - public static let classSettingsIdentifier : OCClassSettingsIdentifier = .confidential - - - public var allowScreenshots: Bool { - return ConfidentalManager.classSetting(forOCClassSettingsKey: .allowScreenshots) as? Bool ?? true - } - - public var markConfidentalViews: Bool { - return ConfidentalManager.classSetting(forOCClassSettingsKey: .markConfidentalViews) as? Bool ?? true - } - - public var allowOverwriteConfidentalMDMSettings: Bool { - return (self.confidentalSettingsEnabled && (ConfidentalManager.classSetting(forOCClassSettingsKey: .allowOverwriteConfidentalMDMSettings) as? Bool ?? true)) - } - - public var confidentalSettingsEnabled: Bool { - if self.allowScreenshots || self.markConfidentalViews { - return true - } - - return false - } - - public var disallowedActions: [String]? { - if confidentalSettingsEnabled, !allowOverwriteConfidentalMDMSettings { - return ["com.owncloud.action.openin", "com.owncloud.action.copy", "com.owncloud.action.collaborate", "action.allow-image-interactions"] - } - - return nil - } - - public static func defaultSettings(forIdentifier identifier: OCClassSettingsIdentifier) -> [OCClassSettingsKey : Any]? { - if identifier == .confidential { - return [ - .allowScreenshots : false, - .markConfidentalViews : true, - .allowOverwriteConfidentalMDMSettings : false - ] - } - - return nil - } - - public static func classSettingsMetadata() -> [OCClassSettingsKey : [OCClassSettingsMetadataKey : Any]]? { - return [ - .allowScreenshots : [ - .type : OCClassSettingsMetadataType.boolean, - .description : "Controls whether screenshots are allowed or not. If not allowed confidental views will be marked as sensitive and are not visible in screenshots.", - .category : "Confidental", - .status : OCClassSettingsKeyStatus.debugOnly - ], - .markConfidentalViews : [ - .type : OCClassSettingsMetadataType.boolean, - .description : "Controls if views which contains sensitive content contains a watermark or not.", - .category : "Confidental", - .status : OCClassSettingsKeyStatus.debugOnly - ], - .allowOverwriteConfidentalMDMSettings : [ - .type : OCClassSettingsMetadataType.boolean, - .description : "Controls if confidental related MDM settings can be overwritten.", - .category : "Confidental", - .status : OCClassSettingsKeyStatus.debugOnly - ] - ] - } -} diff --git a/ownCloudAppFramework/Confidential/ConfidentialManager.h b/ownCloudAppFramework/Confidential/ConfidentialManager.h index d9e08488b..797e0124c 100644 --- a/ownCloudAppFramework/Confidential/ConfidentialManager.h +++ b/ownCloudAppFramework/Confidential/ConfidentialManager.h @@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN @property (assign, readonly) BOOL markConfidentialViews; @property (assign, readonly) BOOL allowOverwriteConfidentialMDMSettings; @property (assign, readonly) BOOL confidentialSettingsEnabled; -@property (nonatomic, readonly, nullable) NSArray *disallowedActions; +@property (nonatomic, readonly, nullable) NSArray *disallowedActions; @end diff --git a/ownCloudAppFramework/Confidential/ConfidentialManager.m b/ownCloudAppFramework/Confidential/ConfidentialManager.m index 4c1c64e28..f10a1343e 100644 --- a/ownCloudAppFramework/Confidential/ConfidentialManager.m +++ b/ownCloudAppFramework/Confidential/ConfidentialManager.m @@ -40,19 +40,18 @@ + (OCClassSettingsIdentifier)classSettingsIdentifier } - (BOOL)allowScreenshots { - id value = [ConfidentialManager classSettingForOCClassSettingsKey:OCClassSettingsKeyAllowScreenshots]; - return value ? [value boolValue] : YES; + NSNumber *value = [ConfidentialManager classSettingForOCClassSettingsKey:OCClassSettingsKeyAllowScreenshots]; + return (value != nil) ? value.boolValue : YES; } - (BOOL)markConfidentialViews { - id value = [ConfidentialManager classSettingForOCClassSettingsKey:OCClassSettingsKeyMarkConfidentialViews]; - return value ? [value boolValue] : YES; + NSNumber *value = [ConfidentialManager classSettingForOCClassSettingsKey:OCClassSettingsKeyMarkConfidentialViews]; + return (value != nil) ? value.boolValue : YES; } - (BOOL)allowOverwriteConfidentialMDMSettings { - BOOL confidentialSettingsEnabled = self.confidentialSettingsEnabled; - id value = [ConfidentialManager classSettingForOCClassSettingsKey:OCClassSettingsKeyAllowOverwriteConfidentialMDMSettings]; - return confidentialSettingsEnabled && (value ? [value boolValue] : YES); + NSNumber *value = [ConfidentialManager classSettingForOCClassSettingsKey:OCClassSettingsKeyAllowOverwriteConfidentialMDMSettings]; + return self.confidentialSettingsEnabled && ((value != nil) ? value.boolValue : YES); } - (BOOL)confidentialSettingsEnabled { diff --git a/ownCloudAppShared/Client/Actions/Action.swift b/ownCloudAppShared/Client/Actions/Action.swift index dd4c0099d..a48bfde4f 100644 --- a/ownCloudAppShared/Client/Actions/Action.swift +++ b/ownCloudAppShared/Client/Actions/Action.swift @@ -311,7 +311,7 @@ open class Action : NSObject { disallowedActions.contains(actionIdentifier.rawValue) { return .noMatch } - if let disallowedActions = ConfidentialManager.shared.disallowedActions, disallowedActions.contains(actionIdentifier.rawValue) { + if let disallowedActions = ConfidentialManager.shared.disallowedActions, disallowedActions.contains(OCExtensionIdentifier(rawValue: actionIdentifier.rawValue)) { return .noMatch } } diff --git a/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift index 4fa84e2f3..5843b8fa6 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift @@ -140,9 +140,7 @@ extension DriveListCell { resourceManager?.start(coverImageRequest) } - if let clientItemViewController = collectionItemRef.ocCellConfiguration?.hostViewController as? CollectionViewController { - cell.secureView(core: clientItemViewController.clientContext?.core) - } + cell.secureView(core: collectionItemRef.ocCellConfiguration?.clientContext?.core) cell.accessories = [ .disclosureIndicator() ] } @@ -173,9 +171,7 @@ extension DriveListCell { cell.collectionItemRef = collectionItemRef cell.collectionViewController = collectionItemRef.ocCellConfiguration?.hostViewController - if let clientItemViewController = collectionItemRef.ocCellConfiguration?.hostViewController as? CollectionViewController { - cell.secureView(core: clientItemViewController.clientContext?.core) - } + cell.secureView(core: collectionItemRef.ocCellConfiguration?.clientContext?.core) if let coverImageRequest = coverImageRequest { resourceManager?.start(coverImageRequest) @@ -222,9 +218,7 @@ extension DriveListCell { cell.collectionItemRef = collectionItemRef cell.collectionViewController = collectionItemRef.ocCellConfiguration?.hostViewController - if let clientItemViewController = collectionItemRef.ocCellConfiguration?.hostViewController as? CollectionViewController { - cell.secureView(core: clientItemViewController.clientContext?.core) - } + cell.secureView(core: collectionItemRef.ocCellConfiguration?.clientContext?.core) if let coverImageRequest = coverImageRequest { resourceManager?.start(coverImageRequest) diff --git a/ownCloudAppShared/Client/Sharing/ShareViewController.swift b/ownCloudAppShared/Client/Sharing/ShareViewController.swift index 920a77698..ea9fd8355 100644 --- a/ownCloudAppShared/Client/Sharing/ShareViewController.swift +++ b/ownCloudAppShared/Client/Sharing/ShareViewController.swift @@ -260,16 +260,8 @@ open class ShareViewController: CollectionViewController, SearchViewControllerDe open override func viewDidLoad() { super.viewDidLoad() - let secureView = SecureTextField().secureContainerView - secureView.addSubview(collectionView) - view.addSubview(secureView) - - NSLayoutConstraint.activate([ - secureView.topAnchor.constraint(equalTo: view.topAnchor), - secureView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - secureView.leadingAnchor.constraint(equalTo: view.leadingAnchor).with(priority: .defaultHigh), - secureView.trailingAnchor.constraint(equalTo: view.trailingAnchor) - ]) + let wrappedContentContainerView = collectionView.withScreenshotProtection + view.addSubview(wrappedContentContainerView) // Disable dragging of items, so keyboard control does // not include "Drag Item" in the accessibility actions diff --git a/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift index d2112883e..69d472dc2 100644 --- a/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift +++ b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift @@ -173,7 +173,7 @@ public class ClientLocationPicker : NSObject { allowedLocationFilter = { (targetLocation, context) in OnMainThread { - self.headerView?.secureView(core: context?.core) + headerView?.secureView(core: context?.core) } // Disallow all paths as target that are parent of any of the items if itemParentLocations.contains(targetLocation) { diff --git a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift index 47f233caf..1c8291bbf 100644 --- a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift @@ -77,27 +77,4 @@ public extension UIView { return hostViewController } - - func secureView(core: OCCore?) { - let overlayView = ConfidentialContentView() - overlayView.text = core?.bookmark.user?.emailAddress ?? "Confidential View" - overlayView.subtext = core?.bookmark.userName ?? "Confidential View" - overlayView.backgroundColor = .clear - overlayView.translatesAutoresizingMaskIntoConstraints = false - - if self.frame.height <= 200 { - overlayView.angle = 10 - self.insertSubview(overlayView, aboveSubview: self) - } else { - overlayView.angle = 45 - self.addSubview(overlayView) - } - - NSLayoutConstraint.activate([ - overlayView.topAnchor.constraint(equalTo: self.topAnchor), - overlayView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - overlayView.leadingAnchor.constraint(equalTo: self.leadingAnchor), - overlayView.trailingAnchor.constraint(equalTo: self.trailingAnchor) - ]) - } } diff --git a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift index eff95c909..82714ce1d 100644 --- a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift @@ -71,9 +71,8 @@ open class BrowserNavigationViewController: EmbeddingViewController, Themeable, contentContainerView.translatesAutoresizingMaskIntoConstraints = false contentContainerView.focusGroupIdentifier = "com.owncloud.content" - let secureView = SecureTextField().secureContainerView - secureView.addSubview(contentContainerView) - view.addSubview(secureView) + let wrappedContentContainerView = contentContainerView.withScreenshotProtection + view.addSubview(wrappedContentContainerView) navigationView.translatesAutoresizingMaskIntoConstraints = false navigationView.delegate = self @@ -93,11 +92,6 @@ open class BrowserNavigationViewController: EmbeddingViewController, Themeable, NSLayoutConstraint.activate([ - secureView.topAnchor.constraint(equalTo: view.topAnchor), - secureView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - secureView.leadingAnchor.constraint(equalTo: view.leadingAnchor).with(priority: .defaultHigh), - secureView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - contentContainerView.topAnchor.constraint(equalTo: view.topAnchor), contentContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), contentContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor).with(priority: .defaultHigh), // Allow for flexibility without having to remove this constraint. It will be overridden by constraints with higher priority (default is .required) when necessary diff --git a/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift b/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift index a3de0dbe2..da0c1c99e 100644 --- a/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift +++ b/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift @@ -19,7 +19,7 @@ import ownCloudSDK import ownCloudApp -public class ConfidentialContentView: UIView { +public class ConfidentialContentView: UIView, Themeable { var text: String = "Confidential Content" { didSet { @@ -31,12 +31,12 @@ public class ConfidentialContentView: UIView { setNeedsDisplay() } } - var textColor: UIColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.button], for: nil)?.withAlphaComponent(0.8) ?? .red { + var textColor: UIColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.confidentialLabel], for: nil) ?? .red { didSet { setNeedsDisplay() } } - var subtitleTextColor: UIColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.button], for: nil)?.withAlphaComponent(0.4) ?? .red { + var subtitleTextColor: UIColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.confidentialSecondaryLabel], for: nil) ?? .red { didSet { setNeedsDisplay() } @@ -77,6 +77,7 @@ public class ConfidentialContentView: UIView { override init(frame: CGRect) { super.init(frame: frame) setupOrientationObserver() + Theme.shared.register(client: self, applyImmediately: true) } required init?(coder: NSCoder) { @@ -85,6 +86,7 @@ public class ConfidentialContentView: UIView { } deinit { + Theme.shared.unregister(client: self) NotificationCenter.default.removeObserver(self) } @@ -182,4 +184,47 @@ public class ConfidentialContentView: UIView { } } } + + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + if let color = collection.css.getColor(.stroke, selectors: [.confidentialLabel], for: nil) { + textColor = color + } + if let color = collection.css.getColor(.stroke, selectors: [.confidentialSecondaryLabel], for: nil) { + subtitleTextColor = color + } + + drawn = false + setNeedsDisplay() + } +} + +public extension UIView { + + func secureView(core: OCCore?) { + let overlayView = ConfidentialContentView() + overlayView.text = core?.bookmark.user?.emailAddress ?? "Confidential View" + overlayView.subtext = core?.bookmark.userName ?? "Confidential View" + overlayView.backgroundColor = .clear + overlayView.translatesAutoresizingMaskIntoConstraints = false + overlayView.angle = (self.frame.height <= 200) ? 10 : 45 + + self.addSubview(overlayView) + + NSLayoutConstraint.activate([ + overlayView.topAnchor.constraint(equalTo: self.topAnchor), + overlayView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + overlayView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + overlayView.trailingAnchor.constraint(equalTo: self.trailingAnchor) + ]) + } + + var withScreenshotProtection: UIView { + if ConfidentialManager.shared.allowScreenshots { + return self + } + + let secureContainerView = SecureTextField().secureContainerView + secureContainerView.embed(toFillWith: self) + return secureContainerView + } } diff --git a/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS.swift b/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS.swift index 957d2d3d9..cd5681abf 100644 --- a/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS.swift +++ b/ownCloudAppShared/User Interface/Theme/CSS/ThemeCSS.swift @@ -51,6 +51,10 @@ public struct ThemeCSSSelector: RawRepresentable, Equatable { public static let sectionFooter = ThemeCSSSelector(rawValue: "sectionFooter") public static let cell = ThemeCSSSelector(rawValue: "cell") public static let accessory = ThemeCSSSelector(rawValue: "accessory") + + // Confidential + public static let confidentialLabel = ThemeCSSSelector(rawValue: "confidentialLabel") + public static let confidentialSecondaryLabel = ThemeCSSSelector(rawValue: "confidentialSecondaryLabel") // Content Elements public static let splitView = ThemeCSSSelector(rawValue: "splitView") diff --git a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift index 94aef8c2c..8eb609497 100644 --- a/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift +++ b/ownCloudAppShared/User Interface/Theme/ThemeCollection.swift @@ -618,7 +618,11 @@ public class ThemeCollection : NSObject { ThemeCSSRecord(selectors: [.label, .destructive], property: .stroke, value: UIColor.red), ThemeCSSRecord(selectors: [.label, .warning], property: .stroke, value: UIColor(hex: 0xF2994A)), ThemeCSSRecord(selectors: [.label, .error], property: .stroke, value: UIColor(hex: 0xEB5757)), - ThemeCSSRecord(selectors: [.label, .success], property: .stroke, value: UIColor(hex: 0x27AE60)) + ThemeCSSRecord(selectors: [.label, .success], property: .stroke, value: UIColor(hex: 0x27AE60)), + + // - Confidential + ThemeCSSRecord(selectors: [.confidentialLabel], property: .stroke, value: tintColor.withAlphaComponent(0.8)), + ThemeCSSRecord(selectors: [.confidentialSecondaryLabel], property: .stroke, value: tintColor.withAlphaComponent(0.4)) ]) // - Fill styles From f38f53c79d233816ca916066117a43a9458511bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Mon, 20 Jan 2025 16:00:30 +0100 Subject: [PATCH 07/17] I have added a force quit feature to prevent the application from reaching this point. --- .../User Interface/Confidential/SecureTextField.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift b/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift index af60fba21..db23865ce 100644 --- a/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift +++ b/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift @@ -46,6 +46,9 @@ class SecureTextField : UITextField { return secureView } + // If screenshot protection was not possible, force close the application. + exit(0) + let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false From d14c94952f414f1448b8ab19675eda326c753055 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Mon, 20 Jan 2025 22:37:54 +0100 Subject: [PATCH 08/17] - ios-sdk: fix infinite loop in class settings source support - ConfidentialManager: - implement OCClassSettingsSource to inject settings - remove effectless "action.allow-image-interactions" from .disallowedActions - IntentSettings, ImageScrollView, OCFileProviderSettings: remove ConfidentialManager references (=> equivalent implemented via ConfidentialManager) - Action: use actionIdentifier directly, instead of rawValue - FileProvider: add ConfidentialManager to build target --- ios-sdk | 2 +- ownCloud Intents/IntentSettings.swift | 4 -- ownCloud.xcodeproj/project.pbxproj | 2 + ownCloud/UI Elements/ImageScrollView.swift | 1 - .../Confidential/ConfidentialManager.h | 4 +- .../Confidential/ConfidentialManager.m | 50 +++++++++++++++++-- .../OCFileProviderSettings.m | 4 -- ownCloudAppShared/Client/Actions/Action.swift | 2 +- 8 files changed, 54 insertions(+), 15 deletions(-) diff --git a/ios-sdk b/ios-sdk index da220ce30..7b14277fb 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit da220ce30108d11e9115c409d5ad2efd46a6fd58 +Subproject commit 7b14277fb8048130a05211d83f98685a0b3dd88b diff --git a/ownCloud Intents/IntentSettings.swift b/ownCloud Intents/IntentSettings.swift index 434af6cb4..aa3390ec3 100644 --- a/ownCloud Intents/IntentSettings.swift +++ b/ownCloud Intents/IntentSettings.swift @@ -30,10 +30,6 @@ class IntentSettings: NSObject { } var isEnabled : Bool { - if !ConfidentialManager.shared.allowOverwriteConfidentialMDMSettings { - return false - } - return (self.classSetting(forOCClassSettingsKey: .shortcutsEnabled) as? Bool) ?? true } diff --git a/ownCloud.xcodeproj/project.pbxproj b/ownCloud.xcodeproj/project.pbxproj index bbdec175c..55bcb70b3 100644 --- a/ownCloud.xcodeproj/project.pbxproj +++ b/ownCloud.xcodeproj/project.pbxproj @@ -297,6 +297,7 @@ DC2A68D529D492B300BFF393 /* space.tvg in Resources */ = {isa = PBXBuildFile; fileRef = DC2A68D429D492B200BFF393 /* space.tvg */; }; DC2A68D729D4E93300BFF393 /* SharedKeyCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2A68D629D4E93300BFF393 /* SharedKeyCommands.swift */; }; DC2A8E6A2B57EA8F001F0522 /* AccountControllerSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2A8E692B57EA8F001F0522 /* AccountControllerSearchViewController.swift */; }; + DC2FB16A2D3EF4FD00726E97 /* ConfidentialManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 39D091B52D079644001329DF /* ConfidentialManager.m */; }; DC36885824DC98BF00333600 /* OCFileProviderServiceSession.h in Headers */ = {isa = PBXBuildFile; fileRef = DC36885624DC98BF00333600 /* OCFileProviderServiceSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC36885924DC98BF00333600 /* OCFileProviderServiceSession.m in Sources */ = {isa = PBXBuildFile; fileRef = DC36885724DC98BF00333600 /* OCFileProviderServiceSession.m */; }; DC36885D24DD916800333600 /* ownCloudApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCC0855C2293F1FD008CC05C /* ownCloudApp.framework */; }; @@ -5011,6 +5012,7 @@ DC2218CC2823329100808BCE /* FileProviderContentEnumerator.m in Sources */, DC27A1E920CC56B0008ACB6C /* FileProviderExtensionThumbnailRequest.m in Sources */, DCF2DA7A24C82E480026D790 /* FileProviderServiceSource.m in Sources */, + DC2FB16A2D3EF4FD00726E97 /* ConfidentialManager.m in Sources */, DC625141225C904700736874 /* NSError+MessageResolution.m in Sources */, DC27A1A820CC095C008ACB6C /* OCCore+FileProviderTools.m in Sources */, DC1251E82C7470C30040FBC6 /* OCBookmark+AppExtensions.m in Sources */, diff --git a/ownCloud/UI Elements/ImageScrollView.swift b/ownCloud/UI Elements/ImageScrollView.swift index 3b8028a99..6cebf544b 100644 --- a/ownCloud/UI Elements/ImageScrollView.swift +++ b/ownCloud/UI Elements/ImageScrollView.swift @@ -197,7 +197,6 @@ extension ImageScrollView { } var imageInteractionsAllowed: Bool { - guard ConfidentialManager.shared.allowOverwriteConfidentialMDMSettings else { return false } return Action.classSetting(forOCClassSettingsKey: .allowImageInteractions) as? Bool ?? true } } diff --git a/ownCloudAppFramework/Confidential/ConfidentialManager.h b/ownCloudAppFramework/Confidential/ConfidentialManager.h index 797e0124c..bab497cbf 100644 --- a/ownCloudAppFramework/Confidential/ConfidentialManager.h +++ b/ownCloudAppFramework/Confidential/ConfidentialManager.h @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface ConfidentialManager : NSObject +@interface ConfidentialManager : NSObject @property(class,strong,nonatomic,readonly) ConfidentialManager *sharedConfidentialManager; @@ -33,6 +33,8 @@ NS_ASSUME_NONNULL_BEGIN @end +extern OCClassSettingsSourceIdentifier OCClassSettingsSourceIdentifierConfidentialManager; + extern OCClassSettingsIdentifier OCClassSettingsIdentifierConfidential; extern OCClassSettingsKey OCClassSettingsKeyAllowScreenshots; diff --git a/ownCloudAppFramework/Confidential/ConfidentialManager.m b/ownCloudAppFramework/Confidential/ConfidentialManager.m index f10a1343e..ce6dd52a1 100644 --- a/ownCloudAppFramework/Confidential/ConfidentialManager.m +++ b/ownCloudAppFramework/Confidential/ConfidentialManager.m @@ -17,9 +17,15 @@ */ #import "ConfidentialManager.h" +#import "OCFileProviderSettings.h" @implementation ConfidentialManager ++ (void)load +{ + [OCClassSettings.sharedSettings addSource:ConfidentialManager.sharedConfidentialManager]; +} + + (instancetype)sharedConfidentialManager { static dispatch_once_t onceToken; @@ -55,15 +61,14 @@ - (BOOL)allowOverwriteConfidentialMDMSettings { } - (BOOL)confidentialSettingsEnabled { - return self.allowScreenshots || self.markConfidentialViews; + return !self.allowScreenshots || self.markConfidentialViews; } - (NSArray *)disallowedActions { if (self.confidentialSettingsEnabled && !self.allowOverwriteConfidentialMDMSettings) { return @[ @"com.owncloud.action.openin", - @"com.owncloud.action.copy", - @"action.allow-image-interactions" + @"com.owncloud.action.copy" ]; } return nil; @@ -105,8 +110,47 @@ + (OCClassSettingsMetadataCollection)classSettingsMetadata }); } +#pragma mark - Class settings source +- (OCClassSettingsSourceIdentifier)settingsSourceIdentifier +{ + return (OCClassSettingsSourceIdentifierConfidentialManager); +} + +- (nullable NSDictionary *)settingsForIdentifier:(OCClassSettingsIdentifier)identifier +{ + if (!self.allowOverwriteConfidentialMDMSettings && self.confidentialSettingsEnabled) { + // Action + if ([identifier isEqual:@"action"]) { // OCClassSettingsIdentifier.action + // Disallow image interactions (ImageScrollView.imageInteractionsAllowed) + return (@{ + @"allow-image-interactions" : @(NO) // OCClassSettingsKey.allowImageInteractions + }); + } + + // File Provider + if ([identifier isEqual:OCClassSettingsIdentifierFileProvider]) { + // Disallow File Provider browsing + return (@{ + OCClassSettingsKeyFileProviderBrowseable : @(NO) + }); + } + + // Shortcuts + if ([identifier isEqual:@"shortcuts"]) { // OCClassSettingsIdentifier.shortcuts + // Disallow shortcuts (IntentSettings.isEnabled) + return (@{ + @"enabled" : @(NO) // OCClassSettingsKey.shortcutsEnabled + }); + } + } + + return (nil); +} + @end +OCClassSettingsSourceIdentifier OCClassSettingsSourceIdentifierConfidentialManager = @"confidential"; + OCClassSettingsIdentifier OCClassSettingsIdentifierConfidential = @"confidential"; OCClassSettingsKey OCClassSettingsKeyAllowScreenshots = @"allow-screenshots"; diff --git a/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m b/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m index 7def15b3c..c675529e4 100644 --- a/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m +++ b/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m @@ -47,10 +47,6 @@ + (OCClassSettingsMetadataCollection)classSettingsMetadata + (BOOL)browseable { - if ([[ConfidentialManager sharedConfidentialManager] allowOverwriteConfidentialMDMSettings] == false) { - return false; - } - return ([([self classSettingForOCClassSettingsKey:OCClassSettingsKeyFileProviderBrowseable]) boolValue]); } diff --git a/ownCloudAppShared/Client/Actions/Action.swift b/ownCloudAppShared/Client/Actions/Action.swift index a48bfde4f..ee4950a64 100644 --- a/ownCloudAppShared/Client/Actions/Action.swift +++ b/ownCloudAppShared/Client/Actions/Action.swift @@ -311,7 +311,7 @@ open class Action : NSObject { disallowedActions.contains(actionIdentifier.rawValue) { return .noMatch } - if let disallowedActions = ConfidentialManager.shared.disallowedActions, disallowedActions.contains(OCExtensionIdentifier(rawValue: actionIdentifier.rawValue)) { + if let disallowedActions = ConfidentialManager.shared.disallowedActions, disallowedActions.contains(actionIdentifier) { return .noMatch } } From 568c49c710a59cef91119814bbf4e83db6df4ba3 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Mon, 20 Jan 2025 22:52:00 +0100 Subject: [PATCH 09/17] - remove no longer needed imports and includes --- ownCloud Intents/IntentSettings.swift | 1 - ownCloud/UI Elements/ImageScrollView.swift | 1 - .../File Provider Services/OCFileProviderSettings.m | 1 - 3 files changed, 3 deletions(-) diff --git a/ownCloud Intents/IntentSettings.swift b/ownCloud Intents/IntentSettings.swift index aa3390ec3..99457bdee 100644 --- a/ownCloud Intents/IntentSettings.swift +++ b/ownCloud Intents/IntentSettings.swift @@ -17,7 +17,6 @@ */ import ownCloudApp -import ownCloudAppShared import ownCloudSDK class IntentSettings: NSObject { diff --git a/ownCloud/UI Elements/ImageScrollView.swift b/ownCloud/UI Elements/ImageScrollView.swift index 6cebf544b..999a6cbef 100644 --- a/ownCloud/UI Elements/ImageScrollView.swift +++ b/ownCloud/UI Elements/ImageScrollView.swift @@ -19,7 +19,6 @@ import UIKit import VisionKit import ownCloudSDK -import ownCloudApp import ownCloudAppShared class ImageScrollView: UIScrollView { diff --git a/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m b/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m index c675529e4..88869cd39 100644 --- a/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m +++ b/ownCloudAppFramework/File Provider Services/OCFileProviderSettings.m @@ -17,7 +17,6 @@ */ #import "OCFileProviderSettings.h" -#import "ownCloudApp/ConfidentialManager.h" @implementation OCFileProviderSettings From c4a492725fd45b0f6e4ca1a2d23cc7be175e1d69 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Mon, 20 Jan 2025 23:54:50 +0100 Subject: [PATCH 10/17] - CollectionView, MoreViewHeader: adopt UIView.withScreenshotProtection - ShareViewController: remove redundant screenshot protection code - BrowserNavigationViewController: fully adopt UIView.withScreenshotProtection - ConfidentialContentView, SecureTextField: fix SwiftLint warnings --- .../CollectionViewController.swift | 10 ++-- .../Client/Sharing/ShareViewController.swift | 5 +- .../BrowserNavigationViewController.swift | 29 +++++----- .../ConfidentialContentView.swift | 54 +++++++++---------- .../Confidential/SecureTextField.swift | 17 +++--- .../User Interface/More/MoreViewHeader.swift | 31 +++++------ 6 files changed, 66 insertions(+), 80 deletions(-) diff --git a/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift b/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift index a745eb40c..2158bbc36 100644 --- a/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift +++ b/ownCloudAppShared/Client/Collection Views/CollectionViewController.swift @@ -90,11 +90,11 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, if usesStackViewRoot, let stackView = stackView { let safeAreaView = ThemeCSSView(frame: view.bounds) safeAreaView.translatesAutoresizingMaskIntoConstraints = false - safeAreaView.embed(toFillWith: collectionView, enclosingAnchors: safeAreaView.safeAreaAnchorSet) + safeAreaView.embed(toFillWith: collectionView.withScreenshotProtection, enclosingAnchors: safeAreaView.safeAreaAnchorSet) stackView.addArrangedSubview(safeAreaView) } else { - view.embed(toFillWith: collectionView, enclosingAnchors: view.defaultAnchorSet) + view.embed(toFillWith: collectionView.withScreenshotProtection, enclosingAnchors: view.defaultAnchorSet) } } @@ -167,14 +167,10 @@ open class CollectionViewController: UIViewController, UICollectionViewDelegate, public override func loadView() { super.loadView() - let secureView = SecureTextField().secureContainerView - - view.embed(toFillWith: secureView, enclosingAnchors: compressForKeyboard ? view.safeAreaWithKeyboardAnchorSet : view.safeAreaAnchorSet) - if usesStackViewRoot { createStackView() if let stackView { - secureView.embed(toFillWith: stackView, enclosingAnchors: compressForKeyboard ? view.safeAreaWithKeyboardAnchorSet : view.safeAreaAnchorSet) + view.embed(toFillWith: stackView, enclosingAnchors: compressForKeyboard ? view.safeAreaWithKeyboardAnchorSet : view.safeAreaAnchorSet) } } } diff --git a/ownCloudAppShared/Client/Sharing/ShareViewController.swift b/ownCloudAppShared/Client/Sharing/ShareViewController.swift index ea9fd8355..f663a80fb 100644 --- a/ownCloudAppShared/Client/Sharing/ShareViewController.swift +++ b/ownCloudAppShared/Client/Sharing/ShareViewController.swift @@ -259,10 +259,7 @@ open class ShareViewController: CollectionViewController, SearchViewControllerDe open override func viewDidLoad() { super.viewDidLoad() - - let wrappedContentContainerView = collectionView.withScreenshotProtection - view.addSubview(wrappedContentContainerView) - + // Disable dragging of items, so keyboard control does // not include "Drag Item" in the accessibility actions // invoked with Tab + Z diff --git a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift index 82714ce1d..10ea821aa 100644 --- a/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift +++ b/ownCloudAppShared/User Interface/Browser Navigation Controller/BrowserNavigationViewController.swift @@ -70,7 +70,7 @@ open class BrowserNavigationViewController: EmbeddingViewController, Themeable, contentContainerView.cssSelector = .content contentContainerView.translatesAutoresizingMaskIntoConstraints = false contentContainerView.focusGroupIdentifier = "com.owncloud.content" - + let wrappedContentContainerView = contentContainerView.withScreenshotProtection view.addSubview(wrappedContentContainerView) @@ -91,20 +91,19 @@ open class BrowserNavigationViewController: EmbeddingViewController, Themeable, navigationBarTopConstraint = navigationBarTopConstraint(for: navigationBarHidden) NSLayoutConstraint.activate([ - - contentContainerView.topAnchor.constraint(equalTo: view.topAnchor), - contentContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - contentContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor).with(priority: .defaultHigh), // Allow for flexibility without having to remove this constraint. It will be overridden by constraints with higher priority (default is .required) when necessary - contentContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - - contentContainerLidView.topAnchor.constraint(equalTo: contentContainerView.topAnchor), - contentContainerLidView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor), - contentContainerLidView.leadingAnchor.constraint(equalTo: contentContainerView.leadingAnchor), - contentContainerLidView.trailingAnchor.constraint(equalTo: contentContainerView.trailingAnchor), - - sideBarSeperatorView.topAnchor.constraint(equalTo: contentContainerView.topAnchor), - sideBarSeperatorView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor), - sideBarSeperatorView.leadingAnchor.constraint(equalTo: contentContainerView.leadingAnchor, constant: -1), + wrappedContentContainerView.topAnchor.constraint(equalTo: view.topAnchor), + wrappedContentContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + wrappedContentContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor).with(priority: .defaultHigh), // Allow for flexibility without having to remove this constraint. It will be overridden by constraints with higher priority (default is .required) when necessary + wrappedContentContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + contentContainerLidView.topAnchor.constraint(equalTo: wrappedContentContainerView.topAnchor), + contentContainerLidView.bottomAnchor.constraint(equalTo: wrappedContentContainerView.bottomAnchor), + contentContainerLidView.leadingAnchor.constraint(equalTo: wrappedContentContainerView.leadingAnchor), + contentContainerLidView.trailingAnchor.constraint(equalTo: wrappedContentContainerView.trailingAnchor), + + sideBarSeperatorView.topAnchor.constraint(equalTo: wrappedContentContainerView.topAnchor), + sideBarSeperatorView.bottomAnchor.constraint(equalTo: wrappedContentContainerView.bottomAnchor), + sideBarSeperatorView.leadingAnchor.constraint(equalTo: wrappedContentContainerView.leadingAnchor, constant: -1), sideBarSeperatorView.widthAnchor.constraint(equalToConstant: 1), navigationBarTopConstraint!, diff --git a/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift b/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift index da0c1c99e..b47fc146e 100644 --- a/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift +++ b/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift @@ -71,25 +71,25 @@ public class ConfidentialContentView: UIView, Themeable { setNeedsDisplay() } } - + private var drawn: Bool = false - + override init(frame: CGRect) { super.init(frame: frame) setupOrientationObserver() Theme.shared.register(client: self, applyImmediately: true) } - + required init?(coder: NSCoder) { super.init(coder: coder) setupOrientationObserver() } - + deinit { Theme.shared.unregister(client: self) NotificationCenter.default.removeObserver(self) } - + private func setupOrientationObserver() { NotificationCenter.default.addObserver( self, @@ -98,12 +98,12 @@ public class ConfidentialContentView: UIView, Themeable { object: nil ) } - + @objc private func handleOrientationChange() { drawn = false setNeedsDisplay() } - + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { for subview in subviews.reversed() { let subviewPoint = convert(point, to: subview) @@ -113,17 +113,17 @@ public class ConfidentialContentView: UIView, Themeable { } return nil // Allow touches to pass through } - + public override func draw(_ rect: CGRect) { guard ConfidentialManager.shared.markConfidentialViews, let context = UIGraphicsGetCurrentContext() else { return } - + drawn = true - + context.saveGState() - + let radians = angle * .pi / 180 context.rotate(by: radians) - + let textAttributes: [NSAttributedString.Key: Any] = [ .font: font, .foregroundColor: textColor @@ -132,23 +132,23 @@ public class ConfidentialContentView: UIView, Themeable { .font: font, .foregroundColor: textColor ] - + let textSize = text.size(withAttributes: textAttributes) let subtextSize = subtext.size(withAttributes: subtextAttributes) - + let stepX = textSize.width + columnSpacing let stepY = textSize.height + lineSpacing - + let stepSubtextX = subtextSize.width + columnSpacing let stepSubTextY = subtextSize.height + lineSpacing - + let rotatedDiagonal = sqrt(rect.width * rect.width + rect.height * rect.height) - + let startX = -rotatedDiagonal let startY = -rotatedDiagonal + marginY let endX = rotatedDiagonal let endY = rotatedDiagonal - marginY - + var y = startY while y <= endY { var x = startX @@ -165,9 +165,9 @@ public class ConfidentialContentView: UIView, Themeable { } y += stepY } - + context.restoreGState() - + if angle < 45.0 { let combinedText = "\(subtext) - \(text)" let combinedTextAttributes: [NSAttributedString.Key: Any] = [ @@ -175,7 +175,7 @@ public class ConfidentialContentView: UIView, Themeable { .foregroundColor: subtitleTextColor ] let combinedTextSize = combinedText.size(withAttributes: combinedTextAttributes) - + var x = CGFloat(0) let subtextY = rect.height - combinedTextSize.height - 2 while x < rect.width { @@ -184,7 +184,7 @@ public class ConfidentialContentView: UIView, Themeable { } } } - + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { if let color = collection.css.getColor(.stroke, selectors: [.confidentialLabel], for: nil) { textColor = color @@ -192,14 +192,14 @@ public class ConfidentialContentView: UIView, Themeable { if let color = collection.css.getColor(.stroke, selectors: [.confidentialSecondaryLabel], for: nil) { subtitleTextColor = color } - + drawn = false setNeedsDisplay() } } public extension UIView { - + func secureView(core: OCCore?) { let overlayView = ConfidentialContentView() overlayView.text = core?.bookmark.user?.emailAddress ?? "Confidential View" @@ -207,9 +207,9 @@ public extension UIView { overlayView.backgroundColor = .clear overlayView.translatesAutoresizingMaskIntoConstraints = false overlayView.angle = (self.frame.height <= 200) ? 10 : 45 - + self.addSubview(overlayView) - + NSLayoutConstraint.activate([ overlayView.topAnchor.constraint(equalTo: self.topAnchor), overlayView.bottomAnchor.constraint(equalTo: self.bottomAnchor), @@ -217,7 +217,7 @@ public extension UIView { overlayView.trailingAnchor.constraint(equalTo: self.trailingAnchor) ]) } - + var withScreenshotProtection: UIView { if ConfidentialManager.shared.allowScreenshots { return self diff --git a/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift b/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift index db23865ce..05a4336f6 100644 --- a/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift +++ b/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift @@ -19,25 +19,24 @@ import UIKit import ownCloudApp class SecureTextField : UITextField { - override init(frame: CGRect) { super.init(frame: .zero) self.isSecureTextEntry = true self.translatesAutoresizingMaskIntoConstraints = false } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + var secureContainerView: UIView { guard !ConfidentialManager.shared.allowScreenshots else { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false - + return view } - + if let secureView = self.subviews.filter({ subview in type(of: subview).description().contains("CanvasView") }).first { @@ -45,16 +44,16 @@ class SecureTextField : UITextField { secureView.isUserInteractionEnabled = true return secureView } - + // If screenshot protection was not possible, force close the application. exit(0) - + let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false - + return view } - + override var canBecomeFirstResponder: Bool { false } override func becomeFirstResponder() -> Bool { false } } diff --git a/ownCloudAppShared/User Interface/More/MoreViewHeader.swift b/ownCloudAppShared/User Interface/More/MoreViewHeader.swift index 8c12d00a4..75e60315a 100644 --- a/ownCloudAppShared/User Interface/More/MoreViewHeader.swift +++ b/ownCloudAppShared/User Interface/More/MoreViewHeader.swift @@ -86,12 +86,12 @@ open class MoreViewHeader: UIView { private func render() { cssSelectors = [.more, .header] - - let secureView = SecureTextField().secureContainerView + let contentContainerView = UIView() contentContainerView.translatesAutoresizingMaskIntoConstraints = false - secureView.addSubview(contentContainerView) - self.addSubview(secureView) + + let wrappedContentContainerView = contentContainerView.withScreenshotProtection + self.addSubview(wrappedContentContainerView) titleLabel.translatesAutoresizingMaskIntoConstraints = false detailLabel.translatesAutoresizingMaskIntoConstraints = false @@ -110,18 +110,13 @@ open class MoreViewHeader: UIView { titleLabel.setContentCompressionResistancePriority(.required, for: .vertical) detailLabel.setContentCompressionResistancePriority(.required, for: .vertical) labelContainerView.setContentCompressionResistancePriority(.required, for: .vertical) - - + NSLayoutConstraint.activate([ - secureView.topAnchor.constraint(equalTo: self.topAnchor), - secureView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - secureView.leadingAnchor.constraint(equalTo: self.leadingAnchor), - secureView.trailingAnchor.constraint(equalTo: self.trailingAnchor), - - contentContainerView.topAnchor.constraint(equalTo: self.topAnchor), - contentContainerView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - contentContainerView.leadingAnchor.constraint(equalTo: self.leadingAnchor), - + wrappedContentContainerView.topAnchor.constraint(equalTo: self.topAnchor), + wrappedContentContainerView.bottomAnchor.constraint(equalTo: self.bottomAnchor), + wrappedContentContainerView.leadingAnchor.constraint(equalTo: self.leadingAnchor), + wrappedContentContainerView.trailingAnchor.constraint(equalTo: self.trailingAnchor), + titleLabel.leadingAnchor.constraint(equalTo: labelContainerView.leadingAnchor), titleLabel.trailingAnchor.constraint(equalTo: labelContainerView.trailingAnchor), titleLabel.topAnchor.constraint(equalTo: labelContainerView.topAnchor), @@ -152,7 +147,7 @@ open class MoreViewHeader: UIView { if showFavoriteButton { updateFavoriteButtonImage() favoriteButton.addTarget(self, action: #selector(toogleFavoriteState), for: UIControl.Event.touchUpInside) - self.addSubview(favoriteButton) + contentContainerView.addSubview(favoriteButton) favoriteButton.isPointerInteractionEnabled = true NSLayoutConstraint.activate([ @@ -163,7 +158,7 @@ open class MoreViewHeader: UIView { favoriteButton.leadingAnchor.constraint(equalTo: labelContainerView.trailingAnchor, constant: 10) ]) } else if showActivityIndicator { - self.addSubview(activityIndicator) + contentContainerView.addSubview(activityIndicator) NSLayoutConstraint.activate([ activityIndicator.centerYAnchor.constraint(equalTo: self.centerYAnchor), @@ -227,7 +222,7 @@ open class MoreViewHeader: UIView { core?.vault.resourceManager?.start(iconRequest) titleLabel.numberOfLines = 0 - + self.secureView(core: core) } From a81867b9ad433a3d5d155d12fa5da5cf3ea6ee9a Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 21 Jan 2025 00:05:35 +0100 Subject: [PATCH 11/17] - ClientLocationPicker: fix CR finding, moving secureView installation from init/allowedLocationFilter to .provideViewController() --- .../Location Picker/ClientLocationPicker.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift index 69d472dc2..705fd4b49 100644 --- a/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift +++ b/ownCloudAppShared/Client/View Controllers/Location Picker/ClientLocationPicker.swift @@ -172,9 +172,6 @@ public class ClientLocationPicker : NSObject { } allowedLocationFilter = { (targetLocation, context) in - OnMainThread { - headerView?.secureView(core: context?.core) - } // Disallow all paths as target that are parent of any of the items if itemParentLocations.contains(targetLocation) { return false @@ -297,6 +294,8 @@ public class ClientLocationPicker : NSObject { } } + headerView?.secureView(core: context.core) + return viewController } From d3287bf0dec2cbede9880d9ad800b1a01e7a534e Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Tue, 21 Jan 2025 00:09:08 +0100 Subject: [PATCH 12/17] - UIView+Extension: remove no longer needed ownCloudSDK import --- ownCloudAppShared/UIKit Extension/UIView+Extension.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift index 1c8291bbf..b281fd66f 100644 --- a/ownCloudAppShared/UIKit Extension/UIView+Extension.swift +++ b/ownCloudAppShared/UIKit Extension/UIView+Extension.swift @@ -17,7 +17,6 @@ */ import UIKit -import ownCloudSDK public extension UIView { // MARK: - Animation From aa863699a605ffb5d2ce045372d2e97e7dba8554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Tue, 21 Jan 2025 16:16:41 +0100 Subject: [PATCH 13/17] redacte title and subtitle in spaces views --- .../Client/Collection Views/Cells/DriveGridCell.swift | 6 +++--- .../Client/Collection Views/Cells/DriveListCell.swift | 8 ++++---- .../User Interface/More/MoreViewHeader.swift | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ownCloudAppShared/Client/Collection Views/Cells/DriveGridCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/DriveGridCell.swift index 1dc63fc6d..669463f5f 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/DriveGridCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/DriveGridCell.swift @@ -88,14 +88,14 @@ class DriveGridCell: DriveHeaderCell { override var title: String? { didSet { - accessibilityLabel = title + accessibilityLabel = title?.redacted() } } override var subtitle: String? { didSet { - subtitleLabel.text = subtitle ?? " " // Ensure the grid cells' titles align by always showing a subtitle - if necessary, an empty one - accessibilityHint = subtitle + subtitleLabel.text = subtitle?.redacted() ?? " " // Ensure the grid cells' titles align by always showing a subtitle - if necessary, an empty one + accessibilityHint = subtitle?.redacted() } } diff --git a/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift b/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift index 5843b8fa6..18a7b0599 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/DriveListCell.swift @@ -41,12 +41,12 @@ class DriveListCell: ThemeableCollectionViewListCell { var title : String? { didSet { - titleLabel.text = title + titleLabel.text = title?.redacted() } } var subtitle : String? { didSet { - subtitleLabel.text = subtitle + subtitleLabel.text = subtitle?.redacted() } } @@ -153,8 +153,8 @@ extension DriveListCell { collectionItemRef.ocCellConfiguration?.configureCell(for: collectionItemRef, with: { itemRecord, item, cellConfiguration in if let presentable = OCDataRenderer.default.renderItem(item, asType: .presentable, error: nil, withOptions: nil) as? OCDataItemPresentable { - title = presentable.title - subtitle = presentable.subtitle + title = presentable.title?.redacted() + subtitle = presentable.subtitle?.redacted() resourceManager = cellConfiguration.core?.vault.resourceManager diff --git a/ownCloudAppShared/User Interface/More/MoreViewHeader.swift b/ownCloudAppShared/User Interface/More/MoreViewHeader.swift index 75e60315a..c5ae3bcac 100644 --- a/ownCloudAppShared/User Interface/More/MoreViewHeader.swift +++ b/ownCloudAppShared/User Interface/More/MoreViewHeader.swift @@ -200,7 +200,7 @@ open class MoreViewHeader: UIView { } } - titleLabel.attributedText = NSAttributedString(string: itemName ?? "", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold)]) + titleLabel.attributedText = NSAttributedString(string: itemName?.redacted() ?? "", attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17, weight: .semibold)]) let byteCountFormatter = ByteCountFormatter() byteCountFormatter.countStyle = .file @@ -227,8 +227,8 @@ open class MoreViewHeader: UIView { } public func updateHeader(title: String, subtitle: String) { - titleLabel.text = title - detailLabel.text = subtitle + titleLabel.text = title.redacted() + detailLabel.text = subtitle.redacted() } public required init?(coder aDecoder: NSCoder) { From 9df71cfd154e2d2a9b9c3eb332f606616b6c7779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Hu=CC=88hne?= Date: Tue, 21 Jan 2025 16:17:31 +0100 Subject: [PATCH 14/17] redacte title in copy and delete action --- ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift | 2 +- ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift b/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift index dfafb0b9f..a1926d386 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/CopyAction.swift @@ -121,7 +121,7 @@ class CopyAction : Action { if items.count > 1 { titleText = OCLocalizedFormat("Copy {{itemCount}} items", ["itemCount" : "\(items.count)"]) } else { - titleText = OCLocalizedFormat("Copy \"{{itemName}}\"", ["itemName" : items.first?.name ?? "?"]) + titleText = OCLocalizedFormat("Copy \"{{itemName}}\"", ["itemName" : items.first?.name?.redacted() ?? "?"]) } let locationPicker = ClientLocationPicker(location: startLocation, selectButtonTitle: OCLocalizedString("Copy here", nil), headerTitle: titleText, headerSubTitle: OCLocalizedString("Select target.", nil), avoidConflictsWith: items, choiceHandler: { (selectedDirectoryItem, location, _, cancelled) in diff --git a/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift b/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift index a244e79d1..75b0910bc 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/DeleteAction.swift @@ -61,7 +61,7 @@ class DeleteAction : Action { if items.count > 1 { itemDescripton = OCLocalizedString("Multiple items", nil) } else { - itemDescripton = items.first?.name + itemDescripton = items.first?.name?.redacted() } guard let name = itemDescripton else { From 6f4101306f42e279096c86037c89039255b8ebde Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 23 Jan 2025 17:38:40 +0100 Subject: [PATCH 15/17] - update ios-sdk - allow redacting item names in Move and Sharing operations (QA finding fix) --- ios-sdk | 2 +- .../Client/Actions/Actions+Extensions/MoveAction.swift | 2 +- .../OCItem+UniversalItemListCellContentProvider.swift | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ios-sdk b/ios-sdk index 7b14277fb..813ce8388 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 7b14277fb8048130a05211d83f98685a0b3dd88b +Subproject commit 813ce8388bb212e05966b790e835cd5a4810a4f8 diff --git a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift index 99c2a1914..b5f75a49d 100644 --- a/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift +++ b/ownCloud/Client/Actions/Actions+Extensions/MoveAction.swift @@ -66,7 +66,7 @@ class MoveAction : Action { if items.count > 1 { titleText = OCLocalizedFormat("Move {{itemCount}} items", ["itemCount" : "\(items.count)"]) } else { - titleText = OCLocalizedFormat("Move \"{{itemName}}\"", ["itemName" : items.first?.name ?? "?"]) + titleText = OCLocalizedFormat("Move \"{{itemName}}\"", ["itemName" : items.first?.name?.redacted() ?? "?"]) } let locationPicker = ClientLocationPicker(location: startLocation, selectButtonTitle: OCLocalizedString("Move here", nil), headerTitle: titleText, headerSubTitle: OCLocalizedString("Select target.", nil), avoidConflictsWith: items, choiceHandler: { (selectedDirectoryItem, location, _, cancelled) in diff --git a/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItem+UniversalItemListCellContentProvider.swift b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItem+UniversalItemListCellContentProvider.swift index 8106ddec5..f552f6ae5 100644 --- a/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItem+UniversalItemListCellContentProvider.swift +++ b/ownCloudAppShared/Client/Collection Views/Cells/UniversalItemListCell Content Providers/OCItem+UniversalItemListCellContentProvider.swift @@ -170,8 +170,12 @@ extension OCItem: UniversalItemListCellContentProvider { content.iconDisabled = isPlaceholder // Title - if let name = self.name { - content.title = isFile ? .file(name: name) : .folder(name: name) + if let name { + var displayName = name + if configuration?.style.type == .header { + displayName = name.redacted() + } + content.title = isFile ? .file(name: displayName) : .folder(name: displayName) } // Details From cda5ccb35d138579cd086606e75eb78457932f10 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Thu, 23 Jan 2025 17:46:26 +0100 Subject: [PATCH 16/17] - update SDK --- ios-sdk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios-sdk b/ios-sdk index 813ce8388..5b3598b17 160000 --- a/ios-sdk +++ b/ios-sdk @@ -1 +1 @@ -Subproject commit 813ce8388bb212e05966b790e835cd5a4810a4f8 +Subproject commit 5b3598b17ba06733aacadbd5031e1ae577e2e73b From b09987f0fb7d5dade6470a729d809e96c53d6774 Mon Sep 17 00:00:00 2001 From: Felix Schwarz Date: Mon, 27 Jan 2025 09:59:00 +0100 Subject: [PATCH 17/17] - ConfidentialManager: - add markup action to list of disallowedActions. The markup action could not be modified to implement protection mechanisms - not even on the CALayer level - without interaction with the system-provided view breaking - ConfidentialContentView: - break up ConfidentialContentView into Watermark (struct) and a CGContext extension to draw watermarks - refactor ConfidentialContentView to call the CGContext extension and use the Watermark struct - implement a CALayer subclass that also draws its content via the CGContext extension using the Watermark struct - extend the UIView extension to allow choosing between watermarking by subview or sublayer - context: these changes were made in an attempt to see if watermarking can be made work with EditDocumentViewController, which eventually failed (see above) - SecureTextField: remove unused code path, eliminating the compiler warning - EditDocumentViewController: change place where the watermark view is injected, allowing it to be sized correctly directly - addresses finding (4) in #1430 --- .../Actions/EditDocumentViewController.swift | 4 + .../Confidential/ConfidentialManager.m | 12 +- .../ConfidentialContentView.swift | 212 +++++++++--------- .../Confidential/SecureTextField.swift | 5 - 4 files changed, 127 insertions(+), 106 deletions(-) diff --git a/ownCloud/Client/Actions/EditDocumentViewController.swift b/ownCloud/Client/Actions/EditDocumentViewController.swift index 56b83f623..13f059d5a 100644 --- a/ownCloud/Client/Actions/EditDocumentViewController.swift +++ b/ownCloud/Client/Actions/EditDocumentViewController.swift @@ -121,6 +121,10 @@ class EditDocumentViewController: QLPreviewController, Themeable { } } timer!.resume() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) self.view.secureView(core: core) } diff --git a/ownCloudAppFramework/Confidential/ConfidentialManager.m b/ownCloudAppFramework/Confidential/ConfidentialManager.m index ce6dd52a1..42e57d867 100644 --- a/ownCloudAppFramework/Confidential/ConfidentialManager.m +++ b/ownCloudAppFramework/Confidential/ConfidentialManager.m @@ -68,7 +68,17 @@ - (BOOL)confidentialSettingsEnabled { if (self.confidentialSettingsEnabled && !self.allowOverwriteConfidentialMDMSettings) { return @[ @"com.owncloud.action.openin", - @"com.owncloud.action.copy" + @"com.owncloud.action.copy", + /* + As of iOS 18.2.1: + The markup action could not be modified to implement protection mechanisms - + not even on the CALayer level - without interaction with the system-provided + view breaking and becoming unusable in different ways. A possible reason for + this is that the markup feature is delivered by the OS as Remote UI (visible + as QLRemoteUIHostViewController in the view hierarchy) and that otherwise working + approaches to "passing through" events are not usable with these. + */ + @"com.owncloud.action.markup" ]; } return nil; diff --git a/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift b/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift index b47fc146e..4ae382db2 100644 --- a/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift +++ b/ownCloudAppShared/User Interface/Confidential/ConfidentialContentView.swift @@ -19,70 +19,80 @@ import ownCloudSDK import ownCloudApp -public class ConfidentialContentView: UIView, Themeable { - - var text: String = "Confidential Content" { - didSet { - setNeedsDisplay() - } - } - var subtext: String = "Confidential Content" { - didSet { - setNeedsDisplay() - } +public struct Watermark { + var text: String + var subtext: String + var textColor: UIColor + var subtitleTextColor: UIColor + var font: UIFont + var subtitleFont: UIFont + var angle: CGFloat + var columnSpacing: CGFloat + var lineSpacing: CGFloat + var marginY: CGFloat + + init(text: String, subtext: String, angle: CGFloat = 45) { + self.text = text + self.subtext = subtext + self.textColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.confidentialLabel], for: nil) ?? .red + self.subtitleTextColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.confidentialSecondaryLabel], for: nil) ?? .red + self.font = UIFont.systemFont(ofSize: 14) + self.subtitleFont = UIFont.systemFont(ofSize: 8) + self.angle = angle + self.columnSpacing = 50 + self.lineSpacing = 40 + self.marginY = 10 } - var textColor: UIColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.confidentialLabel], for: nil) ?? .red { - didSet { - setNeedsDisplay() - } - } - var subtitleTextColor: UIColor = Theme.shared.activeCollection.css.getColor(.stroke, selectors: [.confidentialSecondaryLabel], for: nil) ?? .red { +} + +public class ConfidentialContentLayer: CALayer { + var watermark: Watermark = .init(text: "Confidential Content", subtext: "Confidential Content") { didSet { setNeedsDisplay() } } - var font: UIFont = UIFont.systemFont(ofSize: 14) { - didSet { - setNeedsDisplay() - } + + init(watermark: Watermark) { + super.init() + self.watermark = watermark } - var subtitleFont: UIFont = UIFont.systemFont(ofSize: 8) { - didSet { - setNeedsDisplay() - } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - var angle: CGFloat = 45 { + + public override var frame: CGRect { didSet { setNeedsDisplay() } } - var columnSpacing: CGFloat = 50 { - didSet { - setNeedsDisplay() - } + + public override func draw(in ctx: CGContext) { + ctx.draw(watermark: watermark, in: bounds) } - var lineSpacing: CGFloat = 40 { - didSet { - setNeedsDisplay() + + public override func layoutIfNeeded() { + if let superlayer { + frame = superlayer.bounds } } - var marginY: CGFloat = 10 { +} + +public class ConfidentialContentView: UIView, Themeable { + var watermark: Watermark = .init(text: "Confidential Content", subtext: "Confidential Content") { didSet { setNeedsDisplay() } } - private var drawn: Bool = false - override init(frame: CGRect) { super.init(frame: frame) - setupOrientationObserver() - Theme.shared.register(client: self, applyImmediately: true) + setupViewAndObservers() } required init?(coder: NSCoder) { super.init(coder: coder) - setupOrientationObserver() + setupViewAndObservers() } deinit { @@ -90,64 +100,77 @@ public class ConfidentialContentView: UIView, Themeable { NotificationCenter.default.removeObserver(self) } - private func setupOrientationObserver() { + private func setupViewAndObservers() { NotificationCenter.default.addObserver( self, selector: #selector(handleOrientationChange), name: UIDevice.orientationDidChangeNotification, object: nil ) + + backgroundColor = .clear + + Theme.shared.register(client: self, applyImmediately: true) } @objc private func handleOrientationChange() { - drawn = false setNeedsDisplay() } public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - for subview in subviews.reversed() { - let subviewPoint = convert(point, to: subview) - if let hitView = subview.hitTest(subviewPoint, with: event) { - return hitView - } - } - return nil // Allow touches to pass through + let view = super.hitTest(point, with: event) + return view == self ? nil : view // Allow touches to pass through } public override func draw(_ rect: CGRect) { guard ConfidentialManager.shared.markConfidentialViews, let context = UIGraphicsGetCurrentContext() else { return } + context.draw(watermark: watermark, in: rect) + } + + public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { + if let color = collection.css.getColor(.stroke, selectors: [.confidentialLabel], for: nil) { + watermark.textColor = color + } + if let color = collection.css.getColor(.stroke, selectors: [.confidentialSecondaryLabel], for: nil) { + watermark.subtitleTextColor = color + } + setNeedsDisplay() + } +} - drawn = true +public extension CGContext { + func draw(watermark: Watermark, in rect: CGRect) { + UIGraphicsPushContext(self) - context.saveGState() + saveGState() - let radians = angle * .pi / 180 - context.rotate(by: radians) + let radians = watermark.angle * .pi / 180 + rotate(by: radians) let textAttributes: [NSAttributedString.Key: Any] = [ - .font: font, - .foregroundColor: textColor + .font: watermark.font, + .foregroundColor: watermark.textColor ] let subtextAttributes: [NSAttributedString.Key: Any] = [ - .font: font, - .foregroundColor: textColor + .font: watermark.font, + .foregroundColor: watermark.textColor ] - let textSize = text.size(withAttributes: textAttributes) - let subtextSize = subtext.size(withAttributes: subtextAttributes) + let textSize = watermark.text.size(withAttributes: textAttributes) + let subtextSize = watermark.subtext.size(withAttributes: subtextAttributes) - let stepX = textSize.width + columnSpacing - let stepY = textSize.height + lineSpacing + let stepX = textSize.width + watermark.columnSpacing + let stepY = textSize.height + watermark.lineSpacing - let stepSubtextX = subtextSize.width + columnSpacing - let stepSubTextY = subtextSize.height + lineSpacing + let stepSubtextX = subtextSize.width + watermark.columnSpacing + let stepSubTextY = subtextSize.height + watermark.lineSpacing let rotatedDiagonal = sqrt(rect.width * rect.width + rect.height * rect.height) let startX = -rotatedDiagonal - let startY = -rotatedDiagonal + marginY + let startY = -rotatedDiagonal + watermark.marginY let endX = rotatedDiagonal - let endY = rotatedDiagonal - marginY + let endY = rotatedDiagonal - watermark.marginY var y = startY while y <= endY { @@ -155,10 +178,10 @@ public class ConfidentialContentView: UIView, Themeable { var col = 0 while x <= endX { if col % 2 == 0 { - text.draw(at: CGPoint(x: x, y: y), withAttributes: textAttributes) + watermark.text.draw(at: CGPoint(x: x, y: y), withAttributes: textAttributes) x += stepX } else { - subtext.draw(at: CGPoint(x: x, y: y + (stepSubTextY / 2)), withAttributes: subtextAttributes) + watermark.subtext.draw(at: CGPoint(x: x, y: y + (stepSubTextY / 2)), withAttributes: subtextAttributes) x += stepSubtextX } col += 1 @@ -166,13 +189,13 @@ public class ConfidentialContentView: UIView, Themeable { y += stepY } - context.restoreGState() + restoreGState() - if angle < 45.0 { - let combinedText = "\(subtext) - \(text)" + if watermark.angle < 45.0 { + let combinedText = "\(watermark.subtext) - \(watermark.text)" let combinedTextAttributes: [NSAttributedString.Key: Any] = [ - .font: subtitleFont, - .foregroundColor: subtitleTextColor + .font: watermark.subtitleFont, + .foregroundColor: watermark.subtitleTextColor ] let combinedTextSize = combinedText.size(withAttributes: combinedTextAttributes) @@ -180,42 +203,31 @@ public class ConfidentialContentView: UIView, Themeable { let subtextY = rect.height - combinedTextSize.height - 2 while x < rect.width { combinedText.draw(at: CGPoint(x: x, y: subtextY), withAttributes: combinedTextAttributes) - x += combinedTextSize.width + columnSpacing + x += combinedTextSize.width + watermark.columnSpacing } } - } - public func applyThemeCollection(theme: Theme, collection: ThemeCollection, event: ThemeEvent) { - if let color = collection.css.getColor(.stroke, selectors: [.confidentialLabel], for: nil) { - textColor = color - } - if let color = collection.css.getColor(.stroke, selectors: [.confidentialSecondaryLabel], for: nil) { - subtitleTextColor = color - } - - drawn = false - setNeedsDisplay() + UIGraphicsPopContext() } } public extension UIView { - - func secureView(core: OCCore?) { - let overlayView = ConfidentialContentView() - overlayView.text = core?.bookmark.user?.emailAddress ?? "Confidential View" - overlayView.subtext = core?.bookmark.userName ?? "Confidential View" - overlayView.backgroundColor = .clear - overlayView.translatesAutoresizingMaskIntoConstraints = false - overlayView.angle = (self.frame.height <= 200) ? 10 : 45 - - self.addSubview(overlayView) - - NSLayoutConstraint.activate([ - overlayView.topAnchor.constraint(equalTo: self.topAnchor), - overlayView.bottomAnchor.constraint(equalTo: self.bottomAnchor), - overlayView.leadingAnchor.constraint(equalTo: self.leadingAnchor), - overlayView.trailingAnchor.constraint(equalTo: self.trailingAnchor) - ]) + func secureView(core: OCCore?, useLayer: Bool = false) { + if !ConfidentialManager.shared.markConfidentialViews { return } + + let watermark = Watermark(text: core?.bookmark.user?.emailAddress ?? "Confidential View", subtext: core?.bookmark.userName ?? "Confidential View", angle: (frame.height <= 200) ? 10 : 45) + + if useLayer { + let overlayLayer = ConfidentialContentLayer(watermark: watermark) + overlayLayer.frame = layer.bounds + layer.addSublayer(overlayLayer) + } else { + let overlayView = ConfidentialContentView() + overlayView.watermark = watermark + overlayView.backgroundColor = .clear + overlayView.translatesAutoresizingMaskIntoConstraints = false + embed(toFillWith: overlayView) + } } var withScreenshotProtection: UIView { diff --git a/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift b/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift index 05a4336f6..73f0b06e6 100644 --- a/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift +++ b/ownCloudAppShared/User Interface/Confidential/SecureTextField.swift @@ -47,11 +47,6 @@ class SecureTextField : UITextField { // If screenshot protection was not possible, force close the application. exit(0) - - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - - return view } override var canBecomeFirstResponder: Bool { false }