From 1a607bbbda3e39f18bb1dc52e62d1b11d27a08af Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Fri, 22 Jul 2022 13:38:10 +0200 Subject: [PATCH 01/45] Moved Coordinator to CryptomatorCommonCore --- Cryptomator/Common/ChildCoordinator.swift | 1 + .../AccountListViewController.swift | 1 + Cryptomator/Common/Coordinator.swift | 7 ------- .../Common/LocalWeb/LocalWebViewController.swift | 1 + Cryptomator/Common/PoppingCloseCoordinator.swift | 2 ++ Cryptomator/S3/S3Authenticator+VC.swift | 1 + .../CryptomatorCommonCore/Coordinator.swift | 15 +++++++++++++++ 7 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Coordinator.swift diff --git a/Cryptomator/Common/ChildCoordinator.swift b/Cryptomator/Common/ChildCoordinator.swift index 174634cac..b2a35d185 100644 --- a/Cryptomator/Common/ChildCoordinator.swift +++ b/Cryptomator/Common/ChildCoordinator.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCommonCore import Foundation protocol ChildCoordinator: Coordinator { diff --git a/Cryptomator/Common/CloudAccountList/AccountListViewController.swift b/Cryptomator/Common/CloudAccountList/AccountListViewController.swift index 8326f5ef3..52bf024b8 100644 --- a/Cryptomator/Common/CloudAccountList/AccountListViewController.swift +++ b/Cryptomator/Common/CloudAccountList/AccountListViewController.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCommon import CryptomatorCommonCore import Foundation import Promises diff --git a/Cryptomator/Common/Coordinator.swift b/Cryptomator/Common/Coordinator.swift index fd78c21e5..d416ab674 100644 --- a/Cryptomator/Common/Coordinator.swift +++ b/Cryptomator/Common/Coordinator.swift @@ -10,13 +10,6 @@ import CocoaLumberjackSwift import CryptomatorCommonCore import UIKit -protocol Coordinator: AnyObject { - var childCoordinators: [Coordinator] { get set } - var navigationController: UINavigationController { get set } - - func start() -} - extension Coordinator { func handleError(_ error: Error, for viewController: UIViewController) { DDLogError("Error: \(error)") diff --git a/Cryptomator/Common/LocalWeb/LocalWebViewController.swift b/Cryptomator/Common/LocalWeb/LocalWebViewController.swift index 1f297407b..32084aba1 100644 --- a/Cryptomator/Common/LocalWeb/LocalWebViewController.swift +++ b/Cryptomator/Common/LocalWeb/LocalWebViewController.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCommonCore import Foundation import UIKit import WebKit diff --git a/Cryptomator/Common/PoppingCloseCoordinator.swift b/Cryptomator/Common/PoppingCloseCoordinator.swift index 2331f7f11..baa1fe3c4 100644 --- a/Cryptomator/Common/PoppingCloseCoordinator.swift +++ b/Cryptomator/Common/PoppingCloseCoordinator.swift @@ -6,7 +6,9 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCommonCore import UIKit + protocol PoppingCloseCoordinator: Coordinator { var oldTopViewController: UIViewController? { get } } diff --git a/Cryptomator/S3/S3Authenticator+VC.swift b/Cryptomator/S3/S3Authenticator+VC.swift index f4cd6ea4f..caa9999d0 100644 --- a/Cryptomator/S3/S3Authenticator+VC.swift +++ b/Cryptomator/S3/S3Authenticator+VC.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import CryptomatorCommonCore import Promises import UIKit diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Coordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Coordinator.swift new file mode 100644 index 000000000..7a5351dbe --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Coordinator.swift @@ -0,0 +1,15 @@ +// +// Coordinator.swift +// CryptomatorCommon +// +// Created by Philipp Schmid on 04.01.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import UIKit +public protocol Coordinator: AnyObject { + var childCoordinators: [Coordinator] { get set } + var navigationController: UINavigationController { get set } + + func start() +} From 39d53be102ba872fffa96aaef4164db1274f605c Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sat, 23 Jul 2022 13:27:48 +0200 Subject: [PATCH 02/45] Preliminary commit to add support for Cryptomator hub --- Cryptomator.xcodeproj/project.pbxproj | 17 +- .../xcshareddata/swiftpm/Package.resolved | 14 +- .../AddVault/AddVaultSuccessCoordinator.swift | 3 +- ...ExistingLegacyVaultPasswordViewModel.swift | 55 ++- .../OpenExistingVaultCoordinator.swift | 78 +++- .../OpenExistingVaultPasswordViewModel.swift | 60 +-- Cryptomator/Common/Coordinator.swift | 27 -- CryptomatorCommon/Package.swift | 2 +- .../AddHubVaultViewModel.swift | 176 +++++++ .../CryptomatorHubAuthenticator.swift | 179 +++++++ .../CryptomatorHubCoordinator.swift | 61 +++ .../CryptomatorCommon/Placeholder.swift | 9 - .../CryptomatorCommonCore/Coordinator.swift | 21 + .../CryptomatorDatabase.swift | 14 + .../CryptomatorKeychain.swift | 1 + .../FileProviderXPC/VaultUnlocking.swift | 30 ++ .../Hub/AddHubVaultView.swift | 53 +++ .../Hub/CryptomatorHubAuthenticator.swift | 164 +++++++ .../Hub/CryptomatorHubKeyProvider.swift | 40 ++ .../Hub/HubVaultCoordinator.swift | 64 +++ .../Hub/HubVaultUnlockViewModel.swift | 49 ++ .../Hub/HubVaultViewController.swift | 37 ++ .../Hub/HubVaultViewModel.swift | 129 ++++++ .../CryptomatorCommonCore/JWEHelper.swift | 32 ++ .../Manager/HubAccountManager.swift | 155 +++++++ .../Manager/VaultDBCache.swift | 2 +- .../Manager/VaultDBManager.swift | 174 ++++++- .../Mocks/VaultManagerMock.swift | 437 +++++++++--------- .../FileProviderAdapterManager.swift | 20 + .../VaultUnlockingServiceSource.swift | 22 + .../FileProviderCoordinator.swift | 83 +++- 31 files changed, 1876 insertions(+), 332 deletions(-) delete mode 100644 Cryptomator/Common/Coordinator.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubCoordinator.swift delete mode 100644 CryptomatorCommon/Sources/CryptomatorCommon/Placeholder.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/AddHubVaultView.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubKeyProvider.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultCoordinator.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewController.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/HubAccountManager.swift diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 64419c03e..c6d6e8a6d 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 4A03255525A3685500E63D7A /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03255425A3685500E63D7A /* Coordinator.swift */; }; 4A03255E25A368BF00E63D7A /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03255D25A368BF00E63D7A /* MainCoordinator.swift */; }; 4A03257825A36A6900E63D7A /* VaultListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03257725A36A6900E63D7A /* VaultListViewController.swift */; }; 4A03258125A36B7D00E63D7A /* UIViewController+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A03258025A36B7D00E63D7A /* UIViewController+Preview.swift */; }; @@ -532,7 +531,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 4A03255425A3685500E63D7A /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 4A03255D25A368BF00E63D7A /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = ""; }; 4A03257725A36A6900E63D7A /* VaultListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListViewController.swift; sourceTree = ""; }; 4A03258025A36B7D00E63D7A /* UIViewController+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Preview.swift"; sourceTree = ""; }; @@ -1478,7 +1476,6 @@ 4A644B56267C958F008CBB9A /* ChildCoordinator.swift */, 4AFCE53925B9D6A60069C4FC /* CloudAuthenticator.swift */, 4AFCE51E25B89CD80069C4FC /* CloudProviderType+Localization.swift */, - 4A03255425A3685500E63D7A /* Coordinator.swift */, 4AF91CE125A7234500ACF01E /* DatabaseManager.swift */, 4A8A6423286CA72B001F5EB9 /* DefaultShowEditAccountBehavior.swift */, 4A512D69274277FF00DC26F8 /* EditableDataSource.swift */, @@ -2112,9 +2109,12 @@ buildRules = ( ); dependencies = ( + 4AC4C98E288AD858008C6D2B /* PBXTargetDependency */, 4A9BED69268F379300721BAA /* PBXTargetDependency */, ); name = FileProviderExtensionUI; + packageProductDependencies = ( + ); productName = "File Provider ExtensionUI"; productReference = 4AA621E4249A6A8400A0BCBD /* FileProviderExtensionUI.appex */; productType = "com.apple.product-type.app-extension"; @@ -2421,7 +2421,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ -f ./.cloud-access-secrets.sh ]; then\n source ./.cloud-access-secrets.sh\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:2 string ${ONEDRIVE_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n"; + shellScript = "if [ -f ./.cloud-access-secrets.sh ]; then\n source ./.cloud-access-secrets.sh\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:2 string ${ONEDRIVE_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:3 string ${HUB_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n"; }; 742595D72552EE0000A8A008 /* Set Build Number */ = { isa = PBXShellScriptBuildPhase; @@ -2736,7 +2736,6 @@ 4A5AC43D275A306F00342AA7 /* TrialExpiredNavigationController.swift in Sources */, 4A53CC13267CC1C100853BB3 /* CreateNewVaultPasswordViewController.swift in Sources */, 4A6A51FF268B1BEB006F7368 /* OpenExistingLocalVaultCoordinator.swift in Sources */, - 4A03255525A3685500E63D7A /* Coordinator.swift in Sources */, 4AED9A77286B4BEE00352951 /* S3AuthenticationViewController.swift in Sources */, 4A644B47267A3D43008CBB9A /* SetVaultNameViewModel.swift in Sources */, 4A1EB0D02689C7F8006D072B /* DetectedVaultFailureView.swift in Sources */, @@ -2960,6 +2959,10 @@ target = 740375D62587AE7A0023FF53 /* CryptomatorFileProvider */; targetProxy = 4A9BED68268F379300721BAA /* PBXContainerItemProxy */; }; + 4AC4C98E288AD858008C6D2B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = 4AC4C98D288AD858008C6D2B /* AppAuth */; + }; 4AD3D7DC282EBDE7008188CD /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4AD3D7D3282EBDE7008188CD /* CryptomatorIntents */; @@ -3677,6 +3680,10 @@ isa = XCSwiftPackageProductDependency; productName = CryptomatorCommonCore; }; + 4AC4C98D288AD858008C6D2B /* AppAuth */ = { + isa = XCSwiftPackageProductDependency; + productName = AppAuth; + }; 4AED9A6E286B38DA00352951 /* Introspect */ = { isa = XCSwiftPackageProductDependency; package = 4AED9A6D286B38D900352951 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; diff --git a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e4cc287d1..067c6a7ac 100644 --- a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,9 +41,9 @@ "package": "CryptomatorCloudAccess", "repositoryURL": "https://github.com/cryptomator/cloud-access-swift.git", "state": { - "branch": null, - "revision": "d39cc8bd3763755158bc8fda25fadca3fb308130", - "version": "1.5.0" + "branch": "feature/hub-poc", + "revision": "d0dc2f85644ba8ee9afb1c556035b215073258f0", + "version": null } }, { @@ -111,11 +111,11 @@ }, { "package": "JOSESwift", - "repositoryURL": "https://github.com/airsidemobile/JOSESwift.git", + "repositoryURL": "https://github.com/tobihagemann/JOSESwift.git", "state": { - "branch": null, - "revision": "10ed3b6736def7c26eb87135466b1cb46ea7e37f", - "version": "2.4.0" + "branch": "feature/JWE-ECDH-GCM", + "revision": "e851667a4e6f6e8411d21474e77442041025e93c", + "version": null } }, { diff --git a/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift b/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift index fde389dc8..e04a118fd 100644 --- a/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift +++ b/Cryptomator/AddVault/AddVaultSuccessCoordinator.swift @@ -28,9 +28,8 @@ class AddVaultSuccessCoordinator: AddVaultSuccesing, Coordinator { let viewModel = AddVaultSuccessViewModel(vaultName: vaultName, vaultUID: vaultUID) let successVC = AddVaultSuccessViewController(viewModel: viewModel) successVC.coordinator = self - navigationController.pushViewController(successVC, animated: true) // Remove the previous ViewControllers so that the user cannot navigate to the previous screens. - navigationController.viewControllers = [successVC] + navigationController.setViewControllers([successVC], animated: true) } // MARK: - AddVaultSuccesing diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift index e90cfd352..c4f7b9408 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingLegacyVaultPasswordViewModel.swift @@ -12,8 +12,59 @@ import CryptomatorCommonCore import Foundation import Promises -class OpenExistingLegacyVaultPasswordViewModel: OpenExistingVaultPasswordViewModel { - override func addVault() -> Promise { +class OpenExistingLegacyVaultPasswordViewModel: SingleSectionTableViewModel, OpenExistingVaultPasswordViewModelProtocol { + var lastReturnButtonPressed: AnyPublisher { + return setupReturnButtonSupport(for: [passwordCellViewModel], subscribers: &subscribers) + } + + override var title: String? { + return LocalizedString.getValue("addVault.openExistingVault.title") + } + + override var cells: [TableViewCellViewModel] { + return [passwordCellViewModel] + } + + var enableVerifyButton: AnyPublisher { + return passwordCellViewModel.input.$value.map { input in + return !input.isEmpty + }.eraseToAnyPublisher() + } + + let provider: CloudProvider + let account: CloudProviderAccount + + let vault: VaultItem + var vaultName: String { + return vault.name + } + + let vaultUID: String + let passwordCellViewModel = TextFieldCellViewModel(type: .password, isInitialFirstResponder: true) + var password: String { + return passwordCellViewModel.input.value + } + + let downloadedMasterkeyFile: DownloadedMasterkeyFile + + private lazy var subscribers = Set() + + init(provider: CloudProvider, account: CloudProviderAccount, vault: VaultItem, vaultUID: String, downloadedMasterkeyFile: DownloadedMasterkeyFile) { + self.provider = provider + self.account = account + self.vault = vault + self.vaultUID = vaultUID + self.downloadedMasterkeyFile = downloadedMasterkeyFile + } + + func addVault() -> Promise { return VaultDBManager.shared.createLegacyFromExisting(withVaultUID: vaultUID, delegateAccountUID: account.accountUID, vaultItem: vault, password: password, storePasswordInKeychain: false) } + + override func getFooterTitle(for section: Int) -> String? { + guard section == 0 else { + return nil + } + return String(format: LocalizedString.getValue("addVault.openExistingVault.password.footer"), vaultName) + } } diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift index d4a8588f0..72d137348 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift @@ -6,10 +6,13 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import AppAuth import CocoaLumberjackSwift import CryptomatorCloudAccessCore +import CryptomatorCommon import CryptomatorCommonCore import Foundation +import Promises import UIKit class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator { @@ -121,20 +124,76 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder } func chooseItem(_ item: Item) { - let viewModel: OpenExistingVaultPasswordViewModelProtocol guard let vaultItem = item as? VaultDetailItem else { handleError(VaultCoordinatorError.wrongItemType, for: navigationController) return } + if vaultItem.isLegacyVault { - viewModel = OpenExistingLegacyVaultPasswordViewModel(provider: provider, account: account, vault: vaultItem, vaultUID: UUID().uuidString) + showAddExistingLegacyVault(vaultItem) } else { - viewModel = OpenExistingVaultPasswordViewModel(provider: provider, account: account, vault: vaultItem, vaultUID: UUID().uuidString) + let hud = ProgressHUD() + hud.text = "Downloading Vault…" + hud.show(presentingViewController: navigationController) + VaultDBManager.shared.getUnverifiedVaultConfig(delegateAccountUID: account.accountUID, vaultItem: vaultItem).then { downloadedVaultConfig in + all(hud.dismiss(animated: true), Promise(downloadedVaultConfig)) + }.then { _, downloadedVaultConfig in + self.processDownloadedVaultConfig(downloadedVaultConfig, vaultItem: vaultItem) + }.catch { error in + hud.dismiss(animated: true).then { + self.handleError(error, for: self.navigationController) + } + } } + } + + private func showAddExistingLegacyVault(_ vault: VaultItem) { + let hud = ProgressHUD() + hud.text = "Downloading Vault…" + hud.show(presentingViewController: navigationController) + VaultDBManager.shared.downloadMasterkeyFile(delegateAccountUID: account.accountUID, vaultItem: vault).then { downloadedMasterkeyFile in + all(hud.dismiss(animated: true), Promise(downloadedMasterkeyFile)) + }.then { _, downloadedMasterkeyFile in + let viewModel = OpenExistingLegacyVaultPasswordViewModel(provider: self.provider, + account: self.account, + vault: vault, + vaultUID: UUID().uuidString, + downloadedMasterkeyFile: downloadedMasterkeyFile) + let passwordVC = OpenExistingVaultPasswordViewController(viewModel: viewModel) + passwordVC.coordinator = self + self.navigationController.pushViewController(passwordVC, animated: true) + }.catch { error in + hud.dismiss(animated: true).then { + self.handleError(error, for: self.navigationController) + } + } + } - let passwordVC = OpenExistingVaultPasswordViewController(viewModel: viewModel) - passwordVC.coordinator = self - navigationController.pushViewController(passwordVC, animated: true) + private func processDownloadedVaultConfig(_ downloadedVaultConfig: DownloadedVaultConfig, vaultItem: VaultItem) { + switch VaultConfigHelper.getType(for: downloadedVaultConfig.vaultConfig) { + case .masterkeyFile: + handleMasterkeyFileVaultConfig(downloadedVaultConfig, vaultItem: vaultItem) + case .hub: + handleHubVaultConfig(downloadedVaultConfig, vaultItem: vaultItem) + case .unknown: + fatalError("TODO: Display unsupported vault config error") + } + } + + private func handleMasterkeyFileVaultConfig(_ downloadedVaultConfig: DownloadedVaultConfig, vaultItem: VaultItem) { + VaultDBManager.shared.downloadMasterkeyFile(delegateAccountUID: account.accountUID, vaultItem: vaultItem).then { downloadedMasterkeyFile in + let viewModel = OpenExistingVaultPasswordViewModel(provider: self.provider, account: self.account, vault: vaultItem, vaultUID: UUID().uuidString, downloadedVaultConfig: downloadedVaultConfig, downloadedMasterkeyFile: downloadedMasterkeyFile) + let passwordVC = OpenExistingVaultPasswordViewController(viewModel: viewModel) + passwordVC.coordinator = self + self.navigationController.pushViewController(passwordVC, animated: true) + } + } + + private func handleHubVaultConfig(_ downloadedVaultConfig: DownloadedVaultConfig, vaultItem: VaultItem) { + let child = CryptomatorHubCoordinator(vaultItem: vaultItem, accountUID: account.accountUID, downloadedVaultConfig: downloadedVaultConfig, navigationController: navigationController) + child.parentCoordinator = self + childCoordinators.append(child) + child.start() } func showCreateNewFolder(parentPath: CloudPath) {} @@ -149,9 +208,16 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder // MARK: - VaultInstalling func showSuccessfullyAddedVault(withName name: String, vaultUID: String) { + print("showSuccessfullyAddedVault") let child = AddVaultSuccessCoordinator(vaultName: name, vaultUID: vaultUID, navigationController: navigationController) child.parentCoordinator = self childCoordinators.append(child) child.start() } } + +extension AuthenticatedOpenExistingVaultCoordinator: CryptomatorHubCoordinatorDelegate { + func addedVault(withName name: String, vaultUID: String) { + showSuccessfullyAddedVault(withName: name, vaultUID: vaultUID) + } +} diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift index 9f5df55cf..e29ba053e 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultPasswordViewModel.swift @@ -16,60 +16,22 @@ protocol OpenExistingVaultPasswordViewModelProtocol: SingleSectionTableViewModel var vaultName: String { get } var vaultUID: String { get } var enableVerifyButton: AnyPublisher { get } - // This function is later no longer asynchronous func addVault() -> Promise } -class OpenExistingVaultPasswordViewModel: SingleSectionTableViewModel, OpenExistingVaultPasswordViewModelProtocol { - var lastReturnButtonPressed: AnyPublisher { - return setupReturnButtonSupport(for: [passwordCellViewModel], subscribers: &subscribers) - } - - override var title: String? { - return LocalizedString.getValue("addVault.openExistingVault.title") - } - - override var cells: [TableViewCellViewModel] { - return [passwordCellViewModel] - } - - var enableVerifyButton: AnyPublisher { - return passwordCellViewModel.input.$value.map { input in - return !input.isEmpty - }.eraseToAnyPublisher() - } - - let provider: CloudProvider - let account: CloudProviderAccount - - let vault: VaultItem - var vaultName: String { - return vault.name - } - - let vaultUID: String - let passwordCellViewModel = TextFieldCellViewModel(type: .password, isInitialFirstResponder: true) - var password: String { - return passwordCellViewModel.input.value - } - - private lazy var subscribers = Set() - - init(provider: CloudProvider, account: CloudProviderAccount, vault: VaultItem, vaultUID: String) { - self.provider = provider - self.account = account - self.vault = vault - self.vaultUID = vaultUID - } +class OpenExistingVaultPasswordViewModel: OpenExistingLegacyVaultPasswordViewModel { + let downloadedVaultConfig: DownloadedVaultConfig - func addVault() -> Promise { - return VaultDBManager.shared.createFromExisting(withVaultUID: vaultUID, delegateAccountUID: account.accountUID, vaultItem: vault, password: password, storePasswordInKeychain: false) + init(provider: CloudProvider, account: CloudProviderAccount, vault: VaultItem, vaultUID: String, downloadedVaultConfig: DownloadedVaultConfig, downloadedMasterkeyFile: DownloadedMasterkeyFile) { + self.downloadedVaultConfig = downloadedVaultConfig + super.init(provider: provider, + account: account, + vault: vault, + vaultUID: vaultUID, + downloadedMasterkeyFile: downloadedMasterkeyFile) } - override func getFooterTitle(for section: Int) -> String? { - guard section == 0 else { - return nil - } - return String(format: LocalizedString.getValue("addVault.openExistingVault.password.footer"), vaultName) + override func addVault() -> Promise { + return VaultDBManager.shared.createFromExisting(withVaultUID: vaultUID, delegateAccountUID: account.accountUID, downloadedVaultConfig: downloadedVaultConfig, downloadedMasterkey: downloadedMasterkeyFile, vaultItem: vault, password: password) } } diff --git a/Cryptomator/Common/Coordinator.swift b/Cryptomator/Common/Coordinator.swift deleted file mode 100644 index d416ab674..000000000 --- a/Cryptomator/Common/Coordinator.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Coordinator.swift -// Cryptomator -// -// Created by Philipp Schmid on 04.01.21. -// Copyright © 2021 Skymatic GmbH. All rights reserved. -// - -import CocoaLumberjackSwift -import CryptomatorCommonCore -import UIKit - -extension Coordinator { - func handleError(_ error: Error, for viewController: UIViewController) { - DDLogError("Error: \(error)") - let alertController = UIAlertController(title: LocalizedString.getValue("common.alert.error.title"), message: error.localizedDescription, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: LocalizedString.getValue("common.button.ok"), style: .default)) - viewController.present(alertController, animated: true) - } - - func childDidFinish(_ child: Coordinator?) { - for (index, coordinator) in childCoordinators.enumerated() where coordinator === child { - childCoordinators.remove(at: index) - break - } - } -} diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index 3623801f0..c1ad8d21f 100644 --- a/CryptomatorCommon/Package.swift +++ b/CryptomatorCommon/Package.swift @@ -26,7 +26,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/cryptomator/cloud-access-swift.git", .upToNextMinor(from: "1.5.0")), + .package(url: "https://github.com/cryptomator/cloud-access-swift.git", .branch("feature/hub-poc")), .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", .upToNextMinor(from: "3.7.0")) ], targets: [ diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift new file mode 100644 index 000000000..40a2a6082 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift @@ -0,0 +1,176 @@ +// +// AddHubVaultViewModel.swift +// Cryptomator +// +// Created by Philipp Schmid on 21.07.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import AppAuthCore +import CryptoKit +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import CryptomatorCryptoLib +import FileProvider +import Foundation +import JOSESwift +import Promises + +class AddHubVaultViewModel: HubVaultViewModel, HubVaultAdding { + let downloadedVaultConfig: DownloadedVaultConfig + let vaultItem: VaultItem + let vaultManager: VaultManager + let delegateAccountUID: String + let vaultUID: String + private weak var addHubVaultCoordinator: AddHubVaultCoordinator? + + init(downloadedVaultConfig: DownloadedVaultConfig, vaultItem: VaultItem, vaultUID: String, delegateAccountUID: String, vaultManager: VaultManager = VaultDBManager.shared, coordinator: (HubVaultCoordinator & AddHubVaultCoordinator)? = nil) { + self.downloadedVaultConfig = downloadedVaultConfig + self.vaultItem = vaultItem + self.vaultUID = vaultUID + self.delegateAccountUID = delegateAccountUID + self.vaultManager = vaultManager + self.addHubVaultCoordinator = coordinator + super.init(initialState: .detectedVault, vaultConfig: downloadedVaultConfig.vaultConfig, coordinator: coordinator) + } + + func login() { + error = nil + let vaultConfig = downloadedVaultConfig.vaultConfig + guard let hubConfig = vaultConfig.hub else { + error = AddHubVaultViewModelError.missingHubConfig + return + } + Task { + do { + guard let authState = try await addHubVaultCoordinator?.authenticate(with: hubConfig) else { + setError(to: AddHubVaultViewModelError.missingAuthState) + return + } + self.authState = authState + continueToAccessCheck() + } catch { + setError(to: error) + } + } + } + + override func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) { + addVault(jwe: jwe, privateKey: privateKey, hubAccount: hubAccount) + } + + private func addVault(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) { + vaultManager.addExistingHubVault(vaultUID: vaultUID, + delegateAccountUID: delegateAccountUID, + hubUserID: hubAccount.userID, + jweData: jwe.compactSerializedData, + privateKey: privateKey, + vaultItem: vaultItem, + downloadedVaultConfig: downloadedVaultConfig).then { + self.addHubVaultCoordinator?.addedVault(withName: self.vaultItem.name, vaultUID: self.vaultUID) + }.catch { error in + self.setError(to: error) + } + } +} + +/* + public class HubVaultViewModel: ObservableObject { + fileprivate(set) var authState: OIDAuthState? + @Published var state: AddHubVaultViewModelState + @Published var deviceName: String = "" + @Published var error: Error? + weak var coordinator: HubVaultCoordinator? + let vaultConfig: UnverifiedVaultConfig + + init(initialState: AddHubVaultViewModelState, vaultConfig: UnverifiedVaultConfig, coordinator: HubVaultCoordinator? = nil) { + self.state = initialState + self.vaultConfig = vaultConfig + self.coordinator = coordinator + } + + func register() { + error = nil + guard let hubConfig = vaultConfig.hub else { + error = AddHubVaultViewModelError.missingHubConfig + return + } + guard let authState = authState else { + error = AddHubVaultViewModelError.missingAuthState + return + } + + Task { + do { + try await CryptomatorHubAuthenticator.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState) + } catch { + setError(to: error) + return + } + setState(to: .deviceRegisteredSuccessfully) + } + } + + func continueToAccessCheck() { + setError(to: nil) + guard let authState = authState else { + setError(to: AddHubVaultViewModelError.missingAuthState) + return + } + setState(to: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait.")) + Task { + let authFlow: HubAuthenticationFlow + do { + authFlow = try await CryptomatorHubAuthenticator.receiveKey(authState: authState, vaultConfig: vaultConfig) + } catch { + setError(to: error) + return + } + switch authFlow { + case .receivedExistingKey(let data): + receivedExistingKey(data: data) + case .accessNotGranted: + setState(to: .accessNotGranted) + case .needsDeviceRegistration: + setState(to: .needsDeviceRegistration) + } + } + } + + func refresh() { + continueToAccessCheck() + } + + func receivedExistingKey(data: Data) { + let privateKey: P384.KeyAgreement.PrivateKey + let jwe: JWE + let hubAccount: HubAccount + do { + privateKey = try CryptomatorHubKeyProvider.shared.getPrivateKey() + jwe = try JWE(compactSerialization: data) + hubAccount = try HubAccount(authState: authState!) + try HubAccountManager.shared.saveHubAccount(hubAccount) + } catch { + setError(to: error) + return + } + receivedExistingKey(jwe: jwe, privateKey: privateKey, hubAccount: hubAccount) + } + + func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) { + fatalError("Abstract method receivedExistingKey(jwe:privateKey:hubAccount:) not implemented") + } + + func setState(to newState: AddHubVaultViewModelState) { + DispatchQueue.main.async { + self.state = newState + } + } + + func setError(to newError: Error?) { + DispatchQueue.main.async { + self.error = newError + } + } + } + */ diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator.swift new file mode 100644 index 000000000..2b2bdc1fc --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator.swift @@ -0,0 +1,179 @@ +// +// CryptomatorHubAuthenticator.swift +// +// +// Created by Philipp Schmid on 22.07.22. +// + +import AppAuth +import Base32 +import CryptoKit +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import UIKit + +extension CryptomatorHubAuthenticator: HubAuthenticating { + private static var currentAuthorizationFlow: OIDExternalUserAgentSession? + public func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState { + guard let authorizationEndpoint = URL(string: hubConfig.authEndpoint) else { + fatalError("TODO: throw error") + } + guard let tokenEndpoint = URL(string: hubConfig.tokenEndpoint) else { + fatalError("TODO: throw error") + } + guard let redirectURL = URL(string: "hub.org.cryptomator.ios:/auth") else { + fatalError("TODO: throw error") + } + let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint, + tokenEndpoint: tokenEndpoint) + + let request = OIDAuthorizationRequest(configuration: configuration, clientId: hubConfig.clientId, scopes: nil, redirectURL: redirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil) + return try await withCheckedThrowingContinuation({ continuation in + DispatchQueue.main.async { + CryptomatorHubAuthenticator.currentAuthorizationFlow = + OIDAuthState.authState(byPresenting: request, presenting: viewController) { authState, error in + switch (authState, error) { + case let (.some(authState), nil): + continuation.resume(returning: authState) + case let (nil, .some(error)): + continuation.resume(throwing: error) + default: + continuation.resume(throwing: CryptomatorHubAuthenticatorError.unexpectedError) + } + } + } + }) + } +} + +public protocol HubAuthenticating { + func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState +} + +/* + public class CryptomatorHubAuthenticator { + private static let scheme = "hub+" + private static var currentAuthorizationFlow: OIDExternalUserAgentSession? + public static func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState { + guard let authorizationEndpoint = URL(string: hubConfig.authEndpoint) else { + fatalError("TODO: throw error") + } + guard let tokenEndpoint = URL(string: hubConfig.tokenEndpoint) else { + fatalError("TODO: throw error") + } + guard let redirectURL = URL(string: "hub.org.cryptomator.ios:/auth") else { + fatalError("TODO: throw error") + } + let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint, + tokenEndpoint: tokenEndpoint) + + let request = OIDAuthorizationRequest(configuration: configuration, clientId: hubConfig.clientId, scopes: nil, redirectURL: redirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil) + return try await withCheckedThrowingContinuation({ continuation in + DispatchQueue.main.async { + CryptomatorHubAuthenticator.currentAuthorizationFlow = + OIDAuthState.authState(byPresenting: request, presenting: viewController) { authState, error in + switch (authState, error) { + case let (.some(authState), nil): + continuation.resume(returning: authState) + case let (nil, .some(error)): + continuation.resume(throwing: error) + default: + continuation.resume(throwing: CryptomatorHubAuthenticatorError.unexpectedError) + } + } + } + }) + } + + public static func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { + guard let baseURL = createBaseURL(vaultConfig: vaultConfig) else { + fatalError("TODO throw error") + } + let deviceID = try getDeviceID() + let url = baseURL.appendingPathComponent("/keys").appendingPathComponent("/\(deviceID)") + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + fatalError("TODO throw error") + } + var urlRequest = URLRequest(url: url) + urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (data, response) = try await URLSession.shared.data(with: urlRequest) + switch (response as? HTTPURLResponse)?.statusCode { + case 200: + return .receivedExistingKey(data) + case 403: + return .accessNotGranted + case 404: + return .needsDeviceRegistration + default: + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + } + + static func createBaseURL(vaultConfig: UnverifiedVaultConfig) -> URL? { + guard let keyId = vaultConfig.keyId, keyId.hasPrefix(scheme) else { + return nil + } + let baseURLPath = keyId.deletingPrefix(scheme) + return URL(string: baseURLPath) + } + + static func getDeviceID() throws -> String { + let publicKey = try CryptomatorHubKeyProvider.shared.getPublicKey() + let digest: SHA256.Digest + if #available(iOS 14.0, *) { + digest = SHA256.hash(data: publicKey.derRepresentation) + } else { + fatalError("TODO: Increase the minimum deployment target or change representation") + } + return digest.data.base16EncodedString + } + + public static func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws { + let deviceID = try getDeviceID() + let publicKey = try CryptomatorHubKeyProvider.shared.getPublicKey() + let derPubKey: Data + if #available(iOS 14.0, *) { + derPubKey = publicKey.derRepresentation + } else { + fatalError("TODO: Increase the minimum deployment target or change representation") + } + let dto = CreateDeviceDto(id: deviceID, name: name, publicKey: derPubKey.base64URLEncodedString()) + guard let devicesResourceURL = URL(string: hubConfig.devicesResourceUrl) else { + fatalError("TODO: throw error") + } + let keyURL = devicesResourceURL.appendingPathComponent("\(deviceID)") + var request = URLRequest(url: keyURL) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(dto) + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + fatalError("TODO throw error") + } + request.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (_, response) = try await URLSession.shared.data(with: request) + switch (response as? HTTPURLResponse)?.statusCode { + case 201: + break + case 409: + throw CryptomatorHubAuthenticatorError.deviceNameAlreadyExists + default: + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + } + + struct CreateDeviceDto: Codable { + let id: String + let name: String + let publicKey: String + } + } + + extension String { + func deletingPrefix(_ prefix: String) -> String { + guard self.hasPrefix(prefix) else { return self } + return String(self.dropFirst(prefix.count)) + } + } + */ diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubCoordinator.swift new file mode 100644 index 000000000..93c4f13ec --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubCoordinator.swift @@ -0,0 +1,61 @@ +// +// CryptomatorHubCoordinator.swift +// +// +// Created by Philipp Schmid on 22.07.22. +// + +import AppAuthCore +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import UIKit + +public class CryptomatorHubCoordinator: Coordinator, HubVaultCoordinator, AddHubVaultCoordinator { + public lazy var childCoordinators = [Coordinator]() + public var navigationController: UINavigationController + public weak var parentCoordinator: Coordinator? + let vaultItem: VaultItem + let accountUID: String + let downloadedVaultConfig: DownloadedVaultConfig + + public init(vaultItem: VaultItem, accountUID: String, downloadedVaultConfig: DownloadedVaultConfig, navigationController: UINavigationController) { + self.accountUID = accountUID + self.downloadedVaultConfig = downloadedVaultConfig + self.vaultItem = vaultItem + self.navigationController = navigationController + } + + public func start() { + let viewModel = AddHubVaultViewModel(downloadedVaultConfig: downloadedVaultConfig, vaultItem: vaultItem, vaultUID: UUID().uuidString, delegateAccountUID: accountUID, coordinator: self) + let addHubVaultVC = HubVaultViewController(viewModel: viewModel) + navigationController.pushViewController(addHubVaultVC, animated: true) + } + + public func handleError(_ error: Error) { + handleError(error, for: navigationController) { + self.navigationController.popViewController(animated: true) + self.parentCoordinator?.childDidFinish(self) + } + } + + public func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState { + return try await CryptomatorHubAuthenticator.shared.authenticate(with: hubConfig, from: navigationController) + } + + public func addedVault(withName name: String, vaultUID: String) { + guard let delegate = parentCoordinator as? CryptomatorHubCoordinatorDelegate else { + return + } + delegate.addedVault(withName: name, vaultUID: vaultUID) + parentCoordinator?.childDidFinish(self) + } +} + +public protocol CryptomatorHubCoordinatorDelegate: AnyObject { + func addedVault(withName name: String, vaultUID: String) +} + +protocol AddHubVaultCoordinator: AnyObject { + func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState + func addedVault(withName name: String, vaultUID: String) +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/Placeholder.swift b/CryptomatorCommon/Sources/CryptomatorCommon/Placeholder.swift deleted file mode 100644 index 9589047d7..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommon/Placeholder.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// Placeholder.swift -// CryptomatorCommon -// -// Created by Philipp Schmid on 04.04.21. -// Copyright © 2020 Skymatic GmbH. All rights reserved. -// - -// Workaround to create an "empty" target for SPM diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Coordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Coordinator.swift index 7a5351dbe..dba176a72 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Coordinator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Coordinator.swift @@ -6,10 +6,31 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CocoaLumberjackSwift import UIKit + public protocol Coordinator: AnyObject { var childCoordinators: [Coordinator] { get set } var navigationController: UINavigationController { get set } func start() } + +public extension Coordinator { + func handleError(_ error: Error, for viewController: UIViewController, onOKTapped: (() -> Void)? = nil) { + DDLogError("Error: \(error)") + let alertController = UIAlertController(title: LocalizedString.getValue("common.alert.error.title"), message: error.localizedDescription, preferredStyle: .alert) + let okAction = UIAlertAction(title: LocalizedString.getValue("common.button.ok"), style: .default) { _ in + onOKTapped?() + } + alertController.addAction(okAction) + viewController.present(alertController, animated: true) + } + + func childDidFinish(_ child: Coordinator?) { + for (index, coordinator) in childCoordinators.enumerated() where coordinator === child { + childCoordinators.remove(at: index) + break + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift index ae543ed4d..080e92c41 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift @@ -44,6 +44,9 @@ public class CryptomatorDatabase { migrator.registerMigration("s3DisplayNameMigration") { db in try s3DisplayNameMigration(db) } + migrator.registerMigration("initialHubSupport") { db in + try initialHubSupportMigration(db) + } return migrator } @@ -152,6 +155,17 @@ public class CryptomatorDatabase { """) } + class func initialHubSupportMigration(_ db: Database) throws { + try db.create(table: "hubAccountInfo", body: { table in + table.column("userID", .text).primaryKey() + }) + try db.create(table: "hubVaultAccount", body: { table in + table.column("id", .integer).primaryKey() + table.column("vaultUID", .text).notNull().unique().references("vaultAccounts", onDelete: .cascade) + table.column("hubUserID", .text).notNull().references("hubAccountInfo", onDelete: .cascade) + }) + } + public static func openSharedDatabase(at databaseURL: URL) throws -> DatabasePool { let coordinator = NSFileCoordinator(filePresenter: nil) var coordinatorError: NSError? diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift index 5691119a4..114e0c17e 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift @@ -28,6 +28,7 @@ class CryptomatorKeychain: CryptomatorKeychainType { static let localFileSystem = CryptomatorKeychain(service: "localFileSystem.auth") static let upgrade = CryptomatorKeychain(service: "upgrade") static let keepUnlocked = CryptomatorKeychain(service: "keepUnlocked") + static let hub = CryptomatorKeychain(service: "hub") init(service: String) { self.service = service diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift index 783ebb863..143b54ba1 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/VaultUnlocking.swift @@ -8,11 +8,41 @@ import FileProvider import Foundation +import Promises + @objc public protocol VaultUnlocking: NSFileProviderServiceSource { // "Because communication over XPC is asynchronous, all methods in the protocol must have a return type of void. If you need to return data, you can define a reply block [...]" see: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html func unlockVault(kek: [UInt8], reply: @escaping (NSError?) -> Void) func startBiometricalUnlock() func endBiometricalUnlock() + + func unlockVault(rawKey: [UInt8], reply: @escaping (NSError?) -> Void) +} + +public extension VaultUnlocking { + func unlockVault(kek: [UInt8]) -> Promise { + return Promise { fulfill, reject in + self.unlockVault(kek: kek) { error in + if let error = error { + reject(error) + } else { + fulfill(()) + } + } + } + } + + func unlockVault(rawKey: [UInt8]) -> Promise { + return Promise { fulfill, reject in + self.unlockVault(rawKey: rawKey) { error in + if let error = error { + reject(error) + } else { + fulfill(()) + } + } + } + } } public extension NSFileProviderServiceName { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/AddHubVaultView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/AddHubVaultView.swift new file mode 100644 index 000000000..da95acba6 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/AddHubVaultView.swift @@ -0,0 +1,53 @@ +// +// AddHubVaultView.swift +// Cryptomator +// +// Created by Philipp Schmid on 21.07.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import SwiftUI + +public struct AddHubVaultView: View { + @ObservedObject var viewModel: HubVaultViewModel + public var body: some View { + VStack { + switch viewModel.state { + case .detectedVault: + Text("Detected Hub vault") + Button("Login") { + if let loginViewModel = viewModel as? HubVaultAdding { + loginViewModel.login() + } + } + case .needsDeviceRegistration: + Text("This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device.") + TextField("Device name", text: $viewModel.deviceName) + Button("Register") { + viewModel.register() + } + case .deviceRegisteredSuccessfully: + Text("To access the vault, your device needs to be authorized by the vault owner.") + Button("Continue") { + viewModel.continueToAccessCheck() + } + case .accessNotGranted: + Text("Your device has not vet been authorized to access this vault. Ask the vault owner to authorize it.") + Button("Refresh") { + viewModel.refresh() + } + case .receivedExistingKey: + Text("Received existing key") + case let .loading(text): + if #available(iOS 14.0, *) { + ProgressView() + } + Text(text) + } + } + } +} + +public protocol HubVaultAdding { + func login() +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift new file mode 100644 index 000000000..5c0508c3f --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -0,0 +1,164 @@ +// +// File 2.swift +// +// +// Created by Philipp Schmid on 22.07.22. +// + +import AppAuthCore +import CryptoKit +import CryptomatorCloudAccessCore +import Foundation + +public enum HubAuthenticationFlow { + case receivedExistingKey(Data) + case accessNotGranted + case needsDeviceRegistration +} + +public protocol HubDeviceRegistering { + func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws +} + +public protocol HubKeyReceiving { + func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow +} + +public enum CryptomatorHubAuthenticatorError: Error { + case unexpectedError + case unexpectedResponse + case deviceNameAlreadyExists +} + +public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving { + private static let scheme = "hub+" + public static let shared = CryptomatorHubAuthenticator() + + public func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { + guard let baseURL = createBaseURL(vaultConfig: vaultConfig) else { + fatalError("TODO throw error") + } + let deviceID = try getDeviceID() + let url = baseURL.appendingPathComponent("/keys").appendingPathComponent("/\(deviceID)") + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + fatalError("TODO throw error") + } + var urlRequest = URLRequest(url: url) + urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (data, response) = try await URLSession.shared.data(with: urlRequest) + switch (response as? HTTPURLResponse)?.statusCode { + case 200: + return .receivedExistingKey(data) + case 403: + return .accessNotGranted + case 404: + return .needsDeviceRegistration + default: + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + } + + public func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws { + let deviceID = try getDeviceID() + let publicKey = try CryptomatorHubKeyProvider.shared.getPublicKey() + let derPubKey: Data + if #available(iOS 14.0, *) { + derPubKey = publicKey.derRepresentation + } else { + fatalError("TODO: Increase the minimum deployment target or change representation") + } + let dto = CreateDeviceDto(id: deviceID, name: name, publicKey: derPubKey.base64URLEncodedString()) + guard let devicesResourceURL = URL(string: hubConfig.devicesResourceUrl) else { + fatalError("TODO: throw error") + } + let keyURL = devicesResourceURL.appendingPathComponent("\(deviceID)") + var request = URLRequest(url: keyURL) + request.httpMethod = "PUT" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(dto) + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + fatalError("TODO throw error") + } + request.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (_, response) = try await URLSession.shared.data(with: request) + switch (response as? HTTPURLResponse)?.statusCode { + case 201: + break + case 409: + throw CryptomatorHubAuthenticatorError.deviceNameAlreadyExists + default: + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + } + + func createBaseURL(vaultConfig: UnverifiedVaultConfig) -> URL? { + guard let keyId = vaultConfig.keyId, keyId.hasPrefix(CryptomatorHubAuthenticator.scheme) else { + return nil + } + let baseURLPath = keyId.deletingPrefix(CryptomatorHubAuthenticator.scheme) + return URL(string: baseURLPath) + } + + func getDeviceID() throws -> String { + let publicKey = try CryptomatorHubKeyProvider.shared.getPublicKey() + let digest: SHA256.Digest + if #available(iOS 14.0, *) { + digest = SHA256.hash(data: publicKey.derRepresentation) + } else { + fatalError("TODO: Increase the minimum deployment target or change representation") + } + return digest.data.base16EncodedString + } + + struct CreateDeviceDto: Codable { + let id: String + let name: String + let publicKey: String + } +} + +extension URLSession { + @available(iOS, deprecated: 15.0, message: "This extension is no longer necessary. Use API built into SDK") + func data(with request: URLRequest) async throws -> (Data, URLResponse) { + try await withCheckedThrowingContinuation { continuation in + let task = self.dataTask(with: request) { data, response, error in + guard let data = data, let response = response else { + let error = error ?? URLError(.badServerResponse) + return continuation.resume(throwing: error) + } + + continuation.resume(returning: (data, response)) + } + + task.resume() + } + } +} + +extension Digest { + var bytes: [UInt8] { Array(makeIterator()) } + var data: Data { Data(bytes) } +} + +extension OIDAuthState { + func performAction() async throws -> (String?, String?) { + try await withCheckedThrowingContinuation({ continuation in + performAction { accessToken, idToken, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: (accessToken, idToken)) + } + } + }) + } +} + +extension String { + func deletingPrefix(_ prefix: String) -> String { + guard hasPrefix(prefix) else { return self } + return String(dropFirst(prefix.count)) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubKeyProvider.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubKeyProvider.swift new file mode 100644 index 000000000..eb87de68f --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubKeyProvider.swift @@ -0,0 +1,40 @@ +// +// CryptomatorHubKeyProvider.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 20.07.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import CryptoKit +import Foundation + +public struct CryptomatorHubKeyProvider { + public static let shared: CryptomatorHubKeyProvider = .init(keychain: CryptomatorKeychain.hub) + let keychain: CryptomatorKeychainType + private let keychainKey = "privateKey" + + public func getPublicKey() throws -> P384.KeyAgreement.PublicKey { + let privateKey = try getPrivateKey() + return privateKey.publicKey + } + + public func getPrivateKey() throws -> P384.KeyAgreement.PrivateKey { + let privateKey: P384.KeyAgreement.PrivateKey + if let existingKeyData = keychain.getAsData(keychainKey) { + privateKey = try P384.KeyAgreement.PrivateKey(rawRepresentation: existingKeyData) + } else { + privateKey = P384.KeyAgreement.PrivateKey(compactRepresentable: false) + try saveKey(privateKey) + } + return privateKey + } + + private func saveKey(_ privateKey: P384.KeyAgreement.PrivateKey) throws { + try keychain.set(keychainKey, value: privateKey.rawRepresentation) + } + + public func delete() { + try? keychain.delete(keychainKey) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultCoordinator.swift new file mode 100644 index 000000000..ccaa37da8 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultCoordinator.swift @@ -0,0 +1,64 @@ +// +// File.swift +// +// +// Created by Philipp Schmid on 22.07.22. +// + +import AppAuthCore +import CryptomatorCloudAccessCore +import Foundation +import UIKit + +public protocol HubVaultCoordinator: AnyObject { + var parentCoordinator: Coordinator? { get set } + func handleError(_ error: Error) +} + +public extension HubVaultCoordinator where Self: Coordinator { + func handleError(_ error: Error) { + handleError(error, for: navigationController) { + self.navigationController.popViewController(animated: true) + self.parentCoordinator?.childDidFinish(self) + } + } +} + +public protocol HubVaultUnlockDelegate: AnyObject { + func unlockedVault() +} + +public class CryptomatorHubVaultUnlockCoordinator: Coordinator, HubVaultCoordinator { + public lazy var childCoordinators = [Coordinator]() + public var navigationController: UINavigationController + public weak var parentCoordinator: Coordinator? + public weak var delegate: HubVaultUnlockDelegate? + let domain: NSFileProviderDomain + let hubAccount: HubAccount + let vaultConfig: UnverifiedVaultConfig + + public init(navigationController: UINavigationController, domain: NSFileProviderDomain, hubAccount: HubAccount, vaultConfig: UnverifiedVaultConfig, parentCoordinator: Coordinator? = nil) { + self.navigationController = navigationController + self.domain = domain + self.hubAccount = hubAccount + self.vaultConfig = vaultConfig + self.parentCoordinator = parentCoordinator + } + + public func start() { + let viewModel = HubVaultUnlockViewModel(hubAccount: hubAccount, + domain: domain, + fileProviderConnector: FileProviderXPCConnector.shared, + vaultConfig: vaultConfig, + coordinator: self) + let addHubVaultVC = HubVaultViewController(viewModel: viewModel) + navigationController.pushViewController(addHubVaultVC, animated: true) + } +} + +extension CryptomatorHubVaultUnlockCoordinator: HubVaultUnlockDelegate { + public func unlockedVault() { + delegate?.unlockedVault() + parentCoordinator?.childDidFinish(self) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift new file mode 100644 index 000000000..026e921df --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift @@ -0,0 +1,49 @@ +// +// HubVaultUnlockViewModel.swift +// +// +// Created by Philipp Schmid on 22.07.22. +// + +import CryptoKit +import CryptomatorCloudAccessCore +import CryptomatorCryptoLib +import FileProvider +import Foundation +import JOSESwift +import Promises + +class HubVaultUnlockViewModel: HubVaultViewModel { + let fileProviderConnector: FileProviderConnector + let domain: NSFileProviderDomain + private weak var unlockDelegate: HubVaultUnlockDelegate? + + init(hubAccount: HubAccount, domain: NSFileProviderDomain, fileProviderConnector: FileProviderConnector, vaultConfig: UnverifiedVaultConfig, coordinator: (HubVaultCoordinator & HubVaultUnlockDelegate)? = nil) { + self.fileProviderConnector = fileProviderConnector + self.domain = domain + self.unlockDelegate = coordinator + super.init(initialState: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait."), vaultConfig: vaultConfig, coordinator: coordinator) + self.authState = hubAccount.authState + continueToAccessCheck() + } + + override func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) { + let masterkey: Masterkey + do { + masterkey = try JWEHelper.decrypt(jwe: jwe, with: privateKey) + } catch { + setError(to: error) + return + } + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) + getXPCPromise.then { xpc in + xpc.proxy.unlockVault(rawKey: masterkey.rawKey) + }.then { + self.unlockDelegate?.unlockedVault() + }.catch { + self.setError(to: $0) + }.always { + self.fileProviderConnector.invalidateXPC(getXPCPromise) + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewController.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewController.swift new file mode 100644 index 000000000..9510d4b69 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewController.swift @@ -0,0 +1,37 @@ +// +// HubVaultViewController.swift +// Cryptomator +// +// Created by Philipp Schmid on 21.07.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. +// + +import Combine +import SwiftUI +import UIKit + +public class HubVaultViewController: UIHostingController { + let viewModel: HubVaultViewModel + private var subscriber: AnyCancellable? + private weak var coordinator: Coordinator? + + public init(viewModel: HubVaultViewModel) { + self.viewModel = viewModel + self.coordinator = viewModel.coordinator as? Coordinator + super.init(rootView: .init(viewModel: viewModel)) + } + + @available(*, unavailable) + @MainActor dynamic required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + subscriber = viewModel.$error.receive(on: DispatchQueue.main).compactMap { $0 }.sink { [weak self] error in + if let self = self { + self.coordinator?.handleError(error, for: self) + } + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift new file mode 100644 index 000000000..1efe23007 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift @@ -0,0 +1,129 @@ +// +// File 2.swift +// +// +// Created by Philipp Schmid on 22.07.22. +// + +import AppAuthCore +import CryptoKit +import CryptomatorCloudAccessCore +import Foundation +import JOSESwift + +public enum AddHubVaultViewModelError: Error { + case missingHubConfig + case missingAuthState +} + +public enum AddHubVaultViewModelState { + case detectedVault + case receivedExistingKey + case accessNotGranted + case deviceRegisteredSuccessfully + case needsDeviceRegistration + case loading(text: String) +} + +open class HubVaultViewModel: ObservableObject { + public var authState: OIDAuthState? + @Published public var state: AddHubVaultViewModelState + @Published public var deviceName: String = "" + @Published public var error: Error? + public weak var coordinator: HubVaultCoordinator? + let vaultConfig: UnverifiedVaultConfig + let deviceRegisteringService: HubDeviceRegistering + let hubKeyService: HubKeyReceiving + + public init(initialState: AddHubVaultViewModelState, vaultConfig: UnverifiedVaultConfig, deviceRegisteringService: HubDeviceRegistering = CryptomatorHubAuthenticator.shared, hubKeyService: HubKeyReceiving = CryptomatorHubAuthenticator.shared, coordinator: HubVaultCoordinator? = nil) { + self.state = initialState + self.vaultConfig = vaultConfig + self.deviceRegisteringService = deviceRegisteringService + self.hubKeyService = hubKeyService + self.coordinator = coordinator + } + + public func register() { + error = nil + guard let hubConfig = vaultConfig.hub else { + error = AddHubVaultViewModelError.missingHubConfig + return + } + guard let authState = authState else { + error = AddHubVaultViewModelError.missingAuthState + return + } + + Task { + do { + try await deviceRegisteringService.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState) + } catch { + setError(to: error) + return + } + setState(to: .deviceRegisteredSuccessfully) + } + } + + public func continueToAccessCheck() { + setError(to: nil) + guard let authState = authState else { + setError(to: AddHubVaultViewModelError.missingAuthState) + return + } + setState(to: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait.")) + Task { + let authFlow: HubAuthenticationFlow + do { + authFlow = try await hubKeyService.receiveKey(authState: authState, vaultConfig: vaultConfig) + } catch { + setError(to: error) + return + } + switch authFlow { + case let .receivedExistingKey(data): + receivedExistingKey(data: data) + case .accessNotGranted: + setState(to: .accessNotGranted) + case .needsDeviceRegistration: + setState(to: .needsDeviceRegistration) + } + } + } + + public func refresh() { + continueToAccessCheck() + } + + public func receivedExistingKey(data: Data) { + let privateKey: P384.KeyAgreement.PrivateKey + let jwe: JWE + let hubAccount: HubAccount + do { + privateKey = try CryptomatorHubKeyProvider.shared.getPrivateKey() + jwe = try JWE(compactSerialization: data) + hubAccount = try HubAccount(authState: authState!) + try HubAccountManager.shared.saveHubAccount(hubAccount) + } catch { + setError(to: error) + return + } + receivedExistingKey(jwe: jwe, privateKey: privateKey, hubAccount: hubAccount) + } + + open func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) { + fatalError("Abstract method receivedExistingKey(jwe:privateKey:hubAccount:) not implemented") + } + + public func setState(to newState: AddHubVaultViewModelState) { + DispatchQueue.main.async { + self.state = newState + } + } + + public func setError(to newError: Error?) { + DispatchQueue.main.async { + self.error = newError + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift new file mode 100644 index 000000000..2d88abc2b --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift @@ -0,0 +1,32 @@ +// +// JWEHelper.swift +// +// +// Created by Philipp Schmid on 22.07.22. +// + +import CryptoKit +import CryptomatorCryptoLib +import Foundation +import JOSESwift + +public enum JWEHelper { + public static func decrypt(jwe: JWE, with privateKey: P384.KeyAgreement.PrivateKey) throws -> Masterkey { + // see https://developer.apple.com/forums/thread/680554 + let x = privateKey.x963Representation[1 ..< 49] + let y = privateKey.x963Representation[49 ..< 97] + let k = privateKey.x963Representation[97 ..< 145] + let decryptionKey = try ECPrivateKey(crv: "P-384", x: x.base64UrlEncodedString(), y: y.base64UrlEncodedString(), privateKey: k.base64UrlEncodedString()) + + guard let decrypter = Decrypter(keyManagementAlgorithm: .ECDH_ES, contentEncryptionAlgorithm: .A256GCM, decryptionKey: decryptionKey) else { + throw VaultManagerError.invalidDecrypter + } + let payload = try jwe.decrypt(using: decrypter) + let payloadMasterkey = try JSONDecoder().decode(PayloadMasterkey.self, from: payload.data()) + + guard let masterkeyData = Data(base64Encoded: payloadMasterkey.key) else { + throw VaultManagerError.invalidPayloadMasterkey + } + return Masterkey.createFromRaw(rawKey: [UInt8](masterkeyData)) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/HubAccountManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/HubAccountManager.swift new file mode 100644 index 000000000..c05c84c6b --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/HubAccountManager.swift @@ -0,0 +1,155 @@ +// +// HubAccountManager.swift +// +// +// Created by Philipp Schmid on 22.07.22. +// + +import AppAuthCore +import Foundation +import GRDB + +struct HubAccountInfo: Codable { + let userID: String +} + +extension HubAccountInfo: FetchableRecord, MutablePersistableRecord { + enum Columns: String, ColumnExpression { + case userID + } +} + +public struct HubAccount { + public let userID: String + public let authState: OIDAuthState +} + +extension HubAccount { + init(info: HubAccountInfo, authState: OIDAuthState) { + self.userID = info.userID + self.authState = authState + } +} + +extension HubAccount { + private static let keycloakUserIDKey = "sub" + + public init(authState: OIDAuthState) throws { +// guard let idToken = authState.lastTokenResponse?.idToken ?? authState.lastAuthorizationResponse.idToken else { +// throw HubAccountError.missingIDToken +// } +// guard let claims = OIDIDToken(idTokenString: idToken)?.claims else { +// throw HubAccountError.missingClaims +// } +// guard let userID = claims[HubAccount.keycloakUserIDKey] as? String else { +// throw HubAccountError.missingUserID +// } + let userID = "DemoUser-ID" + self.init(userID: userID, authState: authState) + } +} + +enum HubAccountError: Error { + case missingIDToken + case missingClaims + case missingUserID +} + +struct HubVaultAccount: Codable { + var id: Int64? + let vaultUID: String + let hubUserID: String +} + +extension HubVaultAccount: FetchableRecord, MutablePersistableRecord { + enum Columns: String, ColumnExpression { + case id, vaultUID, hubUserID + } +} + +public struct HubAccountManager { + let dbWriter: DatabaseWriter + let keychain: CryptomatorKeychainType + public static let shared = HubAccountManager(dbWriter: CryptomatorDatabase.shared.dbPool, keychain: CryptomatorKeychain.hub) + + public func getHubAccount(withUserID userID: String) throws -> HubAccount? { + guard let accountInfo = try getHubAccountInfo(withUserID: userID) else { + return nil + } + return getHubAccount(accountInfo: accountInfo) + } + + public func getHubAccount(forVaultUID vaultUID: String) throws -> HubAccount? { + try dbWriter.read { db in + guard let hubVaultAccount = try HubVaultAccount.fetchOne(db, key: [HubVaultAccount.Columns.vaultUID.name: vaultUID]) else { + return nil + } + guard let accountInfo = try HubAccountInfo.fetchOne(db, key: hubVaultAccount.hubUserID) else { + return nil + } + return getHubAccount(accountInfo: accountInfo) + } + } + + public func saveHubAccount(_ hubAccount: HubAccount) throws { + var accountInfo = HubAccountInfo(userID: hubAccount.userID) + try dbWriter.write { db in + try accountInfo.save(db) + try keychain.saveAuthState(hubAccount.authState, for: accountInfo.userID) + } + } + + public func removeHubAccount(withUserID userID: String) throws { + try dbWriter.write { db in + try HubAccountInfo.deleteOne(db, key: [HubAccountInfo.Columns.userID.name: userID]) + try keychain.delete(userID) + } + } + + public func linkVaultToHubAccount(vaultUID: String, hubUserID: String) throws { + let request = HubAccountInfo.filter(HubAccountInfo.Columns.userID == hubUserID) + try dbWriter.write { db in + guard let accountInfo = try HubAccountInfo.fetchOne(db, request) else { + throw HubAccountManagerError.unknownHubUserID + } + guard let vaultAccount = try VaultAccount.fetchOne(db, key: [VaultAccount.vaultUIDKey: vaultUID]) else { + throw HubAccountManagerError.unknownVaultUID + } + var hubVaultAccount = HubVaultAccount(vaultUID: vaultAccount.vaultUID, hubUserID: accountInfo.userID) + try hubVaultAccount.save(db) + } + } + + private func getHubAccountInfo(withUserID userID: String) throws -> HubAccountInfo? { + let request = HubAccountInfo.filter(HubAccountInfo.Columns.userID == userID) + return try dbWriter.read { db in + try HubAccountInfo.fetchOne(db, request) + } + } + + private func getHubAccount(accountInfo: HubAccountInfo) -> HubAccount? { + guard let authState = keychain.getAuthState(accountInfo.userID) else { + return nil + } + return HubAccount(info: accountInfo, authState: authState) + } +} + +enum HubAccountManagerError: Error { + case unknownHubUserID + case unknownVaultUID +} + +private extension CryptomatorKeychainType { + func getAuthState(_ identifier: String) -> OIDAuthState? { + guard let data = getAsData(identifier) else { + return nil + } + return try? NSKeyedUnarchiver.unarchivedObject(ofClass: OIDAuthState.self, from: data) + } + + func saveAuthState(_ authState: OIDAuthState, for identifier: String) throws { + let archivedAuthState = try NSKeyedArchiver.archivedData(withRootObject: authState, requiringSecureCoding: true) + try set(identifier, value: archivedAuthState) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift index 52db65392..6c31623a3 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift @@ -22,7 +22,7 @@ public protocol VaultCache { public struct CachedVault: Codable, Equatable { let vaultUID: String public let masterkeyFileData: Data - let vaultConfigToken: Data? + public let vaultConfigToken: Data? let lastUpToDateCheck: Date var masterkeyFileLastModifiedDate: Date? var vaultConfigLastModifiedDate: Date? diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift index 659fecccc..a71bad4f4 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift @@ -7,10 +7,12 @@ // import CocoaLumberjackSwift +import CryptoKit import CryptomatorCloudAccessCore import CryptomatorCryptoLib import FileProvider import Foundation +import JOSESwift import os.log import Promises @@ -19,6 +21,8 @@ public enum VaultManagerError: Error { case vaultVersionNotSupported case fileProviderDomainNotFound case moveVaultInsideItself + case invalidDecrypter + case invalidPayloadMasterkey } public protocol VaultManager { @@ -31,6 +35,10 @@ public protocol VaultManager { func removeAllUnusedFileProviderDomains() -> Promise func moveVault(account: VaultAccount, to targetVaultPath: CloudPath) -> Promise func changePassphrase(oldPassphrase: String, newPassphrase: String, forVaultUID vaultUID: String) -> Promise + + // swiftlint:disable:next function_parameter_count + func addExistingHubVault(vaultUID: String, delegateAccountUID: String, hubUserID: String, jweData: Data, privateKey: P384.KeyAgreement.PrivateKey, vaultItem: VaultItem, downloadedVaultConfig: DownloadedVaultConfig) -> Promise + func manualUnlockVault(withUID vaultUID: String, rawKey: [UInt8]) throws -> CloudProvider } public class VaultDBManager: VaultManager { @@ -39,7 +47,8 @@ public class VaultDBManager: VaultManager { vaultCache: VaultDBCache(dbWriter: CryptomatorDatabase.shared.dbPool), passwordManager: VaultPasswordKeychainManager(), masterkeyCacheManager: MasterkeyCacheKeychainManager.shared, - masterkeyCacheHelper: VaultKeepUnlockedManager.shared) + masterkeyCacheHelper: VaultKeepUnlockedManager.shared, + hubAccountManager: HubAccountManager.shared) let providerManager: CloudProviderDBManager let vaultAccountManager: VaultAccountManager private static let fakeVaultVersion = 999 @@ -47,19 +56,22 @@ public class VaultDBManager: VaultManager { private let passwordManager: VaultPasswordManager private let masterkeyCacheManager: MasterkeyCacheManager private let masterkeyCacheHelper: MasterkeyCacheHelper + private let hubAccountManager: HubAccountManager init(providerManager: CloudProviderDBManager, vaultAccountManager: VaultAccountManager, vaultCache: VaultCache, passwordManager: VaultPasswordManager, masterkeyCacheManager: MasterkeyCacheManager, - masterkeyCacheHelper: MasterkeyCacheHelper) { + masterkeyCacheHelper: MasterkeyCacheHelper, + hubAccountManager: HubAccountManager) { self.providerManager = providerManager self.vaultAccountManager = vaultAccountManager self.vaultCache = vaultCache self.passwordManager = passwordManager self.masterkeyCacheManager = masterkeyCacheManager self.masterkeyCacheHelper = masterkeyCacheHelper + self.hubAccountManager = hubAccountManager } // MARK: - Create New Vault @@ -195,6 +207,118 @@ public class VaultDBManager: VaultManager { } } + // swiftlint:disable:next function_parameter_count + public func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, downloadedVaultConfig: DownloadedVaultConfig, downloadedMasterkey: DownloadedMasterkeyFile, vaultItem: VaultItem, password: String) -> Promise { + let provider: LocalizedCloudProviderDecorator + do { + provider = LocalizedCloudProviderDecorator(delegate: try providerManager.getProvider(with: delegateAccountUID)) + } catch { + return Promise(error) + } + let vaultPath = vaultItem.vaultPath + let vaultConfigMetadata = downloadedVaultConfig.metadata + let vaultConfigToken = downloadedVaultConfig.token + let masterkeyFile = downloadedMasterkey.masterkeyFile + let masterkeyFileData = downloadedMasterkey.masterkeyFileData + let masterkeyFileMetadata = downloadedMasterkey.metadata + do { + let masterkey = try masterkeyFile.unlock(passphrase: password) + _ = try VaultProviderFactory.createVaultProvider(from: downloadedVaultConfig.vaultConfig, masterkey: masterkey, vaultPath: vaultPath, with: provider.delegate) + } catch { + return Promise(error) + } + let vaultConfigLastModifiedDate = vaultConfigMetadata.lastModifiedDate + let masterkeyFileLastModifiedDate = masterkeyFileMetadata.lastModifiedDate + let lastUpToDateCheck: Date = (vaultConfigLastModifiedDate ?? .distantPast) < (masterkeyFileLastModifiedDate ?? .distantPast) ? masterkeyFileLastModifiedDate! : vaultConfigLastModifiedDate ?? Date() + let cachedVault = CachedVault(vaultUID: vaultUID, masterkeyFileData: masterkeyFileData, vaultConfigToken: vaultConfigToken, lastUpToDateCheck: lastUpToDateCheck, masterkeyFileLastModifiedDate: masterkeyFileMetadata.lastModifiedDate, vaultConfigLastModifiedDate: vaultConfigMetadata.lastModifiedDate) + return addFileProviderDomain(forVaultUID: vaultUID, displayName: vaultItem.name).then { + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: vaultItem.name) + try self.vaultAccountManager.saveNewAccount(vaultAccount) + try self.postProcessVaultCreation(cachedVault: cachedVault, password: password, storePasswordInKeychain: false) + DDLogInfo("Opened existing vault \"\(vaultItem.name)\" (\(vaultUID))") + } + } + + public func getUnverifiedVaultConfig(delegateAccountUID: String, vaultItem: VaultItem) -> Promise { + let provider: LocalizedCloudProviderDecorator + do { + provider = LocalizedCloudProviderDecorator(delegate: try providerManager.getProvider(with: delegateAccountUID)) + } catch { + return Promise(error) + } + let tmpDirURL = FileManager.default.temporaryDirectory + let localVaultConfigURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) + let vaultPath = vaultItem.vaultPath + let vaultConfigPath = vaultPath.appendingPathComponent("vault.cryptomator") + return provider.downloadFileWithMetadata(from: vaultConfigPath, to: localVaultConfigURL).then { vaultConfigMetadata -> DownloadedVaultConfig in + let vaultConfigToken = try Data(contentsOf: localVaultConfigURL) + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: vaultConfigToken) + return DownloadedVaultConfig(vaultConfig: unverifiedVaultConfig, token: vaultConfigToken, metadata: vaultConfigMetadata) + } + } + + public func downloadMasterkeyFile(delegateAccountUID: String, vaultItem: VaultItem) -> Promise { + let provider: LocalizedCloudProviderDecorator + do { + provider = LocalizedCloudProviderDecorator(delegate: try providerManager.getProvider(with: delegateAccountUID)) + } catch { + return Promise(error) + } + let tmpDirURL = FileManager.default.temporaryDirectory + let localMasterkeyURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) + let vaultPath = vaultItem.vaultPath + let masterkeyPath = vaultPath.appendingPathComponent("masterkey.cryptomator") + return provider.downloadFileWithMetadata(from: masterkeyPath, to: localMasterkeyURL).then { masterkeyFileMetadata -> DownloadedMasterkeyFile in + let masterkeyFileData = try Data(contentsOf: localMasterkeyURL) + let masterkeyFile = try MasterkeyFile.withContentFromData(data: masterkeyFileData) + return DownloadedMasterkeyFile(masterkeyFile: masterkeyFile, metadata: masterkeyFileMetadata, masterkeyFileData: masterkeyFileData) + } + } + + // swiftlint:disable:next function_parameter_count + public func addExistingHubVault(vaultUID: String, delegateAccountUID: String, hubUserID: String, jweData: Data, privateKey: P384.KeyAgreement.PrivateKey, vaultItem: VaultItem, downloadedVaultConfig: DownloadedVaultConfig) -> Promise { + let provider: LocalizedCloudProviderDecorator + do { + provider = LocalizedCloudProviderDecorator(delegate: try providerManager.getProvider(with: delegateAccountUID)) + } catch { + return Promise(error) + } + let vaultPath = vaultItem.vaultPath + let vaultConfigMetadata = downloadedVaultConfig.metadata + let vaultConfigToken = downloadedVaultConfig.token + let masterkey: Masterkey + do { + let jwe = try JWE(compactSerialization: jweData) + masterkey = try JWEHelper.decrypt(jwe: jwe, with: privateKey) + } catch { + return Promise(error) + } + do { + _ = try VaultProviderFactory.createVaultProvider(from: downloadedVaultConfig.vaultConfig, masterkey: masterkey, vaultPath: vaultPath, with: provider.delegate) + } catch { + return Promise(error) + } + let cachedVault = CachedVault(vaultUID: vaultUID, + masterkeyFileData: jweData, + vaultConfigToken: vaultConfigToken, + lastUpToDateCheck: Date(), + masterkeyFileLastModifiedDate: nil, + vaultConfigLastModifiedDate: vaultConfigMetadata.lastModifiedDate) + return addFileProviderDomain(forVaultUID: vaultUID, displayName: vaultItem.name).then { + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: vaultItem.name) + try self.vaultAccountManager.saveNewAccount(vaultAccount) + do { + try self.hubAccountManager.linkVaultToHubAccount(vaultUID: vaultUID, hubUserID: hubUserID) + try self.postProcessVaultCreation(cachedVault: cachedVault, password: nil) + } catch { + try self.vaultAccountManager.removeAccount(with: vaultUID) + _ = self.removeFileProviderDomain(withVaultUID: vaultUID) + throw error + } + DDLogInfo("Opened existing vault \"\(vaultItem.name)\" (\(vaultUID))") + } + } + /** Imports an existing legacy Vault. @@ -309,6 +433,26 @@ public class VaultDBManager: VaultManager { return try createVaultProvider(cachedVault: cachedVault, masterkey: masterkey, masterkeyFile: masterkeyFile) } + public func manualUnlockVault(withUID vaultUID: String, rawKey: [UInt8]) throws -> CloudProvider { + let cachedVault = try vaultCache.getCachedVault(withVaultUID: vaultUID) + + guard let vaultConfigToken = cachedVault.vaultConfigToken else { + fatalError("TODO: throw error") + } + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: vaultConfigToken) + let vaultAccount = try vaultAccountManager.getAccount(with: vaultUID) + let provider = try providerManager.getProvider(with: vaultAccount.delegateAccountUID) + let masterkey = Masterkey.createFromRaw(rawKey: rawKey) + let decorator = try VaultProviderFactory.createVaultProvider(from: unverifiedVaultConfig, + masterkey: masterkey, + vaultPath: vaultAccount.vaultPath, + with: provider) + if masterkeyCacheHelper.shouldCacheMasterkey(forVaultUID: vaultUID) { + try masterkeyCacheManager.cacheMasterkey(masterkey, forVaultUID: vaultUID) + } + return decorator + } + public func createVaultProvider(withUID vaultUID: String, masterkey: Masterkey) throws -> CloudProvider { let cachedVault = try vaultCache.getCachedVault(withVaultUID: vaultUID) let masterkeyFile = try MasterkeyFile.withContentFromData(data: cachedVault.masterkeyFileData) @@ -400,6 +544,16 @@ public class VaultDBManager: VaultManager { } } + /** + Post-processing the vault creation by caching the vault and storing the corresponding master password (if set) in the keychain. + */ + func postProcessVaultCreation(cachedVault: CachedVault, password: String?) throws { + try vaultCache.cache(cachedVault) + if let password = password { + try passwordManager.setPassword(password, forVaultUID: cachedVault.vaultUID) + } + } + func postProcessChangePassphrase(masterkeyFileData: Data, masterkeyFileDataLastModifiedDate: Date?, forVaultUID vaultUID: String, newPassphrase: String) throws { try vaultCache.setMasterkeyFileData(masterkeyFileData, forVaultUID: vaultUID, lastModifiedDate: masterkeyFileDataLastModifiedDate) if try passwordManager.hasPassword(forVaultUID: vaultUID) { @@ -533,3 +687,19 @@ public extension NSFileProviderDomain { self.init(identifier: identifier, displayName: "") } } + +public struct DownloadedVaultConfig { + public let vaultConfig: UnverifiedVaultConfig + let token: Data + let metadata: CloudItemMetadata +} + +public struct DownloadedMasterkeyFile { + let masterkeyFile: MasterkeyFile + let metadata: CloudItemMetadata + let masterkeyFileData: Data +} + +struct PayloadMasterkey: Codable { + let key: String +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift index b3ffaaa36..95530914e 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift @@ -5,222 +5,221 @@ // Created by Philipp Schmid on 27.10.21. // Copyright © 2021 Skymatic GmbH. All rights reserved. // - -#if DEBUG -import CryptomatorCloudAccessCore -import CryptomatorCryptoLib -import Foundation -import Promises - -// swiftlint:disable all - -final class VaultManagerMock: VaultManager { - // MARK: - createNewVault - - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainThrowableError: Error? - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount = 0 - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCalled: Bool { - createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount > 0 - } - - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool)? - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool)] = [] - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReturnValue: Promise! - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainClosure: ((String, String, CloudPath, String, Bool) -> Promise)? - - func createNewVault(withVaultUID vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { - if let error = createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainThrowableError { - return Promise(error) - } - createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount += 1 - createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, password: password, storePasswordInKeychain: storePasswordInKeychain) - createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, password: password, storePasswordInKeychain: storePasswordInKeychain)) - return createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultPath, password, storePasswordInKeychain) }) ?? createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReturnValue - } - - // MARK: - createFromExisting - - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError: Error? - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount = 0 - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCalled: Bool { - createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount > 0 - } - - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)? - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)] = [] - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue: Promise! - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure: ((String, String, VaultItem, String, Bool) -> Promise)? - - func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { - if let error = createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError { - return Promise(error) - } - createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount += 1 - createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain) - createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain)) - return createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultItem, password, storePasswordInKeychain) }) ?? createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue - } - - // MARK: - createLegacyFromExisting - - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError: Error? - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount = 0 - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCalled: Bool { - createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount > 0 - } - - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)? - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)] = [] - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue: Promise! - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure: ((String, String, VaultItem, String, Bool) -> Promise)? - - func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { - if let error = createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError { - return Promise(error) - } - createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount += 1 - createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain) - createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain)) - return createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultItem, password, storePasswordInKeychain) }) ?? createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue - } - - // MARK: - manualUnlockVault - - var manualUnlockVaultWithUIDKekThrowableError: Error? - var manualUnlockVaultWithUIDKekCallsCount = 0 - var manualUnlockVaultWithUIDKekCalled: Bool { - manualUnlockVaultWithUIDKekCallsCount > 0 - } - - var manualUnlockVaultWithUIDKekReceivedArguments: (vaultUID: String, kek: [UInt8])? - var manualUnlockVaultWithUIDKekReceivedInvocations: [(vaultUID: String, kek: [UInt8])] = [] - var manualUnlockVaultWithUIDKekReturnValue: CloudProvider! - var manualUnlockVaultWithUIDKekClosure: ((String, [UInt8]) throws -> CloudProvider)? - - func manualUnlockVault(withUID vaultUID: String, kek: [UInt8]) throws -> CloudProvider { - if let error = manualUnlockVaultWithUIDKekThrowableError { - throw error - } - manualUnlockVaultWithUIDKekCallsCount += 1 - manualUnlockVaultWithUIDKekReceivedArguments = (vaultUID: vaultUID, kek: kek) - manualUnlockVaultWithUIDKekReceivedInvocations.append((vaultUID: vaultUID, kek: kek)) - return try manualUnlockVaultWithUIDKekClosure.map({ try $0(vaultUID, kek) }) ?? manualUnlockVaultWithUIDKekReturnValue - } - - // MARK: - createVaultProvider - - var createVaultProviderWithUIDMasterkeyThrowableError: Error? - var createVaultProviderWithUIDMasterkeyCallsCount = 0 - var createVaultProviderWithUIDMasterkeyCalled: Bool { - createVaultProviderWithUIDMasterkeyCallsCount > 0 - } - - var createVaultProviderWithUIDMasterkeyReceivedArguments: (vaultUID: String, masterkey: Masterkey)? - var createVaultProviderWithUIDMasterkeyReceivedInvocations: [(vaultUID: String, masterkey: Masterkey)] = [] - var createVaultProviderWithUIDMasterkeyReturnValue: CloudProvider! - var createVaultProviderWithUIDMasterkeyClosure: ((String, Masterkey) throws -> CloudProvider)? - - func createVaultProvider(withUID vaultUID: String, masterkey: Masterkey) throws -> CloudProvider { - if let error = createVaultProviderWithUIDMasterkeyThrowableError { - throw error - } - createVaultProviderWithUIDMasterkeyCallsCount += 1 - createVaultProviderWithUIDMasterkeyReceivedArguments = (vaultUID: vaultUID, masterkey: masterkey) - createVaultProviderWithUIDMasterkeyReceivedInvocations.append((vaultUID: vaultUID, masterkey: masterkey)) - return try createVaultProviderWithUIDMasterkeyClosure.map({ try $0(vaultUID, masterkey) }) ?? createVaultProviderWithUIDMasterkeyReturnValue - } - - // MARK: - removeVault - - var removeVaultWithUIDThrowableError: Error? - var removeVaultWithUIDCallsCount = 0 - var removeVaultWithUIDCalled: Bool { - removeVaultWithUIDCallsCount > 0 - } - - var removeVaultWithUIDReceivedVaultUID: String? - var removeVaultWithUIDReceivedInvocations: [String] = [] - var removeVaultWithUIDReturnValue: Promise! - var removeVaultWithUIDClosure: ((String) throws -> Promise)? - - func removeVault(withUID vaultUID: String) throws -> Promise { - if let error = removeVaultWithUIDThrowableError { - throw error - } - if let error = removeVaultWithUIDThrowableError { - return Promise(error) - } - removeVaultWithUIDCallsCount += 1 - removeVaultWithUIDReceivedVaultUID = vaultUID - removeVaultWithUIDReceivedInvocations.append(vaultUID) - return try removeVaultWithUIDClosure.map({ try $0(vaultUID) }) ?? removeVaultWithUIDReturnValue - } - - // MARK: - removeAllUnusedFileProviderDomains - - var removeAllUnusedFileProviderDomainsThrowableError: Error? - var removeAllUnusedFileProviderDomainsCallsCount = 0 - var removeAllUnusedFileProviderDomainsCalled: Bool { - removeAllUnusedFileProviderDomainsCallsCount > 0 - } - - var removeAllUnusedFileProviderDomainsReturnValue: Promise! - var removeAllUnusedFileProviderDomainsClosure: (() -> Promise)? - - func removeAllUnusedFileProviderDomains() -> Promise { - if let error = removeAllUnusedFileProviderDomainsThrowableError { - return Promise(error) - } - removeAllUnusedFileProviderDomainsCallsCount += 1 - return removeAllUnusedFileProviderDomainsClosure.map({ $0() }) ?? removeAllUnusedFileProviderDomainsReturnValue - } - - // MARK: - moveVault - - var moveVaultAccountToThrowableError: Error? - var moveVaultAccountToCallsCount = 0 - var moveVaultAccountToCalled: Bool { - moveVaultAccountToCallsCount > 0 - } - - var moveVaultAccountToReceivedArguments: (account: VaultAccount, targetVaultPath: CloudPath)? - var moveVaultAccountToReceivedInvocations: [(account: VaultAccount, targetVaultPath: CloudPath)] = [] - var moveVaultAccountToReturnValue: Promise! - var moveVaultAccountToClosure: ((VaultAccount, CloudPath) -> Promise)? - - func moveVault(account: VaultAccount, to targetVaultPath: CloudPath) -> Promise { - if let error = moveVaultAccountToThrowableError { - return Promise(error) - } - moveVaultAccountToCallsCount += 1 - moveVaultAccountToReceivedArguments = (account: account, targetVaultPath: targetVaultPath) - moveVaultAccountToReceivedInvocations.append((account: account, targetVaultPath: targetVaultPath)) - return moveVaultAccountToClosure.map({ $0(account, targetVaultPath) }) ?? moveVaultAccountToReturnValue - } - - // MARK: - changePassphrase - - var changePassphraseOldPassphraseNewPassphraseForVaultUIDThrowableError: Error? - var changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount = 0 - var changePassphraseOldPassphraseNewPassphraseForVaultUIDCalled: Bool { - changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount > 0 - } - - var changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedArguments: (oldPassphrase: String, newPassphrase: String, vaultUID: String)? - var changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedInvocations: [(oldPassphrase: String, newPassphrase: String, vaultUID: String)] = [] - var changePassphraseOldPassphraseNewPassphraseForVaultUIDReturnValue: Promise! - var changePassphraseOldPassphraseNewPassphraseForVaultUIDClosure: ((String, String, String) -> Promise)? - - func changePassphrase(oldPassphrase: String, newPassphrase: String, forVaultUID vaultUID: String) -> Promise { - if let error = changePassphraseOldPassphraseNewPassphraseForVaultUIDThrowableError { - return Promise(error) - } - changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount += 1 - changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedArguments = (oldPassphrase: oldPassphrase, newPassphrase: newPassphrase, vaultUID: vaultUID) - changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedInvocations.append((oldPassphrase: oldPassphrase, newPassphrase: newPassphrase, vaultUID: vaultUID)) - return changePassphraseOldPassphraseNewPassphraseForVaultUIDClosure.map({ $0(oldPassphrase, newPassphrase, vaultUID) }) ?? changePassphraseOldPassphraseNewPassphraseForVaultUIDReturnValue - } -} - -// swiftlint:enable all -#endif +#warning("TODO: Remove comment") +/* + #if DEBUG + import CryptomatorCloudAccessCore + import CryptomatorCryptoLib + import Foundation + import Promises + + final class VaultManagerMock: VaultManager { + // MARK: - createNewVault + + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainThrowableError: Error? + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount = 0 + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCalled: Bool { + createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount > 0 + } + + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool)? + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool)] = [] + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReturnValue: Promise! + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainClosure: ((String, String, CloudPath, String, Bool) -> Promise)? + + func createNewVault(withVaultUID vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { + if let error = createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainThrowableError { + return Promise(error) + } + createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount += 1 + createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, password: password, storePasswordInKeychain: storePasswordInKeychain) + createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, password: password, storePasswordInKeychain: storePasswordInKeychain)) + return createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultPath, password, storePasswordInKeychain) }) ?? createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReturnValue + } + + // MARK: - createFromExisting + + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError: Error? + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount = 0 + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCalled: Bool { + createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount > 0 + } + + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)? + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)] = [] + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue: Promise! + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure: ((String, String, VaultItem, String, Bool) -> Promise)? + + func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { + if let error = createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError { + return Promise(error) + } + createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount += 1 + createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain) + createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain)) + return createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultItem, password, storePasswordInKeychain) }) ?? createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue + } + + // MARK: - createLegacyFromExisting + + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError: Error? + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount = 0 + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCalled: Bool { + createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount > 0 + } + + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)? + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)] = [] + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue: Promise! + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure: ((String, String, VaultItem, String, Bool) -> Promise)? + + func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { + if let error = createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError { + return Promise(error) + } + createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount += 1 + createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain) + createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain)) + return createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultItem, password, storePasswordInKeychain) }) ?? createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue + } + + // MARK: - manualUnlockVault + + var manualUnlockVaultWithUIDKekThrowableError: Error? + var manualUnlockVaultWithUIDKekCallsCount = 0 + var manualUnlockVaultWithUIDKekCalled: Bool { + manualUnlockVaultWithUIDKekCallsCount > 0 + } + + var manualUnlockVaultWithUIDKekReceivedArguments: (vaultUID: String, kek: [UInt8])? + var manualUnlockVaultWithUIDKekReceivedInvocations: [(vaultUID: String, kek: [UInt8])] = [] + var manualUnlockVaultWithUIDKekReturnValue: CloudProvider! + var manualUnlockVaultWithUIDKekClosure: ((String, [UInt8]) throws -> CloudProvider)? + + func manualUnlockVault(withUID vaultUID: String, kek: [UInt8]) throws -> CloudProvider { + if let error = manualUnlockVaultWithUIDKekThrowableError { + throw error + } + manualUnlockVaultWithUIDKekCallsCount += 1 + manualUnlockVaultWithUIDKekReceivedArguments = (vaultUID: vaultUID, kek: kek) + manualUnlockVaultWithUIDKekReceivedInvocations.append((vaultUID: vaultUID, kek: kek)) + return try manualUnlockVaultWithUIDKekClosure.map({ try $0(vaultUID, kek) }) ?? manualUnlockVaultWithUIDKekReturnValue + } + + // MARK: - createVaultProvider + + var createVaultProviderWithUIDMasterkeyThrowableError: Error? + var createVaultProviderWithUIDMasterkeyCallsCount = 0 + var createVaultProviderWithUIDMasterkeyCalled: Bool { + createVaultProviderWithUIDMasterkeyCallsCount > 0 + } + + var createVaultProviderWithUIDMasterkeyReceivedArguments: (vaultUID: String, masterkey: Masterkey)? + var createVaultProviderWithUIDMasterkeyReceivedInvocations: [(vaultUID: String, masterkey: Masterkey)] = [] + var createVaultProviderWithUIDMasterkeyReturnValue: CloudProvider! + var createVaultProviderWithUIDMasterkeyClosure: ((String, Masterkey) throws -> CloudProvider)? + + func createVaultProvider(withUID vaultUID: String, masterkey: Masterkey) throws -> CloudProvider { + if let error = createVaultProviderWithUIDMasterkeyThrowableError { + throw error + } + createVaultProviderWithUIDMasterkeyCallsCount += 1 + createVaultProviderWithUIDMasterkeyReceivedArguments = (vaultUID: vaultUID, masterkey: masterkey) + createVaultProviderWithUIDMasterkeyReceivedInvocations.append((vaultUID: vaultUID, masterkey: masterkey)) + return try createVaultProviderWithUIDMasterkeyClosure.map({ try $0(vaultUID, masterkey) }) ?? createVaultProviderWithUIDMasterkeyReturnValue + } + + // MARK: - removeVault + + var removeVaultWithUIDThrowableError: Error? + var removeVaultWithUIDCallsCount = 0 + var removeVaultWithUIDCalled: Bool { + removeVaultWithUIDCallsCount > 0 + } + + var removeVaultWithUIDReceivedVaultUID: String? + var removeVaultWithUIDReceivedInvocations: [String] = [] + var removeVaultWithUIDReturnValue: Promise! + var removeVaultWithUIDClosure: ((String) throws -> Promise)? + + func removeVault(withUID vaultUID: String) throws -> Promise { + if let error = removeVaultWithUIDThrowableError { + throw error + } + if let error = removeVaultWithUIDThrowableError { + return Promise(error) + } + removeVaultWithUIDCallsCount += 1 + removeVaultWithUIDReceivedVaultUID = vaultUID + removeVaultWithUIDReceivedInvocations.append(vaultUID) + return try removeVaultWithUIDClosure.map({ try $0(vaultUID) }) ?? removeVaultWithUIDReturnValue + } + + // MARK: - removeAllUnusedFileProviderDomains + + var removeAllUnusedFileProviderDomainsThrowableError: Error? + var removeAllUnusedFileProviderDomainsCallsCount = 0 + var removeAllUnusedFileProviderDomainsCalled: Bool { + removeAllUnusedFileProviderDomainsCallsCount > 0 + } + + var removeAllUnusedFileProviderDomainsReturnValue: Promise! + var removeAllUnusedFileProviderDomainsClosure: (() -> Promise)? + + func removeAllUnusedFileProviderDomains() -> Promise { + if let error = removeAllUnusedFileProviderDomainsThrowableError { + return Promise(error) + } + removeAllUnusedFileProviderDomainsCallsCount += 1 + return removeAllUnusedFileProviderDomainsClosure.map({ $0() }) ?? removeAllUnusedFileProviderDomainsReturnValue + } + + // MARK: - moveVault + + var moveVaultAccountToThrowableError: Error? + var moveVaultAccountToCallsCount = 0 + var moveVaultAccountToCalled: Bool { + moveVaultAccountToCallsCount > 0 + } + + var moveVaultAccountToReceivedArguments: (account: VaultAccount, targetVaultPath: CloudPath)? + var moveVaultAccountToReceivedInvocations: [(account: VaultAccount, targetVaultPath: CloudPath)] = [] + var moveVaultAccountToReturnValue: Promise! + var moveVaultAccountToClosure: ((VaultAccount, CloudPath) -> Promise)? + + func moveVault(account: VaultAccount, to targetVaultPath: CloudPath) -> Promise { + if let error = moveVaultAccountToThrowableError { + return Promise(error) + } + moveVaultAccountToCallsCount += 1 + moveVaultAccountToReceivedArguments = (account: account, targetVaultPath: targetVaultPath) + moveVaultAccountToReceivedInvocations.append((account: account, targetVaultPath: targetVaultPath)) + return moveVaultAccountToClosure.map({ $0(account, targetVaultPath) }) ?? moveVaultAccountToReturnValue + } + + // MARK: - changePassphrase + + var changePassphraseOldPassphraseNewPassphraseForVaultUIDThrowableError: Error? + var changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount = 0 + var changePassphraseOldPassphraseNewPassphraseForVaultUIDCalled: Bool { + changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount > 0 + } + + var changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedArguments: (oldPassphrase: String, newPassphrase: String, vaultUID: String)? + var changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedInvocations: [(oldPassphrase: String, newPassphrase: String, vaultUID: String)] = [] + var changePassphraseOldPassphraseNewPassphraseForVaultUIDReturnValue: Promise! + var changePassphraseOldPassphraseNewPassphraseForVaultUIDClosure: ((String, String, String) -> Promise)? + + func changePassphrase(oldPassphrase: String, newPassphrase: String, forVaultUID vaultUID: String) -> Promise { + if let error = changePassphraseOldPassphraseNewPassphraseForVaultUIDThrowableError { + return Promise(error) + } + changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount += 1 + changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedArguments = (oldPassphrase: oldPassphrase, newPassphrase: newPassphrase, vaultUID: vaultUID) + changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedInvocations.append((oldPassphrase: oldPassphrase, newPassphrase: newPassphrase, vaultUID: vaultUID)) + return changePassphraseOldPassphraseNewPassphraseForVaultUIDClosure.map({ $0(oldPassphrase, newPassphrase, vaultUID) }) ?? changePassphraseOldPassphraseNewPassphraseForVaultUIDReturnValue + } + } + + #endif + */ diff --git a/CryptomatorFileProvider/FileProviderAdapterManager.swift b/CryptomatorFileProvider/FileProviderAdapterManager.swift index e41c0dc5a..c11af6a3c 100644 --- a/CryptomatorFileProvider/FileProviderAdapterManager.swift +++ b/CryptomatorFileProvider/FileProviderAdapterManager.swift @@ -80,6 +80,26 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { return } let provider = try vaultManager.manualUnlockVault(withUID: domainIdentifier.rawValue, kek: kek) + try unlockVaultPostProcessing(provider: provider, + domainIdentifier: domainIdentifier, + dbPath: dbPath, + delegate: delegate, + notificator: notificator) + } + + public func unlockVault(with domainIdentifier: NSFileProviderDomainIdentifier, rawKey: [UInt8], dbPath: URL?, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType) throws { + guard let dbPath = dbPath else { + return + } + let provider = try vaultManager.manualUnlockVault(withUID: domainIdentifier.rawValue, rawKey: rawKey) + try unlockVaultPostProcessing(provider: provider, + domainIdentifier: domainIdentifier, + dbPath: dbPath, + delegate: delegate, + notificator: notificator) + } + + func unlockVaultPostProcessing(provider: CloudProvider, domainIdentifier: NSFileProviderDomainIdentifier, dbPath: URL, delegate: FileProviderAdapterDelegate, notificator: FileProviderNotificatorType) throws { let item = try createAdapterCacheItem(domainIdentifier: domainIdentifier, cloudProvider: provider, dbPath: dbPath, delegate: delegate, notificator: notificator) try vaultKeepUnlockedSettings.setLastUsedDate(Date(), forVaultUID: domainIdentifier.rawValue) adapterCache.cacheItem(item, identifier: domainIdentifier) diff --git a/CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift b/CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift index 0e0e51e37..081a7dfba 100644 --- a/CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift +++ b/CryptomatorFileProvider/ServiceSource/VaultUnlockingServiceSource.swift @@ -61,4 +61,26 @@ public class VaultUnlockingServiceSource: ServiceSource, VaultUnlocking { DDLogInfo("endBiometricalUnlock called for \(vaultUID)") FileProviderAdapterManager.shared.unlockMonitor.endBiometricalUnlock(forVaultUID: vaultUID) } + + public func unlockVault(rawKey: [UInt8], reply: @escaping (NSError?) -> Void) { + let domain = self.domain + let vaultUID = vaultUID + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + guard let notificator = self.notificator else { + DDLogError("Unlocking vault failed, unable to find FileProviderDomain") + reply(VaultManagerError.fileProviderDomainNotFound as NSError) + return + } + do { + try FileProviderAdapterManager.shared.unlockVault(with: domain.identifier, rawKey: rawKey, dbPath: self.dbPath, delegate: self.localURLProvider, notificator: notificator) + FileProviderAdapterManager.shared.unlockMonitor.unlockSucceeded(forVaultUID: vaultUID) + DDLogInfo("Unlocked vault \"\(domain.displayName)\" (\(domain.identifier.rawValue))") + reply(nil) + } catch { + FileProviderAdapterManager.shared.unlockMonitor.unlockFailed(forVaultUID: vaultUID) + DDLogError("Unlocking vault \"\(domain.displayName)\" (\(domain.identifier.rawValue)) failed with error: \(error)") + reply(XPCErrorHelper.bridgeError(error)) + } + } + } } diff --git a/FileProviderExtensionUI/FileProviderCoordinator.swift b/FileProviderExtensionUI/FileProviderCoordinator.swift index 41b645a0b..18946fe22 100644 --- a/FileProviderExtensionUI/FileProviderCoordinator.swift +++ b/FileProviderExtensionUI/FileProviderCoordinator.swift @@ -7,13 +7,15 @@ // import CocoaLumberjackSwift +import CryptomatorCloudAccessCore import CryptomatorCommonCore import CryptomatorFileProvider import FileProviderUI import LocalAuthentication import UIKit -class FileProviderCoordinator { +class FileProviderCoordinator: Coordinator { + lazy var childCoordinators = [Coordinator]() lazy var navigationController: UINavigationController = { let appearance = UINavigationBarAppearance() appearance.configureWithOpaqueBackground() @@ -39,6 +41,8 @@ class FileProviderCoordinator { extensionContext.cancelRequest(withError: NSError(domain: FPUIErrorDomain, code: Int(FPUIExtensionErrorCode.userCancelled.rawValue), userInfo: nil)) } + func start() {} + func startWith(error: Error) { let error = error as NSError let userInfo = error.userInfo @@ -91,7 +95,7 @@ class FileProviderCoordinator { if unlockError == .defaultLock, viewModel.canQuickUnlock { performQuickUnlock(viewModel: viewModel) } else { - showManualPasswordScreen(viewModel: viewModel) + showManualLogin(for: domain, unlockError: unlockError) } } @@ -113,6 +117,68 @@ class FileProviderCoordinator { } } + func showManualLogin(for domain: NSFileProviderDomain, unlockError: UnlockError) { + let vaultUID = domain.identifier.rawValue + let vaultCache = VaultDBCache(dbWriter: CryptomatorDatabase.shared.dbPool) + let vaultAccount: VaultAccount + let provider: CloudProvider + do { + vaultAccount = try VaultAccountDBManager.shared.getAccount(with: vaultUID) + provider = LocalizedCloudProviderDecorator(delegate: try CloudProviderDBManager.shared.getProvider(with: vaultAccount.delegateAccountUID)) + } catch { + handleError(error) + return + } + vaultCache.refreshVaultCache(for: vaultAccount, with: provider).recover { error -> Void in + switch error { + case CloudProviderError.noInternetConnection, LocalizedCloudProviderError.itemNotFound: + break + default: + throw error + } + }.then { + let cachedVault = try vaultCache.getCachedVault(withVaultUID: vaultUID) + if let vaultConfigToken = cachedVault.vaultConfigToken { + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: vaultConfigToken) + switch VaultConfigHelper.getType(for: unverifiedVaultConfig) { + case .hub: + self.showHubLoginScreen(vaultConfig: unverifiedVaultConfig, domain: domain) + case .masterkeyFile: + let viewModel = UnlockVaultViewModel(domain: domain, wrongBiometricalPassword: unlockError == .biometricalUnlockWrongPassword) + self.showManualPasswordScreen(viewModel: viewModel) + case .unknown: + fatalError("TODO: throw error") + } + } else { + let viewModel = UnlockVaultViewModel(domain: domain, wrongBiometricalPassword: unlockError == .biometricalUnlockWrongPassword) + self.showManualPasswordScreen(viewModel: viewModel) + } + }.catch { + self.handleError($0) + } + } + + func showHubLoginScreen(vaultConfig: UnverifiedVaultConfig, domain: NSFileProviderDomain) { + let hubAccount: HubAccount + do { + guard let retrievedHubAccount = try HubAccountManager.shared.getHubAccount(forVaultUID: domain.identifier.rawValue) else { + fatalError("TODO: add error") + } + hubAccount = retrievedHubAccount + } catch { + handleError(error) + return + } + let child = CryptomatorHubVaultUnlockCoordinator(navigationController: navigationController, + domain: domain, + hubAccount: hubAccount, + vaultConfig: vaultConfig) + child.parentCoordinator = self + child.delegate = self + childCoordinators.append(child) + child.start() + } + func showManualPasswordScreen(viewModel: UnlockVaultViewModel) { let unlockVaultVC = UnlockVaultViewController(viewModel: viewModel) unlockVaultVC.coordinator = self @@ -129,4 +195,17 @@ class FileProviderCoordinator { hostViewController.view.addSubview(viewController.view) viewController.didMove(toParent: hostViewController) } + + private func handleError(_ error: Error) { + guard let hostViewController = hostViewController else { + return + } + handleError(error, for: hostViewController) + } +} + +extension FileProviderCoordinator: HubVaultUnlockDelegate { + func unlockedVault() { + done() + } } From 362809556eca2f73de8821050409236420f3233c Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Thu, 9 Mar 2023 17:51:52 +0100 Subject: [PATCH 03/45] Added associated domains --- Cryptomator/Cryptomator.entitlements | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cryptomator/Cryptomator.entitlements b/Cryptomator/Cryptomator.entitlements index 3fc177eb3..d59124a3b 100644 --- a/Cryptomator/Cryptomator.entitlements +++ b/Cryptomator/Cryptomator.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.associated-domains + + applinks:ios.cryptomator.org + com.apple.developer.default-data-protection NSFileProtectionComplete com.apple.security.application-groups From 1ac88785c5c8678ee30724f67716ec1432abb014 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 14 Mar 2023 18:21:01 +0100 Subject: [PATCH 04/45] Increase minimum deployment target to iOS 14 --- Cryptomator.xcodeproj/project.pbxproj | 4 ++-- CryptomatorCommon/Package.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index fdd31812a..6a148b0c1 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -3266,7 +3266,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 2.4.9; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -3328,7 +3328,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 2.4.9; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index fb52eb34e..ee93bd999 100644 --- a/CryptomatorCommon/Package.swift +++ b/CryptomatorCommon/Package.swift @@ -13,7 +13,7 @@ import PackageDescription let package = Package( name: "CryptomatorCommon", platforms: [ - .iOS(.v13) + .iOS(.v14) ], products: [ .library( From 85b619ce8a0b3404b7beb8a4aa683882e0d3d848 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 14 Mar 2023 18:23:04 +0100 Subject: [PATCH 05/45] Increase swift-tools-version to actually use iOS 14 as minimum deployment target --- CryptomatorCommon/Package.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index ee93bd999..cc077cab3 100644 --- a/CryptomatorCommon/Package.swift +++ b/CryptomatorCommon/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.7 // // Package.swift @@ -34,14 +34,14 @@ let package = Package( name: "CryptomatorCommon", dependencies: [ "CryptomatorCommonCore", - "CryptomatorCloudAccess" + .product(name: "CryptomatorCloudAccess", package: "cloud-access-swift") ] ), .target( name: "CryptomatorCommonCore", dependencies: [ - "CocoaLumberjackSwift", - "CryptomatorCloudAccessCore" + .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), + .product(name: "CryptomatorCloudAccessCore", package: "cloud-access-swift") ] ), .testTarget( From a0ebdc0ea91e5701fca8ce79000fe58aa1652a7e Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 14 Mar 2023 22:56:55 +0100 Subject: [PATCH 06/45] First Cleanup - resolved some todos related to error throwing - removed commented code --- ...orHubAuthenticator+HubAuthenticating.swift | 58 ++++++ .../CryptomatorHubAuthenticator.swift | 179 ------------------ .../Hub/CryptomatorHubAuthenticator.swift | 26 +-- 3 files changed, 68 insertions(+), 195 deletions(-) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift delete mode 100644 CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator.swift diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift new file mode 100644 index 000000000..ac5aa6cdd --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift @@ -0,0 +1,58 @@ +// +// CryptomatorHubAuthenticator.swift +// +// +// Created by Philipp Schmid on 22.07.22. +// + +import AppAuth +import Base32 +import CryptoKit +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import UIKit + +enum HubAuthenticationError: Error { + case invalidAuthEndpoint + case invalidTokenEndpoint + case invalidRedirectURL +} + +extension CryptomatorHubAuthenticator: HubAuthenticating { + private static var currentAuthorizationFlow: OIDExternalUserAgentSession? + + public func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState { + guard let authorizationEndpoint = URL(string: hubConfig.authEndpoint) else { + throw HubAuthenticationError.invalidAuthEndpoint + } + guard let tokenEndpoint = URL(string: hubConfig.tokenEndpoint) else { + throw HubAuthenticationError.invalidTokenEndpoint + } + guard let redirectURL = URL(string: "hub.org.cryptomator.ios:/auth") else { + throw HubAuthenticationError.invalidRedirectURL + } + let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint, + tokenEndpoint: tokenEndpoint) + + let request = OIDAuthorizationRequest(configuration: configuration, clientId: hubConfig.clientId, scopes: nil, redirectURL: redirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil) + return try await withCheckedThrowingContinuation({ continuation in + DispatchQueue.main.async { + CryptomatorHubAuthenticator.currentAuthorizationFlow = + OIDAuthState.authState(byPresenting: request, presenting: viewController) { authState, error in + switch (authState, error) { + case let (.some(authState), nil): + continuation.resume(returning: authState) + case let (nil, .some(error)): + continuation.resume(throwing: error) + default: + continuation.resume(throwing: CryptomatorHubAuthenticatorError.unexpectedError) + } + } + } + }) + } +} + +public protocol HubAuthenticating { + func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator.swift deleted file mode 100644 index 2b2bdc1fc..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator.swift +++ /dev/null @@ -1,179 +0,0 @@ -// -// CryptomatorHubAuthenticator.swift -// -// -// Created by Philipp Schmid on 22.07.22. -// - -import AppAuth -import Base32 -import CryptoKit -import CryptomatorCloudAccessCore -import CryptomatorCommonCore -import UIKit - -extension CryptomatorHubAuthenticator: HubAuthenticating { - private static var currentAuthorizationFlow: OIDExternalUserAgentSession? - public func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState { - guard let authorizationEndpoint = URL(string: hubConfig.authEndpoint) else { - fatalError("TODO: throw error") - } - guard let tokenEndpoint = URL(string: hubConfig.tokenEndpoint) else { - fatalError("TODO: throw error") - } - guard let redirectURL = URL(string: "hub.org.cryptomator.ios:/auth") else { - fatalError("TODO: throw error") - } - let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint, - tokenEndpoint: tokenEndpoint) - - let request = OIDAuthorizationRequest(configuration: configuration, clientId: hubConfig.clientId, scopes: nil, redirectURL: redirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil) - return try await withCheckedThrowingContinuation({ continuation in - DispatchQueue.main.async { - CryptomatorHubAuthenticator.currentAuthorizationFlow = - OIDAuthState.authState(byPresenting: request, presenting: viewController) { authState, error in - switch (authState, error) { - case let (.some(authState), nil): - continuation.resume(returning: authState) - case let (nil, .some(error)): - continuation.resume(throwing: error) - default: - continuation.resume(throwing: CryptomatorHubAuthenticatorError.unexpectedError) - } - } - } - }) - } -} - -public protocol HubAuthenticating { - func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState -} - -/* - public class CryptomatorHubAuthenticator { - private static let scheme = "hub+" - private static var currentAuthorizationFlow: OIDExternalUserAgentSession? - public static func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState { - guard let authorizationEndpoint = URL(string: hubConfig.authEndpoint) else { - fatalError("TODO: throw error") - } - guard let tokenEndpoint = URL(string: hubConfig.tokenEndpoint) else { - fatalError("TODO: throw error") - } - guard let redirectURL = URL(string: "hub.org.cryptomator.ios:/auth") else { - fatalError("TODO: throw error") - } - let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint, - tokenEndpoint: tokenEndpoint) - - let request = OIDAuthorizationRequest(configuration: configuration, clientId: hubConfig.clientId, scopes: nil, redirectURL: redirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil) - return try await withCheckedThrowingContinuation({ continuation in - DispatchQueue.main.async { - CryptomatorHubAuthenticator.currentAuthorizationFlow = - OIDAuthState.authState(byPresenting: request, presenting: viewController) { authState, error in - switch (authState, error) { - case let (.some(authState), nil): - continuation.resume(returning: authState) - case let (nil, .some(error)): - continuation.resume(throwing: error) - default: - continuation.resume(throwing: CryptomatorHubAuthenticatorError.unexpectedError) - } - } - } - }) - } - - public static func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { - guard let baseURL = createBaseURL(vaultConfig: vaultConfig) else { - fatalError("TODO throw error") - } - let deviceID = try getDeviceID() - let url = baseURL.appendingPathComponent("/keys").appendingPathComponent("/\(deviceID)") - let (accessToken, _) = try await authState.performAction() - guard let accessToken = accessToken else { - fatalError("TODO throw error") - } - var urlRequest = URLRequest(url: url) - urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] - let (data, response) = try await URLSession.shared.data(with: urlRequest) - switch (response as? HTTPURLResponse)?.statusCode { - case 200: - return .receivedExistingKey(data) - case 403: - return .accessNotGranted - case 404: - return .needsDeviceRegistration - default: - throw CryptomatorHubAuthenticatorError.unexpectedResponse - } - } - - static func createBaseURL(vaultConfig: UnverifiedVaultConfig) -> URL? { - guard let keyId = vaultConfig.keyId, keyId.hasPrefix(scheme) else { - return nil - } - let baseURLPath = keyId.deletingPrefix(scheme) - return URL(string: baseURLPath) - } - - static func getDeviceID() throws -> String { - let publicKey = try CryptomatorHubKeyProvider.shared.getPublicKey() - let digest: SHA256.Digest - if #available(iOS 14.0, *) { - digest = SHA256.hash(data: publicKey.derRepresentation) - } else { - fatalError("TODO: Increase the minimum deployment target or change representation") - } - return digest.data.base16EncodedString - } - - public static func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws { - let deviceID = try getDeviceID() - let publicKey = try CryptomatorHubKeyProvider.shared.getPublicKey() - let derPubKey: Data - if #available(iOS 14.0, *) { - derPubKey = publicKey.derRepresentation - } else { - fatalError("TODO: Increase the minimum deployment target or change representation") - } - let dto = CreateDeviceDto(id: deviceID, name: name, publicKey: derPubKey.base64URLEncodedString()) - guard let devicesResourceURL = URL(string: hubConfig.devicesResourceUrl) else { - fatalError("TODO: throw error") - } - let keyURL = devicesResourceURL.appendingPathComponent("\(deviceID)") - var request = URLRequest(url: keyURL) - request.httpMethod = "PUT" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = try JSONEncoder().encode(dto) - let (accessToken, _) = try await authState.performAction() - guard let accessToken = accessToken else { - fatalError("TODO throw error") - } - request.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] - let (_, response) = try await URLSession.shared.data(with: request) - switch (response as? HTTPURLResponse)?.statusCode { - case 201: - break - case 409: - throw CryptomatorHubAuthenticatorError.deviceNameAlreadyExists - default: - throw CryptomatorHubAuthenticatorError.unexpectedResponse - } - } - - struct CreateDeviceDto: Codable { - let id: String - let name: String - let publicKey: String - } - } - - extension String { - func deletingPrefix(_ prefix: String) -> String { - guard self.hasPrefix(prefix) else { return self } - return String(self.dropFirst(prefix.count)) - } - } - */ diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 5c0508c3f..a1cdc61b1 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -1,8 +1,9 @@ // -// File 2.swift -// +// CryptomatorHubAuthenticator.swift +// CryptomatorCommonCore // // Created by Philipp Schmid on 22.07.22. +// Copyright © 2022 Skymatic GmbH. All rights reserved. // import AppAuthCore @@ -28,6 +29,9 @@ public enum CryptomatorHubAuthenticatorError: Error { case unexpectedError case unexpectedResponse case deviceNameAlreadyExists + + case invalidBaseURL + case invalidDeviceResourceURL } public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving { @@ -36,7 +40,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving public func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { guard let baseURL = createBaseURL(vaultConfig: vaultConfig) else { - fatalError("TODO throw error") + throw CryptomatorHubAuthenticatorError.invalidBaseURL } let deviceID = try getDeviceID() let url = baseURL.appendingPathComponent("/keys").appendingPathComponent("/\(deviceID)") @@ -62,15 +66,10 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving public func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws { let deviceID = try getDeviceID() let publicKey = try CryptomatorHubKeyProvider.shared.getPublicKey() - let derPubKey: Data - if #available(iOS 14.0, *) { - derPubKey = publicKey.derRepresentation - } else { - fatalError("TODO: Increase the minimum deployment target or change representation") - } + let derPubKey = publicKey.derRepresentation let dto = CreateDeviceDto(id: deviceID, name: name, publicKey: derPubKey.base64URLEncodedString()) guard let devicesResourceURL = URL(string: hubConfig.devicesResourceUrl) else { - fatalError("TODO: throw error") + throw CryptomatorHubAuthenticatorError.invalidDeviceResourceURL } let keyURL = devicesResourceURL.appendingPathComponent("\(deviceID)") var request = URLRequest(url: keyURL) @@ -103,12 +102,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving func getDeviceID() throws -> String { let publicKey = try CryptomatorHubKeyProvider.shared.getPublicKey() - let digest: SHA256.Digest - if #available(iOS 14.0, *) { - digest = SHA256.hash(data: publicKey.derRepresentation) - } else { - fatalError("TODO: Increase the minimum deployment target or change representation") - } + let digest = SHA256.hash(data: publicKey.derRepresentation) return digest.data.base16EncodedString } From 761904b0e213b9ece1fdea0e302c8ee628214e99 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 14 Mar 2023 22:57:25 +0100 Subject: [PATCH 07/45] Make ViewModel calls async --- .../AddHubVaultViewModel.swift | 2 +- .../Hub/AddHubVaultView.swift | 17 +++--- .../Hub/HubVaultUnlockViewModel.swift | 4 +- .../Hub/HubVaultViewModel.swift | 53 +++++++++---------- 4 files changed, 40 insertions(+), 36 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift index 40a2a6082..76834e7f3 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift @@ -48,7 +48,7 @@ class AddHubVaultViewModel: HubVaultViewModel, HubVaultAdding { return } self.authState = authState - continueToAccessCheck() + await continueToAccessCheck() } catch { setError(to: error) } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/AddHubVaultView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/AddHubVaultView.swift index da95acba6..6bfbae574 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/AddHubVaultView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/AddHubVaultView.swift @@ -24,27 +24,32 @@ public struct AddHubVaultView: View { Text("This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device.") TextField("Device name", text: $viewModel.deviceName) Button("Register") { - viewModel.register() + Task { + await viewModel.register() + } } case .deviceRegisteredSuccessfully: Text("To access the vault, your device needs to be authorized by the vault owner.") Button("Continue") { - viewModel.continueToAccessCheck() + Task { + await viewModel.continueToAccessCheck() + } } case .accessNotGranted: Text("Your device has not vet been authorized to access this vault. Ask the vault owner to authorize it.") Button("Refresh") { - viewModel.refresh() + Task { + await viewModel.refresh() + } } case .receivedExistingKey: Text("Received existing key") case let .loading(text): - if #available(iOS 14.0, *) { - ProgressView() - } + ProgressView() Text(text) } } + .padding() } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift index 026e921df..2a321bd2b 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift @@ -24,7 +24,9 @@ class HubVaultUnlockViewModel: HubVaultViewModel { self.unlockDelegate = coordinator super.init(initialState: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait."), vaultConfig: vaultConfig, coordinator: coordinator) self.authState = hubAccount.authState - continueToAccessCheck() + Task { + await continueToAccessCheck() + } } override func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift index 1efe23007..1737d9fa7 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift @@ -43,7 +43,7 @@ open class HubVaultViewModel: ObservableObject { self.coordinator = coordinator } - public func register() { + public func register() async { error = nil guard let hubConfig = vaultConfig.hub else { error = AddHubVaultViewModelError.missingHubConfig @@ -54,45 +54,42 @@ open class HubVaultViewModel: ObservableObject { return } - Task { - do { - try await deviceRegisteringService.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState) - } catch { - setError(to: error) - return - } - setState(to: .deviceRegisteredSuccessfully) + do { + try await deviceRegisteringService.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState) + } catch { + setError(to: error) + return } + setState(to: .deviceRegisteredSuccessfully) } - public func continueToAccessCheck() { + public func continueToAccessCheck() async { setError(to: nil) guard let authState = authState else { setError(to: AddHubVaultViewModelError.missingAuthState) return } setState(to: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait.")) - Task { - let authFlow: HubAuthenticationFlow - do { - authFlow = try await hubKeyService.receiveKey(authState: authState, vaultConfig: vaultConfig) - } catch { - setError(to: error) - return - } - switch authFlow { - case let .receivedExistingKey(data): - receivedExistingKey(data: data) - case .accessNotGranted: - setState(to: .accessNotGranted) - case .needsDeviceRegistration: - setState(to: .needsDeviceRegistration) - } + + let authFlow: HubAuthenticationFlow + do { + authFlow = try await hubKeyService.receiveKey(authState: authState, vaultConfig: vaultConfig) + } catch { + setError(to: error) + return + } + switch authFlow { + case let .receivedExistingKey(data): + receivedExistingKey(data: data) + case .accessNotGranted: + setState(to: .accessNotGranted) + case .needsDeviceRegistration: + setState(to: .needsDeviceRegistration) } } - public func refresh() { - continueToAccessCheck() + public func refresh() async { + await continueToAccessCheck() } public func receivedExistingKey(data: Data) { From 650b956fc5d1d6af79953db7b9039e3bf0613f34 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 14 Mar 2023 22:57:56 +0100 Subject: [PATCH 08/45] Remove commented code --- .../AddHubVaultViewModel.swift | 101 ------------------ 1 file changed, 101 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift index 76834e7f3..71ebd27d9 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift @@ -73,104 +73,3 @@ class AddHubVaultViewModel: HubVaultViewModel, HubVaultAdding { } } } - -/* - public class HubVaultViewModel: ObservableObject { - fileprivate(set) var authState: OIDAuthState? - @Published var state: AddHubVaultViewModelState - @Published var deviceName: String = "" - @Published var error: Error? - weak var coordinator: HubVaultCoordinator? - let vaultConfig: UnverifiedVaultConfig - - init(initialState: AddHubVaultViewModelState, vaultConfig: UnverifiedVaultConfig, coordinator: HubVaultCoordinator? = nil) { - self.state = initialState - self.vaultConfig = vaultConfig - self.coordinator = coordinator - } - - func register() { - error = nil - guard let hubConfig = vaultConfig.hub else { - error = AddHubVaultViewModelError.missingHubConfig - return - } - guard let authState = authState else { - error = AddHubVaultViewModelError.missingAuthState - return - } - - Task { - do { - try await CryptomatorHubAuthenticator.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState) - } catch { - setError(to: error) - return - } - setState(to: .deviceRegisteredSuccessfully) - } - } - - func continueToAccessCheck() { - setError(to: nil) - guard let authState = authState else { - setError(to: AddHubVaultViewModelError.missingAuthState) - return - } - setState(to: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait.")) - Task { - let authFlow: HubAuthenticationFlow - do { - authFlow = try await CryptomatorHubAuthenticator.receiveKey(authState: authState, vaultConfig: vaultConfig) - } catch { - setError(to: error) - return - } - switch authFlow { - case .receivedExistingKey(let data): - receivedExistingKey(data: data) - case .accessNotGranted: - setState(to: .accessNotGranted) - case .needsDeviceRegistration: - setState(to: .needsDeviceRegistration) - } - } - } - - func refresh() { - continueToAccessCheck() - } - - func receivedExistingKey(data: Data) { - let privateKey: P384.KeyAgreement.PrivateKey - let jwe: JWE - let hubAccount: HubAccount - do { - privateKey = try CryptomatorHubKeyProvider.shared.getPrivateKey() - jwe = try JWE(compactSerialization: data) - hubAccount = try HubAccount(authState: authState!) - try HubAccountManager.shared.saveHubAccount(hubAccount) - } catch { - setError(to: error) - return - } - receivedExistingKey(jwe: jwe, privateKey: privateKey, hubAccount: hubAccount) - } - - func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) { - fatalError("Abstract method receivedExistingKey(jwe:privateKey:hubAccount:) not implemented") - } - - func setState(to newState: AddHubVaultViewModelState) { - DispatchQueue.main.async { - self.state = newState - } - } - - func setError(to newError: Error?) { - DispatchQueue.main.async { - self.error = newError - } - } - } - */ From 55c13f3563c88f37a80d4f5f19fd0e37960fd1a1 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 15 Mar 2023 19:22:58 +0100 Subject: [PATCH 09/45] Resolve todos related to missing errors --- .../Hub/CryptomatorHubAuthenticator.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index a1cdc61b1..fc39c8093 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -32,6 +32,7 @@ public enum CryptomatorHubAuthenticatorError: Error { case invalidBaseURL case invalidDeviceResourceURL + case missingAccessToken } public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving { @@ -46,7 +47,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving let url = baseURL.appendingPathComponent("/keys").appendingPathComponent("/\(deviceID)") let (accessToken, _) = try await authState.performAction() guard let accessToken = accessToken else { - fatalError("TODO throw error") + throw CryptomatorHubAuthenticatorError.missingAccessToken } var urlRequest = URLRequest(url: url) urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] @@ -78,7 +79,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving request.httpBody = try JSONEncoder().encode(dto) let (accessToken, _) = try await authState.performAction() guard let accessToken = accessToken else { - fatalError("TODO throw error") + throw CryptomatorHubAuthenticatorError.missingAccessToken } request.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] let (_, response) = try await URLSession.shared.data(with: request) From ff282b29a0fc4c9e22962560a19138eef686f67c Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 15 Mar 2023 19:23:27 +0100 Subject: [PATCH 10/45] Code cleanup --- .../Hub/HubVaultUnlockViewModel.swift | 32 ++++++++++------ .../Hub/HubVaultViewModel.swift | 38 +++++++++---------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift index 2a321bd2b..8e94d907f 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift @@ -29,23 +29,33 @@ class HubVaultUnlockViewModel: HubVaultViewModel { } } - override func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) { + override func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) async { let masterkey: Masterkey do { masterkey = try JWEHelper.decrypt(jwe: jwe, with: privateKey) } catch { - setError(to: error) + await setError(to: error) return } - let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) - getXPCPromise.then { xpc in - xpc.proxy.unlockVault(rawKey: masterkey.rawKey) - }.then { - self.unlockDelegate?.unlockedVault() - }.catch { - self.setError(to: $0) - }.always { - self.fileProviderConnector.invalidateXPC(getXPCPromise) + let xpc: XPC + do { + xpc = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) + defer { + fileProviderConnector.invalidateXPC(xpc) + } + try await xpc.proxy.unlockVault(rawKey: masterkey.rawKey).getValue() + unlockDelegate?.unlockedVault() + fileProviderConnector.invalidateXPC(xpc) + } catch { + await setError(to: error) } } } + +extension Promise { + func getValue() async throws -> Value { + try await withCheckedThrowingContinuation({ continuation in + self.then(continuation.resume(returning:)).catch(continuation.resume(throwing:)) + }) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift index 1737d9fa7..b346ea7ed 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift @@ -44,7 +44,7 @@ open class HubVaultViewModel: ObservableObject { } public func register() async { - error = nil + await setError(to: nil) guard let hubConfig = vaultConfig.hub else { error = AddHubVaultViewModelError.missingHubConfig return @@ -57,34 +57,34 @@ open class HubVaultViewModel: ObservableObject { do { try await deviceRegisteringService.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState) } catch { - setError(to: error) + await setError(to: error) return } - setState(to: .deviceRegisteredSuccessfully) + await setState(to: .deviceRegisteredSuccessfully) } public func continueToAccessCheck() async { - setError(to: nil) + await setError(to: nil) guard let authState = authState else { - setError(to: AddHubVaultViewModelError.missingAuthState) + await setError(to: AddHubVaultViewModelError.missingAuthState) return } - setState(to: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait.")) + await setState(to: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait.")) let authFlow: HubAuthenticationFlow do { authFlow = try await hubKeyService.receiveKey(authState: authState, vaultConfig: vaultConfig) } catch { - setError(to: error) + await setError(to: error) return } switch authFlow { case let .receivedExistingKey(data): - receivedExistingKey(data: data) + await receivedExistingKey(data: data) case .accessNotGranted: - setState(to: .accessNotGranted) + await setState(to: .accessNotGranted) case .needsDeviceRegistration: - setState(to: .needsDeviceRegistration) + await setState(to: .needsDeviceRegistration) } } @@ -92,7 +92,7 @@ open class HubVaultViewModel: ObservableObject { await continueToAccessCheck() } - public func receivedExistingKey(data: Data) { + public func receivedExistingKey(data: Data) async { let privateKey: P384.KeyAgreement.PrivateKey let jwe: JWE let hubAccount: HubAccount @@ -102,25 +102,23 @@ open class HubVaultViewModel: ObservableObject { hubAccount = try HubAccount(authState: authState!) try HubAccountManager.shared.saveHubAccount(hubAccount) } catch { - setError(to: error) + await setError(to: error) return } - receivedExistingKey(jwe: jwe, privateKey: privateKey, hubAccount: hubAccount) + await receivedExistingKey(jwe: jwe, privateKey: privateKey, hubAccount: hubAccount) } - open func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) { + open func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) async { fatalError("Abstract method receivedExistingKey(jwe:privateKey:hubAccount:) not implemented") } + @MainActor public func setState(to newState: AddHubVaultViewModelState) { - DispatchQueue.main.async { - self.state = newState - } + state = newState } + @MainActor public func setError(to newError: Error?) { - DispatchQueue.main.async { - self.error = newError - } + error = newError } } From 1b3eb5d16170f85cc4902d0b45df268100a0fbb8 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 15 Mar 2023 19:23:55 +0100 Subject: [PATCH 11/45] Update deprecated Package.swift --- CryptomatorCommon/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index cc077cab3..6b62cc12d 100644 --- a/CryptomatorCommon/Package.swift +++ b/CryptomatorCommon/Package.swift @@ -26,7 +26,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/cryptomator/cloud-access-swift.git", .branch("feature/hub-poc")), + .package(url: "https://github.com/cryptomator/cloud-access-swift.git", branch: "feature/hub-poc"), .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", .upToNextMinor(from: "3.8.0")) ], targets: [ From a0c100b0b60d30d948c68438ffa329ddac090d6c Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 15 Mar 2023 19:24:15 +0100 Subject: [PATCH 12/45] mainactor usage --- .../CryptomatorCommon/AddHubVaultViewModel.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift index 71ebd27d9..b26775954 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift @@ -44,18 +44,18 @@ class AddHubVaultViewModel: HubVaultViewModel, HubVaultAdding { Task { do { guard let authState = try await addHubVaultCoordinator?.authenticate(with: hubConfig) else { - setError(to: AddHubVaultViewModelError.missingAuthState) + await setError(to: AddHubVaultViewModelError.missingAuthState) return } self.authState = authState await continueToAccessCheck() } catch { - setError(to: error) + await setError(to: error) } } } - override func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) { + override func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) async { addVault(jwe: jwe, privateKey: privateKey, hubAccount: hubAccount) } @@ -69,7 +69,9 @@ class AddHubVaultViewModel: HubVaultViewModel, HubVaultAdding { downloadedVaultConfig: downloadedVaultConfig).then { self.addHubVaultCoordinator?.addedVault(withName: self.vaultItem.name, vaultUID: self.vaultUID) }.catch { error in - self.setError(to: error) + Task { + await self.setError(to: error) + } } } } From e4f7acec6a44fcef32168db02b91eec5b23fa19c Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 18 Apr 2023 23:15:13 +0200 Subject: [PATCH 13/45] Refactoring --- Cryptomator.xcodeproj/project.pbxproj | 23 + .../Hub/DetectedHubVaultViewModel.swift | 21 + .../AddVault/Hub/HubAddVaultCoordinator.swift | 97 ++++ .../OpenExistingVaultCoordinator.swift | 17 +- .../AddHubVaultViewModel.swift | 77 --- ...orHubAuthenticator+HubAuthenticating.swift | 4 - .../CryptomatorHubCoordinator.swift | 61 --- .../CryptomatorErrorView.swift | 26 + .../CryptomatorSuccessView.swift | 47 ++ .../Hub/CryptomatorHubAuthenticator.swift | 3 + .../Hub/HubAuthenticating.swift | 7 + .../Hub/HubAuthenticationFlowDelegate.swift | 6 + ...View.swift => HubAuthenticationView.swift} | 36 +- .../Hub/HubAuthenticationViewModel.swift | 132 +++++ .../Hub/HubLoginView.swift | 18 + .../Hub/HubUserLogin.swift | 7 + .../Hub/HubVaultCoordinator.swift | 64 --- .../Hub/HubVaultUnlockViewModel.swift | 61 --- .../Hub/HubVaultViewController.swift | 37 -- .../Hub/HubVaultViewModel.swift | 124 ----- .../Hub/HubXPCLoginCoordinator.swift | 72 +++ .../Manager/ExistingHubVault.swift | 20 + .../Manager/VaultDBManager.swift | 27 +- .../Mocks/VaultManagerMock.swift | 482 ++++++++++-------- .../Promise+StructuredConcurrency.swift | 9 + ...CreateNewVaultPasswordViewModelTests.swift | 8 + .../FileProviderCoordinator.swift | 31 +- 27 files changed, 807 insertions(+), 710 deletions(-) create mode 100644 Cryptomator/AddVault/Hub/DetectedHubVaultViewModel.swift create mode 100644 Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift delete mode 100644 CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift delete mode 100644 CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubCoordinator.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorErrorView.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSuccessView.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticating.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift rename CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/{AddHubVaultView.swift => HubAuthenticationView.swift} (63%) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubLoginView.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserLogin.swift delete mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultCoordinator.swift delete mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift delete mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewController.swift delete mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/ExistingHubVault.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Promise+StructuredConcurrency.swift diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 6a148b0c1..609cc260b 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -290,6 +290,7 @@ 4AC005F327C3D932006FFE87 /* PremiumManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC005F227C3D932006FFE87 /* PremiumManagerMock.swift */; }; 4AC1157627F5BD890023F51B /* Promise+AllIgnoringResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */; }; 4AC1157827F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC1157727F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift */; }; + 4AC49CA029EF3BA6007AF41C /* DetectedHubVaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC49C9F29EF3BA6007AF41C /* DetectedHubVaultViewModel.swift */; }; 4AC86270273598CC00E15BA5 /* UIViewController+ProgressHUDError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC8626F273598CC00E15BA5 /* UIViewController+ProgressHUDError.swift */; }; 4AD0F61C24AF203F0026B765 /* FileProvider+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD0F61B24AF203F0026B765 /* FileProvider+Actions.swift */; }; 4AD3D7D6282EBDE7008188CD /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD3D7D5282EBDE7008188CD /* Intents.framework */; }; @@ -360,6 +361,8 @@ 4AF91CEB25A7306E00ACF01E /* DatabaseManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91CEA25A7306E00ACF01E /* DatabaseManagerTests.swift */; }; 4AF91CF425A8BB0D00ACF01E /* VaultListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91CF325A8BB0D00ACF01E /* VaultListViewModelTests.swift */; }; 4AF91D0D25A8D5EF00ACF01E /* ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91D0C25A8D5EF00ACF01E /* ListViewModel.swift */; }; + 4AF9D44929C262B800EB3822 /* CryptomatorCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 4AF9D44829C262B800EB3822 /* CryptomatorCommon */; }; + 4AF9D44B29C293E600EB3822 /* HubAddVaultCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF9D44A29C293E600EB3822 /* HubAddVaultCoordinator.swift */; }; 4AFBFA142829206D00E30818 /* UploadProgressAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFBFA132829206D00E30818 /* UploadProgressAlertController.swift */; }; 4AFBFA1628293FE200E30818 /* UploadRetryingServiceSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFBFA1528293FE200E30818 /* UploadRetryingServiceSourceTests.swift */; }; 4AFBFA182829414A00E30818 /* ProgressManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */; }; @@ -821,6 +824,7 @@ 4AC005F227C3D932006FFE87 /* PremiumManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumManagerMock.swift; sourceTree = ""; }; 4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+AllIgnoringResult.swift"; sourceTree = ""; }; 4AC1157727F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+AllIgnoringResultsTests.swift"; sourceTree = ""; }; + 4AC49C9F29EF3BA6007AF41C /* DetectedHubVaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedHubVaultViewModel.swift; sourceTree = ""; }; 4AC8626F273598CC00E15BA5 /* UIViewController+ProgressHUDError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+ProgressHUDError.swift"; sourceTree = ""; }; 4AD0F61B24AF203F0026B765 /* FileProvider+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProvider+Actions.swift"; sourceTree = ""; }; 4AD3D7D4282EBDE7008188CD /* CryptomatorIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = CryptomatorIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -895,6 +899,7 @@ 4AF91CEA25A7306E00ACF01E /* DatabaseManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManagerTests.swift; sourceTree = ""; }; 4AF91CF325A8BB0D00ACF01E /* VaultListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListViewModelTests.swift; sourceTree = ""; }; 4AF91D0C25A8D5EF00ACF01E /* ListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewModel.swift; sourceTree = ""; }; + 4AF9D44A29C293E600EB3822 /* HubAddVaultCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubAddVaultCoordinator.swift; sourceTree = ""; }; 4AFBFA132829206D00E30818 /* UploadProgressAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadProgressAlertController.swift; sourceTree = ""; }; 4AFBFA1528293FE200E30818 /* UploadRetryingServiceSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadRetryingServiceSourceTests.swift; sourceTree = ""; }; 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressManagerMock.swift; sourceTree = ""; }; @@ -1058,6 +1063,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4AF9D44929C262B800EB3822 /* CryptomatorCommon in Frameworks */, 4A9BED67268F379300721BAA /* libCryptomatorFileProvider.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1439,6 +1445,7 @@ 4A3D655E268099F9000DA764 /* VaultCoordinatorError.swift */, 4A2FD08125B5E2BA008565C8 /* VaultInstalling.swift */, 4A644B45267A3D21008CBB9A /* CreateNewVault */, + 4AF9D44C29C293F800EB3822 /* Hub */, 4A1EB0D6268A6CF5006D072B /* LocalVault */, 4AA8613F25C1AC4D002A59F5 /* OpenExistingVault */, ); @@ -1871,6 +1878,15 @@ path = DB; sourceTree = ""; }; + 4AF9D44C29C293F800EB3822 /* Hub */ = { + isa = PBXGroup; + children = ( + 4AF9D44A29C293E600EB3822 /* HubAddVaultCoordinator.swift */, + 4AC49C9F29EF3BA6007AF41C /* DetectedHubVaultViewModel.swift */, + ); + path = Hub; + sourceTree = ""; + }; 740375D82587AE7B0023FF53 /* CryptomatorFileProvider */ = { isa = PBXGroup; children = ( @@ -2131,6 +2147,7 @@ ); name = FileProviderExtensionUI; packageProductDependencies = ( + 4AF9D44829C262B800EB3822 /* CryptomatorCommon */, ); productName = "File Provider ExtensionUI"; productReference = 4AA621E4249A6A8400A0BCBD /* FileProviderExtensionUI.appex */; @@ -2703,6 +2720,7 @@ 4A4246F827565D87005BE82D /* PoppingCloseCoordinator.swift in Sources */, 4A66F58B25C489C7001BE15E /* OpenExistingVaultPasswordViewModel.swift in Sources */, 4A03258125A36B7D00E63D7A /* UIViewController+Preview.swift in Sources */, + 4AF9D44B29C293E600EB3822 /* HubAddVaultCoordinator.swift in Sources */, 4A21B49226BBFFE9000D13DF /* AttributedTextHeaderFooterView.swift in Sources */, 4A707802278DC32800AEF4CE /* VaultKeepUnlockedViewModel.swift in Sources */, 4A90E7C327C79DCF00BC858B /* PurchaseCell.swift in Sources */, @@ -2798,6 +2816,7 @@ 4A2FD08B25B5E437008565C8 /* OpenExistingVaultCoordinator.swift in Sources */, 7469AD9A266E26B0000DCD45 /* URL+Zip.swift in Sources */, 4AB8539026BA844300555F00 /* Publisher+OptionalAssign.swift in Sources */, + 4AC49CA029EF3BA6007AF41C /* DetectedHubVaultViewModel.swift in Sources */, 4A1C6D58274CE5BF00B41FFF /* LoadingCell.swift in Sources */, 4A3D65642680A4B7000DA764 /* LocalFileSystemAuthenticationViewController.swift in Sources */, 4AB1D4F427D61035009060AB /* AutoHidingLabel.swift in Sources */, @@ -3712,6 +3731,10 @@ package = 4AED9A6D286B38D900352951 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = Introspect; }; + 4AF9D44829C262B800EB3822 /* CryptomatorCommon */ = { + isa = XCSwiftPackageProductDependency; + productName = CryptomatorCommon; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4A5E5B212453119100BD6298 /* Project object */; diff --git a/Cryptomator/AddVault/Hub/DetectedHubVaultViewModel.swift b/Cryptomator/AddVault/Hub/DetectedHubVaultViewModel.swift new file mode 100644 index 000000000..38d16c837 --- /dev/null +++ b/Cryptomator/AddVault/Hub/DetectedHubVaultViewModel.swift @@ -0,0 +1,21 @@ +import CryptomatorCommonCore +import Foundation +import SwiftUI + +struct DetectedHubVaultViewModel { + let backgroundColor = Color(UIColor.cryptomatorBackground) + let buttonColor = Color(UIColor.cryptomatorPrimary) + let description: String = "Detected Hub vault\nDo you want to login?" + let buttonText: String = "Login" + let onButtonTap: () -> Void +} + +extension CryptomatorSuccessView { + init(viewModel: DetectedHubVaultViewModel) { + self.init(text: viewModel.description, + buttonText: viewModel.buttonText, + onButtonTap: viewModel.onButtonTap, + buttonColor: viewModel.buttonColor, + backgroundColor: viewModel.backgroundColor) + } +} diff --git a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift new file mode 100644 index 000000000..7b1d34bfd --- /dev/null +++ b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift @@ -0,0 +1,97 @@ +// +// HubAddVaultCoordinator.swift +// Cryptomator +// +// Created by Philipp Schmid on 16.03.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import AppAuthCore +import CocoaLumberjackSwift +import CryptoKit +import CryptomatorCloudAccessCore +import CryptomatorCommon +import CryptomatorCommonCore +import JOSESwift +import SwiftUI +import UIKit + +class AddHubVaultCoordinator: Coordinator { + var childCoordinators = [Coordinator]() + var navigationController: UINavigationController + let downloadedVaultConfig: DownloadedVaultConfig + let vaultUID: String + let accountUID: String + let vaultItem: VaultItem + let hubAuthenticator: HubAuthenticating + let vaultManager: VaultManager + weak var parentCoordinator: Coordinator? + weak var delegate: (VaultInstalling & AnyObject)? + + init(navigationController: UINavigationController, + downloadedVaultConfig: DownloadedVaultConfig, + vaultUID: String, + accountUID: String, + vaultItem: VaultItem, + hubAuthenticator: HubAuthenticating, + vaultManager: VaultManager = VaultDBManager.shared) { + self.navigationController = navigationController + self.downloadedVaultConfig = downloadedVaultConfig + self.vaultUID = vaultUID + self.accountUID = accountUID + self.vaultItem = vaultItem + self.hubAuthenticator = hubAuthenticator + self.vaultManager = vaultManager + } + + func start() { + let viewModel = DetectedHubVaultViewModel(onButtonTap: { [weak self] in + Task { [weak self] in + await self?.login() + } + }) + let viewController = UIHostingController(rootView: CryptomatorSuccessView(viewModel: viewModel)) + navigationController.pushViewController(viewController, animated: true) + } + + func login() async { + let viewModel = HubAuthenticationViewModel(vaultConfig: downloadedVaultConfig.vaultConfig, + hubUserAuthenticator: self, + delegate: self) + await viewModel.login() + DispatchQueue.main.sync { + let viewController = UIHostingController(rootView: HubAuthenticationView(viewModel: viewModel)) + navigationController.pushViewController(viewController, animated: false) + } + } +} + +extension AddHubVaultCoordinator: HubAuthenticationFlowDelegate { + func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async { + let hubVault = ExistingHubVault(vaultUID: vaultUID, + delegateAccountUID: accountUID, + jweData: jwe.compactSerializedData, + privateKey: privateKey, + vaultItem: vaultItem, + downloadedVaultConfig: downloadedVaultConfig) + do { + try await vaultManager.addExistingHubVault(hubVault).getValue() + childDidFinish(self) + await showSuccessfullyAddedVault() + } catch { + DDLogError("Add existing Hub vault failed: \(error)") + handleError(error, for: navigationController) + } + } + + @MainActor + private func showSuccessfullyAddedVault() { + delegate?.showSuccessfullyAddedVault(withName: vaultItem.name, vaultUID: vaultUID) + } +} + +extension AddHubVaultCoordinator: HubUserLogin { + public func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState { + try await hubAuthenticator.authenticate(with: hubConfig, from: navigationController) + } +} diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift index 72d137348..92cf84ae9 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift @@ -176,7 +176,7 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder case .hub: handleHubVaultConfig(downloadedVaultConfig, vaultItem: vaultItem) case .unknown: - fatalError("TODO: Display unsupported vault config error") + handleError(error: OpenExistingVaultCoordinatorError.unsupportedVaultConfig) } } @@ -190,8 +190,14 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder } private func handleHubVaultConfig(_ downloadedVaultConfig: DownloadedVaultConfig, vaultItem: VaultItem) { - let child = CryptomatorHubCoordinator(vaultItem: vaultItem, accountUID: account.accountUID, downloadedVaultConfig: downloadedVaultConfig, navigationController: navigationController) + let child = AddHubVaultCoordinator(navigationController: navigationController, + downloadedVaultConfig: downloadedVaultConfig, + vaultUID: UUID().uuidString, + accountUID: account.accountUID, + vaultItem: vaultItem, + hubAuthenticator: CryptomatorHubAuthenticator.shared) child.parentCoordinator = self + child.delegate = self childCoordinators.append(child) child.start() } @@ -216,8 +222,7 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder } } -extension AuthenticatedOpenExistingVaultCoordinator: CryptomatorHubCoordinatorDelegate { - func addedVault(withName name: String, vaultUID: String) { - showSuccessfullyAddedVault(withName: name, vaultUID: vaultUID) - } +enum OpenExistingVaultCoordinatorError: Error { + case unsupportedVaultConfig + // TODO: add Localization } diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift deleted file mode 100644 index b26775954..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommon/AddHubVaultViewModel.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// AddHubVaultViewModel.swift -// Cryptomator -// -// Created by Philipp Schmid on 21.07.22. -// Copyright © 2022 Skymatic GmbH. All rights reserved. -// - -import AppAuthCore -import CryptoKit -import CryptomatorCloudAccessCore -import CryptomatorCommonCore -import CryptomatorCryptoLib -import FileProvider -import Foundation -import JOSESwift -import Promises - -class AddHubVaultViewModel: HubVaultViewModel, HubVaultAdding { - let downloadedVaultConfig: DownloadedVaultConfig - let vaultItem: VaultItem - let vaultManager: VaultManager - let delegateAccountUID: String - let vaultUID: String - private weak var addHubVaultCoordinator: AddHubVaultCoordinator? - - init(downloadedVaultConfig: DownloadedVaultConfig, vaultItem: VaultItem, vaultUID: String, delegateAccountUID: String, vaultManager: VaultManager = VaultDBManager.shared, coordinator: (HubVaultCoordinator & AddHubVaultCoordinator)? = nil) { - self.downloadedVaultConfig = downloadedVaultConfig - self.vaultItem = vaultItem - self.vaultUID = vaultUID - self.delegateAccountUID = delegateAccountUID - self.vaultManager = vaultManager - self.addHubVaultCoordinator = coordinator - super.init(initialState: .detectedVault, vaultConfig: downloadedVaultConfig.vaultConfig, coordinator: coordinator) - } - - func login() { - error = nil - let vaultConfig = downloadedVaultConfig.vaultConfig - guard let hubConfig = vaultConfig.hub else { - error = AddHubVaultViewModelError.missingHubConfig - return - } - Task { - do { - guard let authState = try await addHubVaultCoordinator?.authenticate(with: hubConfig) else { - await setError(to: AddHubVaultViewModelError.missingAuthState) - return - } - self.authState = authState - await continueToAccessCheck() - } catch { - await setError(to: error) - } - } - } - - override func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) async { - addVault(jwe: jwe, privateKey: privateKey, hubAccount: hubAccount) - } - - private func addVault(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) { - vaultManager.addExistingHubVault(vaultUID: vaultUID, - delegateAccountUID: delegateAccountUID, - hubUserID: hubAccount.userID, - jweData: jwe.compactSerializedData, - privateKey: privateKey, - vaultItem: vaultItem, - downloadedVaultConfig: downloadedVaultConfig).then { - self.addHubVaultCoordinator?.addedVault(withName: self.vaultItem.name, vaultUID: self.vaultUID) - }.catch { error in - Task { - await self.setError(to: error) - } - } - } -} diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift index ac5aa6cdd..a4e11951c 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift @@ -52,7 +52,3 @@ extension CryptomatorHubAuthenticator: HubAuthenticating { }) } } - -public protocol HubAuthenticating { - func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState -} diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubCoordinator.swift deleted file mode 100644 index 93c4f13ec..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubCoordinator.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// CryptomatorHubCoordinator.swift -// -// -// Created by Philipp Schmid on 22.07.22. -// - -import AppAuthCore -import CryptomatorCloudAccessCore -import CryptomatorCommonCore -import UIKit - -public class CryptomatorHubCoordinator: Coordinator, HubVaultCoordinator, AddHubVaultCoordinator { - public lazy var childCoordinators = [Coordinator]() - public var navigationController: UINavigationController - public weak var parentCoordinator: Coordinator? - let vaultItem: VaultItem - let accountUID: String - let downloadedVaultConfig: DownloadedVaultConfig - - public init(vaultItem: VaultItem, accountUID: String, downloadedVaultConfig: DownloadedVaultConfig, navigationController: UINavigationController) { - self.accountUID = accountUID - self.downloadedVaultConfig = downloadedVaultConfig - self.vaultItem = vaultItem - self.navigationController = navigationController - } - - public func start() { - let viewModel = AddHubVaultViewModel(downloadedVaultConfig: downloadedVaultConfig, vaultItem: vaultItem, vaultUID: UUID().uuidString, delegateAccountUID: accountUID, coordinator: self) - let addHubVaultVC = HubVaultViewController(viewModel: viewModel) - navigationController.pushViewController(addHubVaultVC, animated: true) - } - - public func handleError(_ error: Error) { - handleError(error, for: navigationController) { - self.navigationController.popViewController(animated: true) - self.parentCoordinator?.childDidFinish(self) - } - } - - public func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState { - return try await CryptomatorHubAuthenticator.shared.authenticate(with: hubConfig, from: navigationController) - } - - public func addedVault(withName name: String, vaultUID: String) { - guard let delegate = parentCoordinator as? CryptomatorHubCoordinatorDelegate else { - return - } - delegate.addedVault(withName: name, vaultUID: vaultUID) - parentCoordinator?.childDidFinish(self) - } -} - -public protocol CryptomatorHubCoordinatorDelegate: AnyObject { - func addedVault(withName name: String, vaultUID: String) -} - -protocol AddHubVaultCoordinator: AnyObject { - func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState - func addedVault(withName name: String, vaultUID: String) -} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorErrorView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorErrorView.swift new file mode 100644 index 000000000..07cca92fc --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorErrorView.swift @@ -0,0 +1,26 @@ +import SwiftUI + +public struct CryptomatorErrorView: View { + let text: String? + + public init(text: String? = nil) { + self.text = text + } + + public var body: some View { + VStack(spacing: 20) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 120)) + .foregroundColor(Color(UIColor.cryptomatorYellow)) + if let text { + Text(text) + } + }.padding(.vertical, 20) + } +} + +struct CryptomatorErrorView_Previews: PreviewProvider { + static var previews: some View { + CryptomatorErrorView() + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSuccessView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSuccessView.swift new file mode 100644 index 000000000..3c1e2d434 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSuccessView.swift @@ -0,0 +1,47 @@ +import SwiftUI + +public struct CryptomatorSuccessView: View { + let text: String + let buttonText: String + let onButtonTap: () -> Void + let buttonColor: Color + let backgroundColor: Color + + public init(text: String, buttonText: String, onButtonTap: @escaping () -> Void, buttonColor: Color, backgroundColor: Color) { + self.text = text + self.buttonText = buttonText + self.onButtonTap = onButtonTap + self.buttonColor = buttonColor + self.backgroundColor = backgroundColor + } + + public var body: some View { + ZStack { + backgroundColor + VStack(spacing: 32) { + Spacer() + Image("bot-vault") + Text(text) + Spacer() + Button { + onButtonTap() + } label: { + Text(buttonText) + .foregroundColor(.white) + .bold() + .padding() + .frame(maxWidth: .infinity) + .background(buttonColor) + .cornerRadius(8) + .padding(.horizontal) + } + } + } + } +} + +struct CryptomatorSuccessView_Previews: PreviewProvider { + static var previews: some View { + CryptomatorSuccessView(text: "Lorem \nipsum", buttonText: "Continue", onButtonTap: {}, buttonColor: .blue, backgroundColor: .clear) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index fc39c8093..42a3532ea 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -15,6 +15,7 @@ public enum HubAuthenticationFlow { case receivedExistingKey(Data) case accessNotGranted case needsDeviceRegistration + case licenseExceeded } public protocol HubDeviceRegistering { @@ -55,6 +56,8 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving switch (response as? HTTPURLResponse)?.statusCode { case 200: return .receivedExistingKey(data) + case 402: + return .licenseExceeded case 403: return .accessNotGranted case 404: diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticating.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticating.swift new file mode 100644 index 000000000..2b074a44b --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticating.swift @@ -0,0 +1,7 @@ +import AppAuthCore +import CryptomatorCloudAccessCore +import UIKit + +public protocol HubAuthenticating { + func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift new file mode 100644 index 000000000..1e37d9d2b --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift @@ -0,0 +1,6 @@ +import CryptoKit +import JOSESwift + +public protocol HubAuthenticationFlowDelegate: AnyObject { + func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/AddHubVaultView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift similarity index 63% rename from CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/AddHubVaultView.swift rename to CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift index 6bfbae574..ebee58819 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/AddHubVaultView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift @@ -1,25 +1,15 @@ -// -// AddHubVaultView.swift -// Cryptomator -// -// Created by Philipp Schmid on 21.07.22. -// Copyright © 2022 Skymatic GmbH. All rights reserved. -// - import SwiftUI -public struct AddHubVaultView: View { - @ObservedObject var viewModel: HubVaultViewModel +public struct HubAuthenticationView: View { + @ObservedObject var viewModel: HubAuthenticationViewModel + + public init(viewModel: HubAuthenticationViewModel) { + self.viewModel = viewModel + } + public var body: some View { VStack { - switch viewModel.state { - case .detectedVault: - Text("Detected Hub vault") - Button("Login") { - if let loginViewModel = viewModel as? HubVaultAdding { - loginViewModel.login() - } - } + switch viewModel.authenticationFlowState { case .needsDeviceRegistration: Text("This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device.") TextField("Device name", text: $viewModel.deviceName) @@ -47,12 +37,14 @@ public struct AddHubVaultView: View { case let .loading(text): ProgressView() Text(text) + case .userLogin: + HubLoginView(onLogin: { Task { await viewModel.login() }}) + case .licenseExceeded: + CryptomatorErrorView(text: "Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license.") + case let .error(info): + CryptomatorErrorView(text: info) } } .padding() } } - -public protocol HubVaultAdding { - func login() -} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift new file mode 100644 index 000000000..9f28beedf --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -0,0 +1,132 @@ +import AppAuthCore +import CryptoKit +import CryptomatorCloudAccessCore +import Foundation +import JOSESwift + +public enum HubAuthenticationViewModelError: Error { + case missingHubConfig + case missingAuthState +} + +public class HubAuthenticationViewModel: ObservableObject { + public enum State: Equatable { + case userLogin + case receivedExistingKey + case accessNotGranted + case licenseExceeded + case deviceRegisteredSuccessfully + case needsDeviceRegistration + case loading(text: String) + case error(description: String) + } + + @Published var authenticationFlowState: State = .userLogin + @Published public var deviceName: String = "" + + private let vaultConfig: UnverifiedVaultConfig + private let deviceRegisteringService: HubDeviceRegistering + private let hubKeyService: HubKeyReceiving + private let hubUserAuthenticator: HubUserLogin + + private var authState: OIDAuthState? + private weak var delegate: HubAuthenticationFlowDelegate? + + public init(vaultConfig: UnverifiedVaultConfig, + deviceRegisteringService: HubDeviceRegistering = CryptomatorHubAuthenticator.shared, + hubUserAuthenticator: HubUserLogin, + hubKeyService: HubKeyReceiving = CryptomatorHubAuthenticator.shared, + delegate: HubAuthenticationFlowDelegate?) { + self.vaultConfig = vaultConfig + self.deviceRegisteringService = deviceRegisteringService + self.hubUserAuthenticator = hubUserAuthenticator + self.hubKeyService = hubKeyService + self.delegate = delegate + } + + public func login() async { + guard let hubConfig = vaultConfig.hub else { + await setStateToErrorState(with: HubAuthenticationViewModelError.missingHubConfig) + return + } + do { + authState = try await hubUserAuthenticator.authenticate(with: hubConfig) + await continueToAccessCheck() + } catch let error as NSError where error.domain == OIDGeneralErrorDomain && error.code == OIDErrorCode.userCanceledAuthorizationFlow.rawValue { + // ignore user cancellation + } catch { + await setStateToErrorState(with: error) + } + } + + public func register() async { + guard let hubConfig = vaultConfig.hub else { + await setStateToErrorState(with: HubAuthenticationViewModelError.missingHubConfig) + return + } + guard let authState = authState else { + await setStateToErrorState(with: HubAuthenticationViewModelError.missingAuthState) + return + } + + do { + try await deviceRegisteringService.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState) + } catch { + await setStateToErrorState(with: error) + return + } + await setState(to: .deviceRegisteredSuccessfully) + } + + public func refresh() async { + await continueToAccessCheck() + } + + public func continueToAccessCheck() async { + guard let authState = authState else { + await setStateToErrorState(with: HubAuthenticationViewModelError.missingAuthState) + return + } + await setState(to: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait.")) + + let authFlow: HubAuthenticationFlow + do { + authFlow = try await hubKeyService.receiveKey(authState: authState, vaultConfig: vaultConfig) + } catch { + await setStateToErrorState(with: error) + return + } + switch authFlow { + case let .receivedExistingKey(data): + await receivedExistingKey(data: data) + case .accessNotGranted: + await setState(to: .accessNotGranted) + case .needsDeviceRegistration: + await setState(to: .needsDeviceRegistration) + case .licenseExceeded: + await setState(to: .licenseExceeded) + } + } + + private func receivedExistingKey(data: Data) async { + let privateKey: P384.KeyAgreement.PrivateKey + let jwe: JWE + do { + privateKey = try CryptomatorHubKeyProvider.shared.getPrivateKey() + jwe = try JWE(compactSerialization: data) + } catch { + await setStateToErrorState(with: error) + return + } + await delegate?.receivedExistingKey(jwe: jwe, privateKey: privateKey) + } + + @MainActor + private func setState(to newState: State) { + authenticationFlowState = newState + } + + private func setStateToErrorState(with error: Error) async { + await setState(to: .error(description: error.localizedDescription)) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubLoginView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubLoginView.swift new file mode 100644 index 000000000..849c19f05 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubLoginView.swift @@ -0,0 +1,18 @@ +import SwiftUI + +struct HubLoginView: View { + var onLogin: () -> Void + + var body: some View { + Text("Login to unlock your vault") + Button("Login") { + onLogin() + } + } +} + +struct HubLoginView_Previews: PreviewProvider { + static var previews: some View { + HubLoginView(onLogin: {}) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserLogin.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserLogin.swift new file mode 100644 index 000000000..219bae4b1 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserLogin.swift @@ -0,0 +1,7 @@ +import AppAuthCore +import CryptomatorCloudAccessCore +import Foundation + +public protocol HubUserLogin { + func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultCoordinator.swift deleted file mode 100644 index ccaa37da8..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultCoordinator.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// File.swift -// -// -// Created by Philipp Schmid on 22.07.22. -// - -import AppAuthCore -import CryptomatorCloudAccessCore -import Foundation -import UIKit - -public protocol HubVaultCoordinator: AnyObject { - var parentCoordinator: Coordinator? { get set } - func handleError(_ error: Error) -} - -public extension HubVaultCoordinator where Self: Coordinator { - func handleError(_ error: Error) { - handleError(error, for: navigationController) { - self.navigationController.popViewController(animated: true) - self.parentCoordinator?.childDidFinish(self) - } - } -} - -public protocol HubVaultUnlockDelegate: AnyObject { - func unlockedVault() -} - -public class CryptomatorHubVaultUnlockCoordinator: Coordinator, HubVaultCoordinator { - public lazy var childCoordinators = [Coordinator]() - public var navigationController: UINavigationController - public weak var parentCoordinator: Coordinator? - public weak var delegate: HubVaultUnlockDelegate? - let domain: NSFileProviderDomain - let hubAccount: HubAccount - let vaultConfig: UnverifiedVaultConfig - - public init(navigationController: UINavigationController, domain: NSFileProviderDomain, hubAccount: HubAccount, vaultConfig: UnverifiedVaultConfig, parentCoordinator: Coordinator? = nil) { - self.navigationController = navigationController - self.domain = domain - self.hubAccount = hubAccount - self.vaultConfig = vaultConfig - self.parentCoordinator = parentCoordinator - } - - public func start() { - let viewModel = HubVaultUnlockViewModel(hubAccount: hubAccount, - domain: domain, - fileProviderConnector: FileProviderXPCConnector.shared, - vaultConfig: vaultConfig, - coordinator: self) - let addHubVaultVC = HubVaultViewController(viewModel: viewModel) - navigationController.pushViewController(addHubVaultVC, animated: true) - } -} - -extension CryptomatorHubVaultUnlockCoordinator: HubVaultUnlockDelegate { - public func unlockedVault() { - delegate?.unlockedVault() - parentCoordinator?.childDidFinish(self) - } -} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift deleted file mode 100644 index 8e94d907f..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultUnlockViewModel.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// HubVaultUnlockViewModel.swift -// -// -// Created by Philipp Schmid on 22.07.22. -// - -import CryptoKit -import CryptomatorCloudAccessCore -import CryptomatorCryptoLib -import FileProvider -import Foundation -import JOSESwift -import Promises - -class HubVaultUnlockViewModel: HubVaultViewModel { - let fileProviderConnector: FileProviderConnector - let domain: NSFileProviderDomain - private weak var unlockDelegate: HubVaultUnlockDelegate? - - init(hubAccount: HubAccount, domain: NSFileProviderDomain, fileProviderConnector: FileProviderConnector, vaultConfig: UnverifiedVaultConfig, coordinator: (HubVaultCoordinator & HubVaultUnlockDelegate)? = nil) { - self.fileProviderConnector = fileProviderConnector - self.domain = domain - self.unlockDelegate = coordinator - super.init(initialState: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait."), vaultConfig: vaultConfig, coordinator: coordinator) - self.authState = hubAccount.authState - Task { - await continueToAccessCheck() - } - } - - override func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) async { - let masterkey: Masterkey - do { - masterkey = try JWEHelper.decrypt(jwe: jwe, with: privateKey) - } catch { - await setError(to: error) - return - } - let xpc: XPC - do { - xpc = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) - defer { - fileProviderConnector.invalidateXPC(xpc) - } - try await xpc.proxy.unlockVault(rawKey: masterkey.rawKey).getValue() - unlockDelegate?.unlockedVault() - fileProviderConnector.invalidateXPC(xpc) - } catch { - await setError(to: error) - } - } -} - -extension Promise { - func getValue() async throws -> Value { - try await withCheckedThrowingContinuation({ continuation in - self.then(continuation.resume(returning:)).catch(continuation.resume(throwing:)) - }) - } -} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewController.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewController.swift deleted file mode 100644 index 9510d4b69..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewController.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// HubVaultViewController.swift -// Cryptomator -// -// Created by Philipp Schmid on 21.07.22. -// Copyright © 2022 Skymatic GmbH. All rights reserved. -// - -import Combine -import SwiftUI -import UIKit - -public class HubVaultViewController: UIHostingController { - let viewModel: HubVaultViewModel - private var subscriber: AnyCancellable? - private weak var coordinator: Coordinator? - - public init(viewModel: HubVaultViewModel) { - self.viewModel = viewModel - self.coordinator = viewModel.coordinator as? Coordinator - super.init(rootView: .init(viewModel: viewModel)) - } - - @available(*, unavailable) - @MainActor dynamic required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func viewDidLoad() { - super.viewDidLoad() - subscriber = viewModel.$error.receive(on: DispatchQueue.main).compactMap { $0 }.sink { [weak self] error in - if let self = self { - self.coordinator?.handleError(error, for: self) - } - } - } -} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift deleted file mode 100644 index b346ea7ed..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubVaultViewModel.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// File 2.swift -// -// -// Created by Philipp Schmid on 22.07.22. -// - -import AppAuthCore -import CryptoKit -import CryptomatorCloudAccessCore -import Foundation -import JOSESwift - -public enum AddHubVaultViewModelError: Error { - case missingHubConfig - case missingAuthState -} - -public enum AddHubVaultViewModelState { - case detectedVault - case receivedExistingKey - case accessNotGranted - case deviceRegisteredSuccessfully - case needsDeviceRegistration - case loading(text: String) -} - -open class HubVaultViewModel: ObservableObject { - public var authState: OIDAuthState? - @Published public var state: AddHubVaultViewModelState - @Published public var deviceName: String = "" - @Published public var error: Error? - public weak var coordinator: HubVaultCoordinator? - let vaultConfig: UnverifiedVaultConfig - let deviceRegisteringService: HubDeviceRegistering - let hubKeyService: HubKeyReceiving - - public init(initialState: AddHubVaultViewModelState, vaultConfig: UnverifiedVaultConfig, deviceRegisteringService: HubDeviceRegistering = CryptomatorHubAuthenticator.shared, hubKeyService: HubKeyReceiving = CryptomatorHubAuthenticator.shared, coordinator: HubVaultCoordinator? = nil) { - self.state = initialState - self.vaultConfig = vaultConfig - self.deviceRegisteringService = deviceRegisteringService - self.hubKeyService = hubKeyService - self.coordinator = coordinator - } - - public func register() async { - await setError(to: nil) - guard let hubConfig = vaultConfig.hub else { - error = AddHubVaultViewModelError.missingHubConfig - return - } - guard let authState = authState else { - error = AddHubVaultViewModelError.missingAuthState - return - } - - do { - try await deviceRegisteringService.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState) - } catch { - await setError(to: error) - return - } - await setState(to: .deviceRegisteredSuccessfully) - } - - public func continueToAccessCheck() async { - await setError(to: nil) - guard let authState = authState else { - await setError(to: AddHubVaultViewModelError.missingAuthState) - return - } - await setState(to: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait.")) - - let authFlow: HubAuthenticationFlow - do { - authFlow = try await hubKeyService.receiveKey(authState: authState, vaultConfig: vaultConfig) - } catch { - await setError(to: error) - return - } - switch authFlow { - case let .receivedExistingKey(data): - await receivedExistingKey(data: data) - case .accessNotGranted: - await setState(to: .accessNotGranted) - case .needsDeviceRegistration: - await setState(to: .needsDeviceRegistration) - } - } - - public func refresh() async { - await continueToAccessCheck() - } - - public func receivedExistingKey(data: Data) async { - let privateKey: P384.KeyAgreement.PrivateKey - let jwe: JWE - let hubAccount: HubAccount - do { - privateKey = try CryptomatorHubKeyProvider.shared.getPrivateKey() - jwe = try JWE(compactSerialization: data) - hubAccount = try HubAccount(authState: authState!) - try HubAccountManager.shared.saveHubAccount(hubAccount) - } catch { - await setError(to: error) - return - } - await receivedExistingKey(jwe: jwe, privateKey: privateKey, hubAccount: hubAccount) - } - - open func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey, hubAccount: HubAccount) async { - fatalError("Abstract method receivedExistingKey(jwe:privateKey:hubAccount:) not implemented") - } - - @MainActor - public func setState(to newState: AddHubVaultViewModelState) { - state = newState - } - - @MainActor - public func setError(to newError: Error?) { - error = newError - } -} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift new file mode 100644 index 000000000..c9c025afe --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift @@ -0,0 +1,72 @@ +import AppAuthCore +import CryptoKit +import CryptomatorCloudAccessCore +import CryptomatorCryptoLib +import JOSESwift +import SwiftUI +import UIKit + +public final class HubXPCLoginCoordinator: Coordinator { + public var childCoordinators = [Coordinator]() + public var navigationController: UINavigationController + let domain: NSFileProviderDomain + let vaultConfig: UnverifiedVaultConfig + let fileProviderConnector: FileProviderConnector + let hubAuthenticator: HubAuthenticating + public let onUnlocked: () -> Void + public let onErrorAlertDismissed: () -> Void + + public init(navigationController: UINavigationController, + domain: NSFileProviderDomain, + vaultConfig: UnverifiedVaultConfig, + fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared, + hubAuthenticator: HubAuthenticating, + onUnlocked: @escaping () -> Void, + onErrorAlertDismissed: @escaping () -> Void) { + self.navigationController = navigationController + self.domain = domain + self.vaultConfig = vaultConfig + self.fileProviderConnector = fileProviderConnector + self.hubAuthenticator = hubAuthenticator + self.onUnlocked = onUnlocked + self.onErrorAlertDismissed = onErrorAlertDismissed + } + + public func start() { + let viewModel = HubAuthenticationViewModel(vaultConfig: vaultConfig, + hubUserAuthenticator: self, + delegate: self) + let viewController = UIHostingController(rootView: HubAuthenticationView(viewModel: viewModel)) + navigationController.pushViewController(viewController, animated: true) + } +} + +extension HubXPCLoginCoordinator: HubAuthenticationFlowDelegate { + public func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async { + let masterkey: Masterkey + do { + masterkey = try JWEHelper.decrypt(jwe: jwe, with: privateKey) + } catch { + handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed) + return + } + let xpc: XPC + do { + xpc = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) + defer { + fileProviderConnector.invalidateXPC(xpc) + } + try await xpc.proxy.unlockVault(rawKey: masterkey.rawKey).getValue() + fileProviderConnector.invalidateXPC(xpc) + onUnlocked() + } catch { + handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed) + } + } +} + +extension HubXPCLoginCoordinator: HubUserLogin { + public func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState { + try await hubAuthenticator.authenticate(with: hubConfig, from: navigationController) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/ExistingHubVault.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/ExistingHubVault.swift new file mode 100644 index 000000000..6851589c3 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/ExistingHubVault.swift @@ -0,0 +1,20 @@ +import CryptoKit +import Foundation + +public struct ExistingHubVault { + let vaultUID: String + let delegateAccountUID: String + let jweData: Data + let privateKey: P384.KeyAgreement.PrivateKey + let vaultItem: VaultItem + let downloadedVaultConfig: DownloadedVaultConfig + + public init(vaultUID: String, delegateAccountUID: String, jweData: Data, privateKey: P384.KeyAgreement.PrivateKey, vaultItem: VaultItem, downloadedVaultConfig: DownloadedVaultConfig) { + self.vaultUID = vaultUID + self.delegateAccountUID = delegateAccountUID + self.jweData = jweData + self.privateKey = privateKey + self.vaultItem = vaultItem + self.downloadedVaultConfig = downloadedVaultConfig + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift index 70ed24fb5..814335dc8 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift @@ -23,6 +23,7 @@ public enum VaultManagerError: Error { case moveVaultInsideItself case invalidDecrypter case invalidPayloadMasterkey + case missingVaultConfigToken } public protocol VaultManager { @@ -35,9 +36,7 @@ public protocol VaultManager { func removeAllUnusedFileProviderDomains() -> Promise func moveVault(account: VaultAccount, to targetVaultPath: CloudPath) -> Promise func changePassphrase(oldPassphrase: String, newPassphrase: String, forVaultUID vaultUID: String) -> Promise - - // swiftlint:disable:next function_parameter_count - func addExistingHubVault(vaultUID: String, delegateAccountUID: String, hubUserID: String, jweData: Data, privateKey: P384.KeyAgreement.PrivateKey, vaultItem: VaultItem, downloadedVaultConfig: DownloadedVaultConfig) -> Promise + func addExistingHubVault(_ vault: ExistingHubVault) -> Promise func manualUnlockVault(withUID vaultUID: String, rawKey: [UInt8]) throws -> CloudProvider } @@ -47,8 +46,7 @@ public class VaultDBManager: VaultManager { vaultCache: VaultDBCache(dbWriter: CryptomatorDatabase.shared.dbPool), passwordManager: VaultPasswordKeychainManager(), masterkeyCacheManager: MasterkeyCacheKeychainManager.shared, - masterkeyCacheHelper: VaultKeepUnlockedManager.shared, - hubAccountManager: HubAccountManager.shared) + masterkeyCacheHelper: VaultKeepUnlockedManager.shared) let providerManager: CloudProviderDBManager let vaultAccountManager: VaultAccountManager private static let fakeVaultVersion = 999 @@ -56,22 +54,19 @@ public class VaultDBManager: VaultManager { private let passwordManager: VaultPasswordManager private let masterkeyCacheManager: MasterkeyCacheManager private let masterkeyCacheHelper: MasterkeyCacheHelper - private let hubAccountManager: HubAccountManager init(providerManager: CloudProviderDBManager, vaultAccountManager: VaultAccountManager, vaultCache: VaultCache, passwordManager: VaultPasswordManager, masterkeyCacheManager: MasterkeyCacheManager, - masterkeyCacheHelper: MasterkeyCacheHelper, - hubAccountManager: HubAccountManager) { + masterkeyCacheHelper: MasterkeyCacheHelper) { self.providerManager = providerManager self.vaultAccountManager = vaultAccountManager self.vaultCache = vaultCache self.passwordManager = passwordManager self.masterkeyCacheManager = masterkeyCacheManager self.masterkeyCacheHelper = masterkeyCacheHelper - self.hubAccountManager = hubAccountManager } // MARK: - Create New Vault @@ -299,21 +294,25 @@ public class VaultDBManager: VaultManager { } } - // swiftlint:disable:next function_parameter_count - public func addExistingHubVault(vaultUID: String, delegateAccountUID: String, hubUserID: String, jweData: Data, privateKey: P384.KeyAgreement.PrivateKey, vaultItem: VaultItem, downloadedVaultConfig: DownloadedVaultConfig) -> Promise { + public func addExistingHubVault(_ vault: ExistingHubVault) -> Promise { + let delegateAccountUID = vault.delegateAccountUID let provider: LocalizedCloudProviderDecorator do { provider = try LocalizedCloudProviderDecorator(delegate: providerManager.getProvider(with: delegateAccountUID)) } catch { return Promise(error) } + let vaultItem = vault.vaultItem + let downloadedVaultConfig = vault.downloadedVaultConfig + let jweData = vault.jweData + let vaultPath = vaultItem.vaultPath let vaultConfigMetadata = downloadedVaultConfig.metadata let vaultConfigToken = downloadedVaultConfig.token let masterkey: Masterkey do { let jwe = try JWE(compactSerialization: jweData) - masterkey = try JWEHelper.decrypt(jwe: jwe, with: privateKey) + masterkey = try JWEHelper.decrypt(jwe: jwe, with: vault.privateKey) } catch { return Promise(error) } @@ -322,6 +321,7 @@ public class VaultDBManager: VaultManager { } catch { return Promise(error) } + let vaultUID = vault.vaultUID let cachedVault = CachedVault(vaultUID: vaultUID, masterkeyFileData: jweData, vaultConfigToken: vaultConfigToken, @@ -332,7 +332,6 @@ public class VaultDBManager: VaultManager { let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: vaultItem.name) try self.vaultAccountManager.saveNewAccount(vaultAccount) do { - try self.hubAccountManager.linkVaultToHubAccount(vaultUID: vaultUID, hubUserID: hubUserID) try self.postProcessVaultCreation(cachedVault: cachedVault, password: nil) } catch { try self.vaultAccountManager.removeAccount(with: vaultUID) @@ -461,7 +460,7 @@ public class VaultDBManager: VaultManager { let cachedVault = try vaultCache.getCachedVault(withVaultUID: vaultUID) guard let vaultConfigToken = cachedVault.vaultConfigToken else { - fatalError("TODO: throw error") + throw VaultManagerError.missingVaultConfigToken } let unverifiedVaultConfig = try UnverifiedVaultConfig(token: vaultConfigToken) let vaultAccount = try vaultAccountManager.getAccount(with: vaultUID) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift index 95530914e..6239ccb10 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift @@ -5,221 +5,267 @@ // Created by Philipp Schmid on 27.10.21. // Copyright © 2021 Skymatic GmbH. All rights reserved. // -#warning("TODO: Remove comment") -/* - #if DEBUG - import CryptomatorCloudAccessCore - import CryptomatorCryptoLib - import Foundation - import Promises - - final class VaultManagerMock: VaultManager { - // MARK: - createNewVault - - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainThrowableError: Error? - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount = 0 - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCalled: Bool { - createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount > 0 - } - - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool)? - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool)] = [] - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReturnValue: Promise! - var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainClosure: ((String, String, CloudPath, String, Bool) -> Promise)? - - func createNewVault(withVaultUID vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { - if let error = createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainThrowableError { - return Promise(error) - } - createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount += 1 - createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, password: password, storePasswordInKeychain: storePasswordInKeychain) - createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, password: password, storePasswordInKeychain: storePasswordInKeychain)) - return createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultPath, password, storePasswordInKeychain) }) ?? createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReturnValue - } - - // MARK: - createFromExisting - - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError: Error? - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount = 0 - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCalled: Bool { - createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount > 0 - } - - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)? - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)] = [] - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue: Promise! - var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure: ((String, String, VaultItem, String, Bool) -> Promise)? - - func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { - if let error = createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError { - return Promise(error) - } - createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount += 1 - createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain) - createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain)) - return createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultItem, password, storePasswordInKeychain) }) ?? createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue - } - - // MARK: - createLegacyFromExisting - - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError: Error? - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount = 0 - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCalled: Bool { - createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount > 0 - } - - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)? - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)] = [] - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue: Promise! - var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure: ((String, String, VaultItem, String, Bool) -> Promise)? - - func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { - if let error = createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError { - return Promise(error) - } - createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount += 1 - createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain) - createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain)) - return createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultItem, password, storePasswordInKeychain) }) ?? createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue - } - - // MARK: - manualUnlockVault - - var manualUnlockVaultWithUIDKekThrowableError: Error? - var manualUnlockVaultWithUIDKekCallsCount = 0 - var manualUnlockVaultWithUIDKekCalled: Bool { - manualUnlockVaultWithUIDKekCallsCount > 0 - } - - var manualUnlockVaultWithUIDKekReceivedArguments: (vaultUID: String, kek: [UInt8])? - var manualUnlockVaultWithUIDKekReceivedInvocations: [(vaultUID: String, kek: [UInt8])] = [] - var manualUnlockVaultWithUIDKekReturnValue: CloudProvider! - var manualUnlockVaultWithUIDKekClosure: ((String, [UInt8]) throws -> CloudProvider)? - - func manualUnlockVault(withUID vaultUID: String, kek: [UInt8]) throws -> CloudProvider { - if let error = manualUnlockVaultWithUIDKekThrowableError { - throw error - } - manualUnlockVaultWithUIDKekCallsCount += 1 - manualUnlockVaultWithUIDKekReceivedArguments = (vaultUID: vaultUID, kek: kek) - manualUnlockVaultWithUIDKekReceivedInvocations.append((vaultUID: vaultUID, kek: kek)) - return try manualUnlockVaultWithUIDKekClosure.map({ try $0(vaultUID, kek) }) ?? manualUnlockVaultWithUIDKekReturnValue - } - - // MARK: - createVaultProvider - - var createVaultProviderWithUIDMasterkeyThrowableError: Error? - var createVaultProviderWithUIDMasterkeyCallsCount = 0 - var createVaultProviderWithUIDMasterkeyCalled: Bool { - createVaultProviderWithUIDMasterkeyCallsCount > 0 - } - - var createVaultProviderWithUIDMasterkeyReceivedArguments: (vaultUID: String, masterkey: Masterkey)? - var createVaultProviderWithUIDMasterkeyReceivedInvocations: [(vaultUID: String, masterkey: Masterkey)] = [] - var createVaultProviderWithUIDMasterkeyReturnValue: CloudProvider! - var createVaultProviderWithUIDMasterkeyClosure: ((String, Masterkey) throws -> CloudProvider)? - - func createVaultProvider(withUID vaultUID: String, masterkey: Masterkey) throws -> CloudProvider { - if let error = createVaultProviderWithUIDMasterkeyThrowableError { - throw error - } - createVaultProviderWithUIDMasterkeyCallsCount += 1 - createVaultProviderWithUIDMasterkeyReceivedArguments = (vaultUID: vaultUID, masterkey: masterkey) - createVaultProviderWithUIDMasterkeyReceivedInvocations.append((vaultUID: vaultUID, masterkey: masterkey)) - return try createVaultProviderWithUIDMasterkeyClosure.map({ try $0(vaultUID, masterkey) }) ?? createVaultProviderWithUIDMasterkeyReturnValue - } - - // MARK: - removeVault - - var removeVaultWithUIDThrowableError: Error? - var removeVaultWithUIDCallsCount = 0 - var removeVaultWithUIDCalled: Bool { - removeVaultWithUIDCallsCount > 0 - } - - var removeVaultWithUIDReceivedVaultUID: String? - var removeVaultWithUIDReceivedInvocations: [String] = [] - var removeVaultWithUIDReturnValue: Promise! - var removeVaultWithUIDClosure: ((String) throws -> Promise)? - - func removeVault(withUID vaultUID: String) throws -> Promise { - if let error = removeVaultWithUIDThrowableError { - throw error - } - if let error = removeVaultWithUIDThrowableError { - return Promise(error) - } - removeVaultWithUIDCallsCount += 1 - removeVaultWithUIDReceivedVaultUID = vaultUID - removeVaultWithUIDReceivedInvocations.append(vaultUID) - return try removeVaultWithUIDClosure.map({ try $0(vaultUID) }) ?? removeVaultWithUIDReturnValue - } - - // MARK: - removeAllUnusedFileProviderDomains - - var removeAllUnusedFileProviderDomainsThrowableError: Error? - var removeAllUnusedFileProviderDomainsCallsCount = 0 - var removeAllUnusedFileProviderDomainsCalled: Bool { - removeAllUnusedFileProviderDomainsCallsCount > 0 - } - - var removeAllUnusedFileProviderDomainsReturnValue: Promise! - var removeAllUnusedFileProviderDomainsClosure: (() -> Promise)? - - func removeAllUnusedFileProviderDomains() -> Promise { - if let error = removeAllUnusedFileProviderDomainsThrowableError { - return Promise(error) - } - removeAllUnusedFileProviderDomainsCallsCount += 1 - return removeAllUnusedFileProviderDomainsClosure.map({ $0() }) ?? removeAllUnusedFileProviderDomainsReturnValue - } - - // MARK: - moveVault - - var moveVaultAccountToThrowableError: Error? - var moveVaultAccountToCallsCount = 0 - var moveVaultAccountToCalled: Bool { - moveVaultAccountToCallsCount > 0 - } - - var moveVaultAccountToReceivedArguments: (account: VaultAccount, targetVaultPath: CloudPath)? - var moveVaultAccountToReceivedInvocations: [(account: VaultAccount, targetVaultPath: CloudPath)] = [] - var moveVaultAccountToReturnValue: Promise! - var moveVaultAccountToClosure: ((VaultAccount, CloudPath) -> Promise)? - - func moveVault(account: VaultAccount, to targetVaultPath: CloudPath) -> Promise { - if let error = moveVaultAccountToThrowableError { - return Promise(error) - } - moveVaultAccountToCallsCount += 1 - moveVaultAccountToReceivedArguments = (account: account, targetVaultPath: targetVaultPath) - moveVaultAccountToReceivedInvocations.append((account: account, targetVaultPath: targetVaultPath)) - return moveVaultAccountToClosure.map({ $0(account, targetVaultPath) }) ?? moveVaultAccountToReturnValue - } - - // MARK: - changePassphrase - - var changePassphraseOldPassphraseNewPassphraseForVaultUIDThrowableError: Error? - var changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount = 0 - var changePassphraseOldPassphraseNewPassphraseForVaultUIDCalled: Bool { - changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount > 0 - } - - var changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedArguments: (oldPassphrase: String, newPassphrase: String, vaultUID: String)? - var changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedInvocations: [(oldPassphrase: String, newPassphrase: String, vaultUID: String)] = [] - var changePassphraseOldPassphraseNewPassphraseForVaultUIDReturnValue: Promise! - var changePassphraseOldPassphraseNewPassphraseForVaultUIDClosure: ((String, String, String) -> Promise)? - - func changePassphrase(oldPassphrase: String, newPassphrase: String, forVaultUID vaultUID: String) -> Promise { - if let error = changePassphraseOldPassphraseNewPassphraseForVaultUIDThrowableError { - return Promise(error) - } - changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount += 1 - changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedArguments = (oldPassphrase: oldPassphrase, newPassphrase: newPassphrase, vaultUID: vaultUID) - changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedInvocations.append((oldPassphrase: oldPassphrase, newPassphrase: newPassphrase, vaultUID: vaultUID)) - return changePassphraseOldPassphraseNewPassphraseForVaultUIDClosure.map({ $0(oldPassphrase, newPassphrase, vaultUID) }) ?? changePassphraseOldPassphraseNewPassphraseForVaultUIDReturnValue - } - } - - #endif - */ + +#if DEBUG +import CryptomatorCloudAccessCore +import CryptomatorCryptoLib +import Foundation +import Promises + +// swiftlint: disable all + +final class VaultManagerMock: VaultManager { + // MARK: - createNewVault + + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainThrowableError: Error? + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount = 0 + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCalled: Bool { + createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount > 0 + } + + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool)? + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool)] = [] + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReturnValue: Promise! + var createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainClosure: ((String, String, CloudPath, String, Bool) -> Promise)? + + func createNewVault(withVaultUID vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { + if let error = createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainThrowableError { + return Promise(error) + } + createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainCallsCount += 1 + createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, password: password, storePasswordInKeychain: storePasswordInKeychain) + createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, password: password, storePasswordInKeychain: storePasswordInKeychain)) + return createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultPath, password, storePasswordInKeychain) }) ?? createNewVaultWithVaultUIDDelegateAccountUIDVaultPathPasswordStorePasswordInKeychainReturnValue + } + + // MARK: - createFromExisting + + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError: Error? + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount = 0 + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCalled: Bool { + createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount > 0 + } + + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)? + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)] = [] + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue: Promise! + var createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure: ((String, String, VaultItem, String, Bool) -> Promise)? + + func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { + if let error = createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError { + return Promise(error) + } + createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount += 1 + createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain) + createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain)) + return createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultItem, password, storePasswordInKeychain) }) ?? createFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue + } + + // MARK: - createLegacyFromExisting + + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError: Error? + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount = 0 + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCalled: Bool { + createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount > 0 + } + + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments: (vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)? + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations: [(vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool)] = [] + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue: Promise! + var createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure: ((String, String, VaultItem, String, Bool) -> Promise)? + + func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { + if let error = createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainThrowableError { + return Promise(error) + } + createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainCallsCount += 1 + createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedArguments = (vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain) + createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReceivedInvocations.append((vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultItem: vaultItem, password: password, storePasswordInKeychain: storePasswordInKeychain)) + return createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainClosure.map({ $0(vaultUID, delegateAccountUID, vaultItem, password, storePasswordInKeychain) }) ?? createLegacyFromExistingWithVaultUIDDelegateAccountUIDVaultItemPasswordStorePasswordInKeychainReturnValue + } + + // MARK: - manualUnlockVault + + var manualUnlockVaultWithUIDKekThrowableError: Error? + var manualUnlockVaultWithUIDKekCallsCount = 0 + var manualUnlockVaultWithUIDKekCalled: Bool { + manualUnlockVaultWithUIDKekCallsCount > 0 + } + + var manualUnlockVaultWithUIDKekReceivedArguments: (vaultUID: String, kek: [UInt8])? + var manualUnlockVaultWithUIDKekReceivedInvocations: [(vaultUID: String, kek: [UInt8])] = [] + var manualUnlockVaultWithUIDKekReturnValue: CloudProvider! + var manualUnlockVaultWithUIDKekClosure: ((String, [UInt8]) throws -> CloudProvider)? + + func manualUnlockVault(withUID vaultUID: String, kek: [UInt8]) throws -> CloudProvider { + if let error = manualUnlockVaultWithUIDKekThrowableError { + throw error + } + manualUnlockVaultWithUIDKekCallsCount += 1 + manualUnlockVaultWithUIDKekReceivedArguments = (vaultUID: vaultUID, kek: kek) + manualUnlockVaultWithUIDKekReceivedInvocations.append((vaultUID: vaultUID, kek: kek)) + return try manualUnlockVaultWithUIDKekClosure.map({ try $0(vaultUID, kek) }) ?? manualUnlockVaultWithUIDKekReturnValue + } + + // MARK: - createVaultProvider + + var createVaultProviderWithUIDMasterkeyThrowableError: Error? + var createVaultProviderWithUIDMasterkeyCallsCount = 0 + var createVaultProviderWithUIDMasterkeyCalled: Bool { + createVaultProviderWithUIDMasterkeyCallsCount > 0 + } + + var createVaultProviderWithUIDMasterkeyReceivedArguments: (vaultUID: String, masterkey: Masterkey)? + var createVaultProviderWithUIDMasterkeyReceivedInvocations: [(vaultUID: String, masterkey: Masterkey)] = [] + var createVaultProviderWithUIDMasterkeyReturnValue: CloudProvider! + var createVaultProviderWithUIDMasterkeyClosure: ((String, Masterkey) throws -> CloudProvider)? + + func createVaultProvider(withUID vaultUID: String, masterkey: Masterkey) throws -> CloudProvider { + if let error = createVaultProviderWithUIDMasterkeyThrowableError { + throw error + } + createVaultProviderWithUIDMasterkeyCallsCount += 1 + createVaultProviderWithUIDMasterkeyReceivedArguments = (vaultUID: vaultUID, masterkey: masterkey) + createVaultProviderWithUIDMasterkeyReceivedInvocations.append((vaultUID: vaultUID, masterkey: masterkey)) + return try createVaultProviderWithUIDMasterkeyClosure.map({ try $0(vaultUID, masterkey) }) ?? createVaultProviderWithUIDMasterkeyReturnValue + } + + // MARK: - removeVault + + var removeVaultWithUIDThrowableError: Error? + var removeVaultWithUIDCallsCount = 0 + var removeVaultWithUIDCalled: Bool { + removeVaultWithUIDCallsCount > 0 + } + + var removeVaultWithUIDReceivedVaultUID: String? + var removeVaultWithUIDReceivedInvocations: [String] = [] + var removeVaultWithUIDReturnValue: Promise! + var removeVaultWithUIDClosure: ((String) throws -> Promise)? + + func removeVault(withUID vaultUID: String) throws -> Promise { + if let error = removeVaultWithUIDThrowableError { + throw error + } + if let error = removeVaultWithUIDThrowableError { + return Promise(error) + } + removeVaultWithUIDCallsCount += 1 + removeVaultWithUIDReceivedVaultUID = vaultUID + removeVaultWithUIDReceivedInvocations.append(vaultUID) + return try removeVaultWithUIDClosure.map({ try $0(vaultUID) }) ?? removeVaultWithUIDReturnValue + } + + // MARK: - removeAllUnusedFileProviderDomains + + var removeAllUnusedFileProviderDomainsThrowableError: Error? + var removeAllUnusedFileProviderDomainsCallsCount = 0 + var removeAllUnusedFileProviderDomainsCalled: Bool { + removeAllUnusedFileProviderDomainsCallsCount > 0 + } + + var removeAllUnusedFileProviderDomainsReturnValue: Promise! + var removeAllUnusedFileProviderDomainsClosure: (() -> Promise)? + + func removeAllUnusedFileProviderDomains() -> Promise { + if let error = removeAllUnusedFileProviderDomainsThrowableError { + return Promise(error) + } + removeAllUnusedFileProviderDomainsCallsCount += 1 + return removeAllUnusedFileProviderDomainsClosure.map({ $0() }) ?? removeAllUnusedFileProviderDomainsReturnValue + } + + // MARK: - moveVault + + var moveVaultAccountToThrowableError: Error? + var moveVaultAccountToCallsCount = 0 + var moveVaultAccountToCalled: Bool { + moveVaultAccountToCallsCount > 0 + } + + var moveVaultAccountToReceivedArguments: (account: VaultAccount, targetVaultPath: CloudPath)? + var moveVaultAccountToReceivedInvocations: [(account: VaultAccount, targetVaultPath: CloudPath)] = [] + var moveVaultAccountToReturnValue: Promise! + var moveVaultAccountToClosure: ((VaultAccount, CloudPath) -> Promise)? + + func moveVault(account: VaultAccount, to targetVaultPath: CloudPath) -> Promise { + if let error = moveVaultAccountToThrowableError { + return Promise(error) + } + moveVaultAccountToCallsCount += 1 + moveVaultAccountToReceivedArguments = (account: account, targetVaultPath: targetVaultPath) + moveVaultAccountToReceivedInvocations.append((account: account, targetVaultPath: targetVaultPath)) + return moveVaultAccountToClosure.map({ $0(account, targetVaultPath) }) ?? moveVaultAccountToReturnValue + } + + // MARK: - changePassphrase + + var changePassphraseOldPassphraseNewPassphraseForVaultUIDThrowableError: Error? + var changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount = 0 + var changePassphraseOldPassphraseNewPassphraseForVaultUIDCalled: Bool { + changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount > 0 + } + + var changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedArguments: (oldPassphrase: String, newPassphrase: String, vaultUID: String)? + var changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedInvocations: [(oldPassphrase: String, newPassphrase: String, vaultUID: String)] = [] + var changePassphraseOldPassphraseNewPassphraseForVaultUIDReturnValue: Promise! + var changePassphraseOldPassphraseNewPassphraseForVaultUIDClosure: ((String, String, String) -> Promise)? + + func changePassphrase(oldPassphrase: String, newPassphrase: String, forVaultUID vaultUID: String) -> Promise { + if let error = changePassphraseOldPassphraseNewPassphraseForVaultUIDThrowableError { + return Promise(error) + } + changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount += 1 + changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedArguments = (oldPassphrase: oldPassphrase, newPassphrase: newPassphrase, vaultUID: vaultUID) + changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedInvocations.append((oldPassphrase: oldPassphrase, newPassphrase: newPassphrase, vaultUID: vaultUID)) + return changePassphraseOldPassphraseNewPassphraseForVaultUIDClosure.map({ $0(oldPassphrase, newPassphrase, vaultUID) }) ?? changePassphraseOldPassphraseNewPassphraseForVaultUIDReturnValue + } + + // MARK: - addExistingHubVault + + var addExistingHubVaultThrowableError: Error? + var addExistingHubVaultCallsCount = 0 + var addExistingHubVaultCalled: Bool { + addExistingHubVaultCallsCount > 0 + } + + var addExistingHubVaultReceivedVault: ExistingHubVault? + var addExistingHubVaultReceivedInvocations: [ExistingHubVault] = [] + var addExistingHubVaultReturnValue: Promise! + var addExistingHubVaultClosure: ((ExistingHubVault) -> Promise)? + + func addExistingHubVault(_ vault: ExistingHubVault) -> Promise { + if let error = addExistingHubVaultThrowableError { + return Promise(error) + } + addExistingHubVaultCallsCount += 1 + addExistingHubVaultReceivedVault = vault + addExistingHubVaultReceivedInvocations.append(vault) + return addExistingHubVaultClosure.map({ $0(vault) }) ?? addExistingHubVaultReturnValue + } + + // MARK: - manualUnlockVault + + var manualUnlockVaultWithUIDRawKeyThrowableError: Error? + var manualUnlockVaultWithUIDRawKeyCallsCount = 0 + var manualUnlockVaultWithUIDRawKeyCalled: Bool { + manualUnlockVaultWithUIDRawKeyCallsCount > 0 + } + + var manualUnlockVaultWithUIDRawKeyReceivedArguments: (vaultUID: String, rawKey: [UInt8])? + var manualUnlockVaultWithUIDRawKeyReceivedInvocations: [(vaultUID: String, rawKey: [UInt8])] = [] + var manualUnlockVaultWithUIDRawKeyReturnValue: CloudProvider! + var manualUnlockVaultWithUIDRawKeyClosure: ((String, [UInt8]) throws -> CloudProvider)? + + func manualUnlockVault(withUID vaultUID: String, rawKey: [UInt8]) throws -> CloudProvider { + if let error = manualUnlockVaultWithUIDRawKeyThrowableError { + throw error + } + manualUnlockVaultWithUIDRawKeyCallsCount += 1 + manualUnlockVaultWithUIDRawKeyReceivedArguments = (vaultUID: vaultUID, rawKey: rawKey) + manualUnlockVaultWithUIDRawKeyReceivedInvocations.append((vaultUID: vaultUID, rawKey: rawKey)) + return try manualUnlockVaultWithUIDRawKeyClosure.map({ try $0(vaultUID, rawKey) }) ?? manualUnlockVaultWithUIDRawKeyReturnValue + } +} + +#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Promise+StructuredConcurrency.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Promise+StructuredConcurrency.swift new file mode 100644 index 000000000..6b8d02d4d --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Promise+StructuredConcurrency.swift @@ -0,0 +1,9 @@ +import Promises + +public extension Promise { + func getValue() async throws -> Value { + try await withCheckedThrowingContinuation({ continuation in + self.then(continuation.resume(returning:)).catch(continuation.resume(throwing:)) + }) + } +} diff --git a/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift b/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift index 9b92fa039..72ca2b8ab 100644 --- a/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift +++ b/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift @@ -186,6 +186,14 @@ private class PasswordVaultManagerMock: VaultManager { func createVaultProvider(withUID vaultUID: String, masterkey: Masterkey) throws -> CloudProvider { throw MockError.notMocked } + + func addExistingHubVault(_ vault: ExistingHubVault) -> Promise { + return Promise(MockError.notMocked) + } + + func manualUnlockVault(withUID vaultUID: String, rawKey: [UInt8]) throws -> CloudProvider { + throw MockError.notMocked + } } private struct CreatedVault { diff --git a/FileProviderExtensionUI/FileProviderCoordinator.swift b/FileProviderExtensionUI/FileProviderCoordinator.swift index b3df5176d..d0d47244a 100644 --- a/FileProviderExtensionUI/FileProviderCoordinator.swift +++ b/FileProviderExtensionUI/FileProviderCoordinator.swift @@ -8,6 +8,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore +import CryptomatorCommon import CryptomatorCommonCore import CryptomatorFileProvider import FileProviderUI @@ -159,22 +160,14 @@ class FileProviderCoordinator: Coordinator { } func showHubLoginScreen(vaultConfig: UnverifiedVaultConfig, domain: NSFileProviderDomain) { - let hubAccount: HubAccount - do { - guard let retrievedHubAccount = try HubAccountManager.shared.getHubAccount(forVaultUID: domain.identifier.rawValue) else { - fatalError("TODO: add error") - } - hubAccount = retrievedHubAccount - } catch { - handleError(error) - return - } - let child = CryptomatorHubVaultUnlockCoordinator(navigationController: navigationController, - domain: domain, - hubAccount: hubAccount, - vaultConfig: vaultConfig) - child.parentCoordinator = self - child.delegate = self + let child = HubXPCLoginCoordinator(navigationController: navigationController, + domain: domain, + vaultConfig: vaultConfig, + hubAuthenticator: CryptomatorHubAuthenticator.shared, + onUnlocked: { [weak self] in self?.done() }, + onErrorAlertDismissed: { [weak self] in self?.done() }) +// child.parentCoordinator = self +// child.delegate = self childCoordinators.append(child) child.start() } @@ -203,9 +196,3 @@ class FileProviderCoordinator: Coordinator { handleError(error, for: hostViewController) } } - -extension FileProviderCoordinator: HubVaultUnlockDelegate { - func unlockedVault() { - done() - } -} From e74f5d5f2c37a04dbc0f33d6fd6e6deff3727158 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 18 Apr 2023 23:33:10 +0200 Subject: [PATCH 14/45] Remove unused class --- .../Manager/HubAccountManager.swift | 155 ------------------ 1 file changed, 155 deletions(-) delete mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/HubAccountManager.swift diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/HubAccountManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/HubAccountManager.swift deleted file mode 100644 index c05c84c6b..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/HubAccountManager.swift +++ /dev/null @@ -1,155 +0,0 @@ -// -// HubAccountManager.swift -// -// -// Created by Philipp Schmid on 22.07.22. -// - -import AppAuthCore -import Foundation -import GRDB - -struct HubAccountInfo: Codable { - let userID: String -} - -extension HubAccountInfo: FetchableRecord, MutablePersistableRecord { - enum Columns: String, ColumnExpression { - case userID - } -} - -public struct HubAccount { - public let userID: String - public let authState: OIDAuthState -} - -extension HubAccount { - init(info: HubAccountInfo, authState: OIDAuthState) { - self.userID = info.userID - self.authState = authState - } -} - -extension HubAccount { - private static let keycloakUserIDKey = "sub" - - public init(authState: OIDAuthState) throws { -// guard let idToken = authState.lastTokenResponse?.idToken ?? authState.lastAuthorizationResponse.idToken else { -// throw HubAccountError.missingIDToken -// } -// guard let claims = OIDIDToken(idTokenString: idToken)?.claims else { -// throw HubAccountError.missingClaims -// } -// guard let userID = claims[HubAccount.keycloakUserIDKey] as? String else { -// throw HubAccountError.missingUserID -// } - let userID = "DemoUser-ID" - self.init(userID: userID, authState: authState) - } -} - -enum HubAccountError: Error { - case missingIDToken - case missingClaims - case missingUserID -} - -struct HubVaultAccount: Codable { - var id: Int64? - let vaultUID: String - let hubUserID: String -} - -extension HubVaultAccount: FetchableRecord, MutablePersistableRecord { - enum Columns: String, ColumnExpression { - case id, vaultUID, hubUserID - } -} - -public struct HubAccountManager { - let dbWriter: DatabaseWriter - let keychain: CryptomatorKeychainType - public static let shared = HubAccountManager(dbWriter: CryptomatorDatabase.shared.dbPool, keychain: CryptomatorKeychain.hub) - - public func getHubAccount(withUserID userID: String) throws -> HubAccount? { - guard let accountInfo = try getHubAccountInfo(withUserID: userID) else { - return nil - } - return getHubAccount(accountInfo: accountInfo) - } - - public func getHubAccount(forVaultUID vaultUID: String) throws -> HubAccount? { - try dbWriter.read { db in - guard let hubVaultAccount = try HubVaultAccount.fetchOne(db, key: [HubVaultAccount.Columns.vaultUID.name: vaultUID]) else { - return nil - } - guard let accountInfo = try HubAccountInfo.fetchOne(db, key: hubVaultAccount.hubUserID) else { - return nil - } - return getHubAccount(accountInfo: accountInfo) - } - } - - public func saveHubAccount(_ hubAccount: HubAccount) throws { - var accountInfo = HubAccountInfo(userID: hubAccount.userID) - try dbWriter.write { db in - try accountInfo.save(db) - try keychain.saveAuthState(hubAccount.authState, for: accountInfo.userID) - } - } - - public func removeHubAccount(withUserID userID: String) throws { - try dbWriter.write { db in - try HubAccountInfo.deleteOne(db, key: [HubAccountInfo.Columns.userID.name: userID]) - try keychain.delete(userID) - } - } - - public func linkVaultToHubAccount(vaultUID: String, hubUserID: String) throws { - let request = HubAccountInfo.filter(HubAccountInfo.Columns.userID == hubUserID) - try dbWriter.write { db in - guard let accountInfo = try HubAccountInfo.fetchOne(db, request) else { - throw HubAccountManagerError.unknownHubUserID - } - guard let vaultAccount = try VaultAccount.fetchOne(db, key: [VaultAccount.vaultUIDKey: vaultUID]) else { - throw HubAccountManagerError.unknownVaultUID - } - var hubVaultAccount = HubVaultAccount(vaultUID: vaultAccount.vaultUID, hubUserID: accountInfo.userID) - try hubVaultAccount.save(db) - } - } - - private func getHubAccountInfo(withUserID userID: String) throws -> HubAccountInfo? { - let request = HubAccountInfo.filter(HubAccountInfo.Columns.userID == userID) - return try dbWriter.read { db in - try HubAccountInfo.fetchOne(db, request) - } - } - - private func getHubAccount(accountInfo: HubAccountInfo) -> HubAccount? { - guard let authState = keychain.getAuthState(accountInfo.userID) else { - return nil - } - return HubAccount(info: accountInfo, authState: authState) - } -} - -enum HubAccountManagerError: Error { - case unknownHubUserID - case unknownVaultUID -} - -private extension CryptomatorKeychainType { - func getAuthState(_ identifier: String) -> OIDAuthState? { - guard let data = getAsData(identifier) else { - return nil - } - return try? NSKeyedUnarchiver.unarchivedObject(ofClass: OIDAuthState.self, from: data) - } - - func saveAuthState(_ authState: OIDAuthState, for identifier: String) throws { - let archivedAuthState = try NSKeyedArchiver.archivedData(withRootObject: authState, requiringSecureCoding: true) - try set(identifier, value: archivedAuthState) - } -} From f340f3f094aa206510698b678f1c5bf20fd4c601 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 18 Apr 2023 23:34:11 +0200 Subject: [PATCH 15/45] Remove comment --- FileProviderExtensionUI/FileProviderCoordinator.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/FileProviderExtensionUI/FileProviderCoordinator.swift b/FileProviderExtensionUI/FileProviderCoordinator.swift index d0d47244a..a28cf0a2f 100644 --- a/FileProviderExtensionUI/FileProviderCoordinator.swift +++ b/FileProviderExtensionUI/FileProviderCoordinator.swift @@ -166,8 +166,6 @@ class FileProviderCoordinator: Coordinator { hubAuthenticator: CryptomatorHubAuthenticator.shared, onUnlocked: { [weak self] in self?.done() }, onErrorAlertDismissed: { [weak self] in self?.done() }) -// child.parentCoordinator = self -// child.delegate = self childCoordinators.append(child) child.start() } From 6c816e12a6e1e5b36575428469a65ab3e17b0b56 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 18 Apr 2023 23:37:01 +0200 Subject: [PATCH 16/45] Fix SwiftLint errors --- .../Mocks/FileProviderConnectorMock.swift | 3 +++ .../Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift | 1 + 2 files changed, 4 insertions(+) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FileProviderConnectorMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FileProviderConnectorMock.swift index 5457463e3..b1d8152a5 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FileProviderConnectorMock.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FileProviderConnectorMock.swift @@ -64,4 +64,7 @@ final class FileProviderConnectorMock: FileProviderConnector { return Promise(xpc ?? getXPCServiceNameDomainIdentifierReturnValue as! XPC) } } + +// swiftlint:enable all + #endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift index 6239ccb10..e6b2ef67c 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/VaultManagerMock.swift @@ -268,4 +268,5 @@ final class VaultManagerMock: VaultManager { } } +// swiftlint: enable all #endif From 05517b1b6203b47fa379545d8033dea461dc7c7e Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sun, 21 May 2023 23:31:44 +0200 Subject: [PATCH 17/45] Design improvements --- Cryptomator.xcodeproj/project.pbxproj | 21 - .../xcshareddata/swiftpm/Package.resolved | 386 +++++++++--------- .../AddVault/Hub/HubAddVaultCoordinator.swift | 17 +- Cryptomator/S3/S3AuthenticationView.swift | 4 +- Cryptomator/WebDAV/WebDAVAuthentication.swift | 4 +- CryptomatorCommon/Package.swift | 6 +- .../CryptomatorSimpleButtonView.swift | 37 ++ .../Hub/HubAuthenticationView.swift | 72 ++-- .../Hub/HubAuthenticationViewController.swift | 63 +++ .../Hub/HubAuthenticationViewModel.swift | 12 +- .../HubDeviceRegisteredSuccessfullyView.swift | 19 + .../Hub/HubDeviceRegistrationView.swift | 41 ++ .../Hub/HubLoginView.swift | 9 +- .../Hub/HubXPCLoginCoordinator.swift | 2 +- .../SwiftUI}/SwiftUI+Focus.swift | 0 .../SwiftUI/SwiftUI+ListBackground.swift | 25 ++ .../UIColor+CryptomatorColors.swift | 7 + 17 files changed, 443 insertions(+), 282 deletions(-) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSimpleButtonView.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewController.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift rename {Cryptomator/Common => CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI}/SwiftUI+Focus.swift (100%) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+ListBackground.swift diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 609cc260b..284c3dd59 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -326,8 +326,6 @@ 4AEBE8C22653FAD40031487F /* WorkflowMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEBE8C12653FAD40031487F /* WorkflowMiddleware.swift */; }; 4AED9A69286B303000352951 /* S3Authenticator+VC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED9A68286B303000352951 /* S3Authenticator+VC.swift */; }; 4AED9A6C286B305200352951 /* S3AuthenticationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED9A6B286B305200352951 /* S3AuthenticationView.swift */; }; - 4AED9A6F286B38DA00352951 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 4AED9A6E286B38DA00352951 /* Introspect */; }; - 4AED9A73286B3D6D00352951 /* SwiftUI+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED9A72286B3D6C00352951 /* SwiftUI+Focus.swift */; }; 4AED9A77286B4BEE00352951 /* S3AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED9A76286B4BEE00352951 /* S3AuthenticationViewController.swift */; }; 4AED9A79286B4DF500352951 /* S3Authenticating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED9A78286B4DF500352951 /* S3Authenticating.swift */; }; 4AEE22F82861D6DC00A9C785 /* OpenVaultIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEE22F72861D6DC00A9C785 /* OpenVaultIntentHandler.swift */; }; @@ -867,7 +865,6 @@ 4AEBE8C12653FAD40031487F /* WorkflowMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkflowMiddleware.swift; sourceTree = ""; }; 4AED9A68286B303000352951 /* S3Authenticator+VC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "S3Authenticator+VC.swift"; sourceTree = ""; }; 4AED9A6B286B305200352951 /* S3AuthenticationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = S3AuthenticationView.swift; sourceTree = ""; }; - 4AED9A72286B3D6C00352951 /* SwiftUI+Focus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+Focus.swift"; sourceTree = ""; }; 4AED9A76286B4BEE00352951 /* S3AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = S3AuthenticationViewController.swift; sourceTree = ""; }; 4AED9A78286B4DF500352951 /* S3Authenticating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = S3Authenticating.swift; sourceTree = ""; }; 4AEE22F72861D6DC00A9C785 /* OpenVaultIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenVaultIntentHandler.swift; sourceTree = ""; }; @@ -1082,7 +1079,6 @@ buildActionMask = 2147483647; files = ( 4A9172822619F17C003C4043 /* CryptomatorCommon in Frameworks */, - 4AED9A6F286B38DA00352951 /* Introspect in Frameworks */, 4A1521E427C55EA2006C96B2 /* TPInAppReceipt in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1506,7 +1502,6 @@ 4A4246F727565D87005BE82D /* PoppingCloseCoordinator.swift */, 4A447E0325BF0B0F00D9520D /* SingleSectionTableViewController.swift */, 4A61F6B8274582E3007AA422 /* StaticUITableViewController.swift */, - 4AED9A72286B3D6C00352951 /* SwiftUI+Focus.swift */, 4A61F6B62745353E007AA422 /* TableViewModel.swift */, 4AF91CCF25A71C5800ACF01E /* UIImage+CloudProviderType.swift */, 4AC8626F273598CC00E15BA5 /* UIViewController+ProgressHUDError.swift */, @@ -2197,7 +2192,6 @@ packageProductDependencies = ( 4A9172812619F17C003C4043 /* CryptomatorCommon */, 4A1521E327C55EA2006C96B2 /* TPInAppReceipt */, - 4AED9A6E286B38DA00352951 /* Introspect */, ); productName = Cryptomator; productReference = 4AE97DA824572E4900452814 /* Cryptomator.app */; @@ -2327,7 +2321,6 @@ mainGroup = 4A5E5B202453119100BD6298; packageReferences = ( 4A1521E227C55EA2006C96B2 /* XCRemoteSwiftPackageReference "TPInAppReceipt" */, - 4AED9A6D286B38D900352951 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, ); productRefGroup = 4A5E5B2A2453119100BD6298 /* Products */; projectDirPath = ""; @@ -2789,7 +2782,6 @@ 4A587FA828B55CD600C69A1E /* WebDAVCredentialCoordinator.swift in Sources */, 4A53CC11267CBFA100853BB3 /* AddVaultSuccessCoordinator.swift in Sources */, 4A6CF80027428CCB0061380A /* VaultCellViewModel.swift in Sources */, - 4AED9A73286B3D6D00352951 /* SwiftUI+Focus.swift in Sources */, 4A8D05D625C5CBE10082C5F7 /* AddVaultSuccessViewController.swift in Sources */, 4A0C07EB25AC832900B83211 /* VaultListPosition.swift in Sources */, 4A3D658226838991000DA764 /* OpenExistingLocalVaultViewModel.swift in Sources */, @@ -3694,14 +3686,6 @@ minimumVersion = 3.3.0; }; }; - 4AED9A6D286B38D900352951 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/siteline/SwiftUI-Introspect.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.1.4; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3726,11 +3710,6 @@ isa = XCSwiftPackageProductDependency; productName = AppAuth; }; - 4AED9A6E286B38DA00352951 /* Introspect */ = { - isa = XCSwiftPackageProductDependency; - package = 4AED9A6D286B38D900352951 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; - productName = Introspect; - }; 4AF9D44829C262B800EB3822 /* CryptomatorCommon */ = { isa = XCSwiftPackageProductDependency; productName = CryptomatorCommon; diff --git a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 12efb43eb..a42dc6a6f 100644 --- a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,196 +1,194 @@ { - "object": { - "pins": [ - { - "package": "AppAuth", - "repositoryURL": "https://github.com/openid/AppAuth-iOS.git", - "state": { - "branch": null, - "revision": "3d36a58a2b736f7bc499453e996a704929b25080", - "version": "1.6.0" - } - }, - { - "package": "ASN1Swift", - "repositoryURL": "https://github.com/tikhop/ASN1Swift", - "state": { - "branch": null, - "revision": "b53bee03a942623db25afc5bfb80227b2cb3b425", - "version": "1.2.4" - } - }, - { - "package": "AWSiOSSDKV2", - "repositoryURL": "https://github.com/aws-amplify/aws-sdk-ios-spm.git", - "state": { - "branch": null, - "revision": "51d99d74be7249ac6444581bd1e394fb60ea86a3", - "version": "2.30.4" - } - }, - { - "package": "Base32", - "repositoryURL": "https://github.com/norio-nomura/Base32.git", - "state": { - "branch": null, - "revision": "c4bc0a49689999ae2c7c778f3830a6a6e694efb8", - "version": "0.9.0" - } - }, - { - "package": "CryptomatorCloudAccess", - "repositoryURL": "https://github.com/cryptomator/cloud-access-swift.git", - "state": { - "branch": "feature/hub-poc", - "revision": "302afc0f5960fac7dd67cc3c09e52d21ba45f829", - "version": null - } - }, - { - "package": "CocoaLumberjack", - "repositoryURL": "https://github.com/CocoaLumberjack/CocoaLumberjack.git", - "state": { - "branch": null, - "revision": "0188d31089b5881a269e01777be74c7316924346", - "version": "3.8.0" - } - }, - { - "package": "CryptomatorCryptoLib", - "repositoryURL": "https://github.com/cryptomator/cryptolib-swift.git", - "state": { - "branch": null, - "revision": "6e5dbea6e05742ad82a074bf7ee8c3305d92fbae", - "version": "1.1.0" - } - }, - { - "package": "ObjectiveDropboxOfficial", - "repositoryURL": "https://github.com/phil1995/dropbox-sdk-obj-c-spm.git", - "state": { - "branch": null, - "revision": "f0eafe25d26c52377c4a1c08f1dbd77320164994", - "version": "7.0.0" - } - }, - { - "package": "GoogleAPIClientForREST", - "repositoryURL": "https://github.com/google/google-api-objectivec-client-for-rest.git", - "state": { - "branch": null, - "revision": "260501c0425e95e038c65436436161266bf548e9", - "version": "3.0.0" - } - }, - { - "package": "GRDB", - "repositoryURL": "https://github.com/groue/GRDB.swift.git", - "state": { - "branch": null, - "revision": "dd7e7f39e8e4d7a22d258d9809a882f914690b01", - "version": "5.26.1" - } - }, - { - "package": "GTMSessionFetcher", - "repositoryURL": "https://github.com/google/gtm-session-fetcher.git", - "state": { - "branch": null, - "revision": "efda500b6d9858d38a76dbfbfa396bd644692e4a", - "version": "3.0.0" - } - }, - { - "package": "GTMAppAuth", - "repositoryURL": "https://github.com/google/GTMAppAuth.git", - "state": { - "branch": null, - "revision": "cee3c709307912d040bd1e06ca919875a92339c6", - "version": "2.0.0" - } - }, - { - "package": "JOSESwift", - "repositoryURL": "https://github.com/tobihagemann/JOSESwift.git", - "state": { - "branch": "feature/JWE-ECDH-GCM", - "revision": "e851667a4e6f6e8411d21474e77442041025e93c", - "version": null - } - }, - { - "package": "MSAL", - "repositoryURL": "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", - "state": { - "branch": null, - "revision": "31a806298d6aa71b40504e7ebda6d6a8923f0ebf", - "version": "1.2.5" - } - }, - { - "package": "MSGraphClientModels", - "repositoryURL": "https://github.com/phil1995/msgraph-sdk-objc-models-spm.git", - "state": { - "branch": null, - "revision": "172b07fe8a7da6072149e2fd92051a510b25035e", - "version": "1.3.0" - } - }, - { - "package": "MSGraphClientSDK", - "repositoryURL": "https://github.com/phil1995/msgraph-sdk-objc-spm.git", - "state": { - "branch": null, - "revision": "0320c6a99207b53288970382afcf5054852f9724", - "version": "1.0.0" - } - }, - { - "package": "PCloudSDKSwift", - "repositoryURL": "https://github.com/pCloud/pcloud-sdk-swift.git", - "state": { - "branch": null, - "revision": "6da4ca6bb4e7068145d9325988e29862d26300ba", - "version": "3.2.0" - } - }, - { - "package": "Promises", - "repositoryURL": "https://github.com/google/promises.git", - "state": { - "branch": null, - "revision": "611337c330350c9c1823ad6d671e7f936af5ee13", - "version": "2.0.0" - } - }, - { - "package": "swift-log", - "repositoryURL": "https://github.com/apple/swift-log.git", - "state": { - "branch": null, - "revision": "3e3ef75109d6801b2c44504e73f55f0dce6662c9", - "version": "1.5.1" - } - }, - { - "package": "Introspect", - "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", - "state": { - "branch": null, - "revision": "f2616860a41f9d9932da412a8978fec79c06fe24", - "version": "0.1.4" - } - }, - { - "package": "TPInAppReceipt", - "repositoryURL": "https://github.com/tikhop/TPInAppReceipt.git", - "state": { - "branch": null, - "revision": "5b830d6ce6c34bb4bb976917576ab560e7945037", - "version": "3.3.4" - } - } - ] - }, - "version": 1 + "pins" : [ + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "3d36a58a2b736f7bc499453e996a704929b25080", + "version" : "1.6.0" + } + }, + { + "identity" : "asn1swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tikhop/ASN1Swift", + "state" : { + "revision" : "b53bee03a942623db25afc5bfb80227b2cb3b425", + "version" : "1.2.4" + } + }, + { + "identity" : "aws-sdk-ios-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/aws-amplify/aws-sdk-ios-spm.git", + "state" : { + "revision" : "51d99d74be7249ac6444581bd1e394fb60ea86a3", + "version" : "2.30.4" + } + }, + { + "identity" : "base32", + "kind" : "remoteSourceControl", + "location" : "https://github.com/norio-nomura/Base32.git", + "state" : { + "revision" : "c4bc0a49689999ae2c7c778f3830a6a6e694efb8", + "version" : "0.9.0" + } + }, + { + "identity" : "cloud-access-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/cryptomator/cloud-access-swift.git", + "state" : { + "branch" : "feature/hub-poc", + "revision" : "302afc0f5960fac7dd67cc3c09e52d21ba45f829" + } + }, + { + "identity" : "cocoalumberjack", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", + "state" : { + "revision" : "0188d31089b5881a269e01777be74c7316924346", + "version" : "3.8.0" + } + }, + { + "identity" : "cryptolib-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/cryptomator/cryptolib-swift.git", + "state" : { + "revision" : "6e5dbea6e05742ad82a074bf7ee8c3305d92fbae", + "version" : "1.1.0" + } + }, + { + "identity" : "dropbox-sdk-obj-c-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/phil1995/dropbox-sdk-obj-c-spm.git", + "state" : { + "revision" : "f0eafe25d26c52377c4a1c08f1dbd77320164994", + "version" : "7.0.0" + } + }, + { + "identity" : "google-api-objectivec-client-for-rest", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/google-api-objectivec-client-for-rest.git", + "state" : { + "revision" : "260501c0425e95e038c65436436161266bf548e9", + "version" : "3.0.0" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/groue/GRDB.swift.git", + "state" : { + "revision" : "dd7e7f39e8e4d7a22d258d9809a882f914690b01", + "version" : "5.26.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "efda500b6d9858d38a76dbfbfa396bd644692e4a", + "version" : "3.0.0" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "cee3c709307912d040bd1e06ca919875a92339c6", + "version" : "2.0.0" + } + }, + { + "identity" : "joseswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tobihagemann/JOSESwift.git", + "state" : { + "branch" : "feature/JWE-ECDH-GCM", + "revision" : "e851667a4e6f6e8411d21474e77442041025e93c" + } + }, + { + "identity" : "microsoft-authentication-library-for-objc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", + "state" : { + "revision" : "31a806298d6aa71b40504e7ebda6d6a8923f0ebf", + "version" : "1.2.5" + } + }, + { + "identity" : "msgraph-sdk-objc-models-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/phil1995/msgraph-sdk-objc-models-spm.git", + "state" : { + "revision" : "172b07fe8a7da6072149e2fd92051a510b25035e", + "version" : "1.3.0" + } + }, + { + "identity" : "msgraph-sdk-objc-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/phil1995/msgraph-sdk-objc-spm.git", + "state" : { + "revision" : "0320c6a99207b53288970382afcf5054852f9724", + "version" : "1.0.0" + } + }, + { + "identity" : "pcloud-sdk-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pCloud/pcloud-sdk-swift.git", + "state" : { + "revision" : "6da4ca6bb4e7068145d9325988e29862d26300ba", + "version" : "3.2.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "611337c330350c9c1823ad6d671e7f936af5ee13", + "version" : "2.0.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "3e3ef75109d6801b2c44504e73f55f0dce6662c9", + "version" : "1.5.1" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "5b3f3996c7a2a84d5f4ba0e03cd7d584154778f2", + "version" : "0.3.1" + } + }, + { + "identity" : "tpinappreceipt", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tikhop/TPInAppReceipt.git", + "state" : { + "revision" : "5b830d6ce6c34bb4bb976917576ab560e7945037", + "version" : "3.3.4" + } + } + ], + "version" : 2 } diff --git a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift index 7b1d34bfd..e96a17901 100644 --- a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift +++ b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift @@ -45,24 +45,11 @@ class AddHubVaultCoordinator: Coordinator { } func start() { - let viewModel = DetectedHubVaultViewModel(onButtonTap: { [weak self] in - Task { [weak self] in - await self?.login() - } - }) - let viewController = UIHostingController(rootView: CryptomatorSuccessView(viewModel: viewModel)) - navigationController.pushViewController(viewController, animated: true) - } - - func login() async { let viewModel = HubAuthenticationViewModel(vaultConfig: downloadedVaultConfig.vaultConfig, hubUserAuthenticator: self, delegate: self) - await viewModel.login() - DispatchQueue.main.sync { - let viewController = UIHostingController(rootView: HubAuthenticationView(viewModel: viewModel)) - navigationController.pushViewController(viewController, animated: false) - } + let viewController = HubAuthenticationViewController(viewModel: viewModel) + navigationController.pushViewController(viewController, animated: true) } } diff --git a/Cryptomator/S3/S3AuthenticationView.swift b/Cryptomator/S3/S3AuthenticationView.swift index 83c9f2a0b..521aea10b 100644 --- a/Cryptomator/S3/S3AuthenticationView.swift +++ b/Cryptomator/S3/S3AuthenticationView.swift @@ -56,9 +56,7 @@ struct S3AuthenticationView: View { .disableAutocorrection(true) .autocapitalization(.none) } - .introspectTableView(customize: { tableView in - tableView.backgroundColor = .cryptomatorBackground - }) + .setListBackgroundColor(.cryptomatorBackground) } } diff --git a/Cryptomator/WebDAV/WebDAVAuthentication.swift b/Cryptomator/WebDAV/WebDAVAuthentication.swift index 1a17f8fa2..02b0cd3b0 100644 --- a/Cryptomator/WebDAV/WebDAVAuthentication.swift +++ b/Cryptomator/WebDAV/WebDAVAuthentication.swift @@ -39,9 +39,7 @@ struct WebDAVAuthentication: View { } .focusedLegacy($focusedField, equals: .password) } - .introspectTableView(customize: { tableView in - tableView.backgroundColor = .cryptomatorBackground - }) + .setListBackgroundColor(.cryptomatorBackground) } } diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index 6b62cc12d..c616a5cb0 100644 --- a/CryptomatorCommon/Package.swift +++ b/CryptomatorCommon/Package.swift @@ -27,7 +27,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/cryptomator/cloud-access-swift.git", branch: "feature/hub-poc"), - .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", .upToNextMinor(from: "3.8.0")) + .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", .upToNextMinor(from: "3.8.0")), + .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", .upToNextMajor(from: "0.3.0")) ], targets: [ .target( @@ -41,7 +42,8 @@ let package = Package( name: "CryptomatorCommonCore", dependencies: [ .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), - .product(name: "CryptomatorCloudAccessCore", package: "cloud-access-swift") + .product(name: "CryptomatorCloudAccessCore", package: "cloud-access-swift"), + .product(name: "Introspect", package: "SwiftUI-Introspect") ] ), .testTarget( diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSimpleButtonView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSimpleButtonView.swift new file mode 100644 index 000000000..79bee3c53 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSimpleButtonView.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct CryptomatorSimpleButtonView: View { + let buttonTitle: String + let onButtonTap: () -> Void + let headerTitle: String + + var body: some View { + List { + Section { + Button(buttonTitle) { + onButtonTap() + } + } header: { + HStack { + Spacer() + VStack(alignment: .center, spacing: 20) { + Image("bot-vault") + Text(headerTitle) + .textCase(.none) + .foregroundColor(.primary) + .font(.body) + } + .padding(.bottom, 12) + Spacer() + } + } + } + .setListBackgroundColor(.cryptomatorBackground) + } +} + +struct CryptomatorSimpleButtonView_Previews: PreviewProvider { + static var previews: some View { + CryptomatorSimpleButtonView(buttonTitle: "Button", onButtonTap: {}, headerTitle: "Header title.") + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift index ebee58819..e7deae92e 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift @@ -8,43 +8,45 @@ public struct HubAuthenticationView: View { } public var body: some View { - VStack { - switch viewModel.authenticationFlowState { - case .needsDeviceRegistration: - Text("This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device.") - TextField("Device name", text: $viewModel.deviceName) - Button("Register") { - Task { - await viewModel.register() - } + ZStack { + Color.cryptomatorBackground + .ignoresSafeArea() + VStack { + switch viewModel.authenticationFlowState { + case .deviceRegistration: + HubDeviceRegistrationView( + deviceName: $viewModel.deviceName, + onRegisterTap: { Task { await viewModel.register() }} + ) + case .accessNotGranted: + HubAccessNotGrantedView(onRefresh: { Task { await viewModel.refresh() }}) + case .receivedExistingKey: + Text("Received existing key") + case let .loading(text): + ProgressView() + Text(text) + case .userLogin: + HubLoginView(onLogin: { Task { await viewModel.login() }}) + case .licenseExceeded: + CryptomatorErrorView(text: "Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license.") + case let .error(info): + CryptomatorErrorView(text: info) } - case .deviceRegisteredSuccessfully: - Text("To access the vault, your device needs to be authorized by the vault owner.") - Button("Continue") { - Task { - await viewModel.continueToAccessCheck() - } - } - case .accessNotGranted: - Text("Your device has not vet been authorized to access this vault. Ask the vault owner to authorize it.") - Button("Refresh") { - Task { - await viewModel.refresh() - } - } - case .receivedExistingKey: - Text("Received existing key") - case let .loading(text): - ProgressView() - Text(text) - case .userLogin: - HubLoginView(onLogin: { Task { await viewModel.login() }}) - case .licenseExceeded: - CryptomatorErrorView(text: "Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license.") - case let .error(info): - CryptomatorErrorView(text: info) + } + .padding() + .navigationTitle("Hub Vault") + .alert( + isPresented: .init( + get: { viewModel.authenticationFlowState == .deviceRegistration(.needsAuthorization) }, + set: { _ in Task { await viewModel.continueToAccessCheck() }} + ) + ) { + Alert( + title: Text("Information"), + message: Text("To access the vault, your device needs to be authorized by the vault owner."), + dismissButton: .default(Text("Continue")) + ) } } - .padding() } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewController.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewController.swift new file mode 100644 index 000000000..e08a46a17 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewController.swift @@ -0,0 +1,63 @@ +import Combine +import Foundation +import SwiftUI + +/** + ViewController for the `HubAuthenticationView`. + + This ViewController build the bridge between UIKit and the SwiftUI `HubAuthenticationView`. + This bridge is needed to show the tool bar items of `HubAuthenticationView` in a UIKit `UINavigationController`. + */ +public class HubAuthenticationViewController: UIViewController { + private let viewModel: HubAuthenticationViewModel + private var cancellables = Set() + + public init(viewModel: HubAuthenticationViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + + viewModel.$authenticationFlowState + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] in + self?.updateToolbar(state: $0) + }) + .store(in: &cancellables) + setupSwiftUIView() + } + + private func setupSwiftUIView() { + let child = UIHostingController(rootView: HubAuthenticationView(viewModel: viewModel)) + addChild(child) + view.addSubview(child.view) + child.didMove(toParent: self) + child.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate(child.view.constraints(equalTo: view)) + } + + /** + Updates the `UINavigationItem` based on the given `state`. + - Note: This solution is far from ideal as we need to update the content of the tool bar in two places, i.e. in this method and inside the SwiftUI itself. Otherwise the behavior can differ when used inside a UINavigationController and a "SwiftUI native" `NavigationView`/ `NavigationStackView`. + */ + private func updateToolbar(state: HubAuthenticationViewModel.State) { + switch state { + case .deviceRegistration: + let registerButton = UIBarButtonItem(title: "Register", style: .done, target: self, action: #selector(registerButtonTapped)) + navigationItem.rightBarButtonItem = registerButton + default: + navigationItem.rightBarButtonItem = nil + } + } + + @objc private func registerButtonTapped() { + Task { await viewModel.register() } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index 9f28beedf..3fec23489 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -15,12 +15,16 @@ public class HubAuthenticationViewModel: ObservableObject { case receivedExistingKey case accessNotGranted case licenseExceeded - case deviceRegisteredSuccessfully - case needsDeviceRegistration + case deviceRegistration(DeviceRegistration) case loading(text: String) case error(description: String) } + public enum DeviceRegistration: Equatable { + case deviceName + case needsAuthorization + } + @Published var authenticationFlowState: State = .userLogin @Published public var deviceName: String = "" @@ -75,7 +79,7 @@ public class HubAuthenticationViewModel: ObservableObject { await setStateToErrorState(with: error) return } - await setState(to: .deviceRegisteredSuccessfully) + await setState(to: .deviceRegistration(.needsAuthorization)) } public func refresh() async { @@ -102,7 +106,7 @@ public class HubAuthenticationViewModel: ObservableObject { case .accessNotGranted: await setState(to: .accessNotGranted) case .needsDeviceRegistration: - await setState(to: .needsDeviceRegistration) + await setState(to: .deviceRegistration(.deviceName)) case .licenseExceeded: await setState(to: .licenseExceeded) } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift new file mode 100644 index 000000000..43092e133 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct HubAccessNotGrantedView: View { + var onRefresh: () -> Void + + var body: some View { + CryptomatorSimpleButtonView( + buttonTitle: "Refresh", + onButtonTap: onRefresh, + headerTitle: "Your device has not vet been authorized to access this vault. Ask the vault owner to authorize it." + ) + } +} + +struct HubDeviceRegisteredSuccessfullyView_Previews: PreviewProvider { + static var previews: some View { + HubAccessNotGrantedView(onRefresh: {}) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift new file mode 100644 index 000000000..066fe177c --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift @@ -0,0 +1,41 @@ +import SwiftUI + +struct HubDeviceRegistrationView: View { + @Binding var deviceName: String + var onRegisterTap: () -> Void + + @FocusStateLegacy private var field: Field? = .deviceName + + private enum Field: CaseIterable { + case deviceName + } + + var body: some View { + List { + Section { + TextField( + "", + text: $deviceName, + onCommit: onRegisterTap + ) + .focusedLegacy($field, equals: .deviceName) + } footer: { + Text("This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device.") + } + } + .setListBackgroundColor(.cryptomatorBackground) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Register") { + onRegisterTap() + } + } + } + } +} + +struct HubDeviceRegistrationView_Previews: PreviewProvider { + static var previews: some View { + HubDeviceRegistrationView(deviceName: .constant(""), onRegisterTap: {}) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubLoginView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubLoginView.swift index 849c19f05..86dffcd3b 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubLoginView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubLoginView.swift @@ -4,10 +4,11 @@ struct HubLoginView: View { var onLogin: () -> Void var body: some View { - Text("Login to unlock your vault") - Button("Login") { - onLogin() - } + CryptomatorSimpleButtonView( + buttonTitle: "Login", + onButtonTap: onLogin, + headerTitle: "Login to unlock your vault" + ) } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift index c9c025afe..9ecb8dbd3 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift @@ -36,7 +36,7 @@ public final class HubXPCLoginCoordinator: Coordinator { let viewModel = HubAuthenticationViewModel(vaultConfig: vaultConfig, hubUserAuthenticator: self, delegate: self) - let viewController = UIHostingController(rootView: HubAuthenticationView(viewModel: viewModel)) + let viewController = HubAuthenticationViewController(viewModel: viewModel) navigationController.pushViewController(viewController, animated: true) } } diff --git a/Cryptomator/Common/SwiftUI+Focus.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+Focus.swift similarity index 100% rename from Cryptomator/Common/SwiftUI+Focus.swift rename to CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+Focus.swift diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+ListBackground.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+ListBackground.swift new file mode 100644 index 000000000..779e849e3 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+ListBackground.swift @@ -0,0 +1,25 @@ +import Introspect +import SwiftUI + +public extension View { + func setListBackgroundColor(_ color: Color) -> some View { + modifier(ListBackgroundModifier(color: color)) + } +} + +struct ListBackgroundModifier: ViewModifier { + let color: Color + + public func body(content: Content) -> some View { + if #available(iOS 16, *) { + content + .scrollContentBackground(.hidden) + .background(color) + } else { + content + .introspectTableView { + $0.backgroundColor = UIColor(color) + } + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/UIColor+CryptomatorColors.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/UIColor+CryptomatorColors.swift index 86deea7f6..aec402502 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/UIColor+CryptomatorColors.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/UIColor+CryptomatorColors.swift @@ -6,6 +6,7 @@ // Copyright © 2022 Skymatic GmbH. All rights reserved. // +import SwiftUI import UIKit public extension UIColor { @@ -21,3 +22,9 @@ public extension UIColor { return UIColor(named: "yellow")! } } + +public extension Color { + static var cryptomatorPrimary: Color { Color(UIColor.cryptomatorPrimary) } + static var cryptomatorBackground: Color { Color(UIColor.cryptomatorBackground) } + static var cryptomatorYellow: Color { Color(UIColor.cryptomatorYellow) } +} From cbd5d3408c3356f9e6e0ac0e345431f9c93520dc Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 10 Jul 2023 15:45:53 +0200 Subject: [PATCH 18/45] added device type [ci skip] --- .../Hub/CryptomatorHubAuthenticator.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 42a3532ea..736a1b684 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -71,7 +71,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving let deviceID = try getDeviceID() let publicKey = try CryptomatorHubKeyProvider.shared.getPublicKey() let derPubKey = publicKey.derRepresentation - let dto = CreateDeviceDto(id: deviceID, name: name, publicKey: derPubKey.base64URLEncodedString()) + let dto = CreateDeviceDto(id: deviceID, name: name, type: "MOBILE", publicKey: derPubKey.base64URLEncodedString()) guard let devicesResourceURL = URL(string: hubConfig.devicesResourceUrl) else { throw CryptomatorHubAuthenticatorError.invalidDeviceResourceURL } @@ -113,6 +113,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving struct CreateDeviceDto: Codable { let id: String let name: String + let type: String let publicKey: String } } From 16c401706063ddeb6ba114bd7acaeefb740b6d38 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 22 Aug 2023 14:58:55 +0200 Subject: [PATCH 19/45] changed redirect url to associated domain --- Cryptomator/AppDelegate.swift | 9 +++++++++ ...CryptomatorHubAuthenticator+HubAuthenticating.swift | 10 +++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Cryptomator/AppDelegate.swift b/Cryptomator/AppDelegate.swift index 8635e1f1a..4c2922d1e 100644 --- a/Cryptomator/AppDelegate.swift +++ b/Cryptomator/AppDelegate.swift @@ -116,6 +116,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { switch userActivity.activityType { case "OpenVaultIntent": return handleOpenInFilesApp(for: userActivity) + case NSUserActivityTypeBrowsingWeb where urlIsHubAuth(userActivity.webpageURL): + return CryptomatorHubAuthenticator.currentAuthorizationFlow?.resumeExternalUserAgentFlow(with: userActivity.webpageURL!) ?? false default: DDLogInfo("Received an unsupported userActivity of type: \(String(describing: userActivity.activityType))") return false @@ -131,6 +133,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } + private func urlIsHubAuth(_ url: URL?) -> Bool { + guard let url = url else { + return false + } + return url.scheme == "https" && url.host == "ios.cryptomator.org" && url.path == "/hub/auth" + } + private func cleanup() { _ = VaultDBManager.shared.removeAllUnusedFileProviderDomains() do { diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift index a4e11951c..ed2ae8377 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift @@ -1,5 +1,5 @@ // -// CryptomatorHubAuthenticator.swift +// CryptomatorHubAuthenticator+HubAuthenticating.swift // // // Created by Philipp Schmid on 22.07.22. @@ -19,7 +19,7 @@ enum HubAuthenticationError: Error { } extension CryptomatorHubAuthenticator: HubAuthenticating { - private static var currentAuthorizationFlow: OIDExternalUserAgentSession? + public static var currentAuthorizationFlow: OIDExternalUserAgentSession? public func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState { guard let authorizationEndpoint = URL(string: hubConfig.authEndpoint) else { @@ -28,13 +28,13 @@ extension CryptomatorHubAuthenticator: HubAuthenticating { guard let tokenEndpoint = URL(string: hubConfig.tokenEndpoint) else { throw HubAuthenticationError.invalidTokenEndpoint } - guard let redirectURL = URL(string: "hub.org.cryptomator.ios:/auth") else { + guard let redirectURL = URL(string: "https://ios.cryptomator.org/hub/auth") else { throw HubAuthenticationError.invalidRedirectURL } let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint, tokenEndpoint: tokenEndpoint) - - let request = OIDAuthorizationRequest(configuration: configuration, clientId: hubConfig.clientId, scopes: nil, redirectURL: redirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil) + let additionalParameters = ["prompt": "login"] // Required for redirect to associated domain, so there is always user interaction + let request = OIDAuthorizationRequest(configuration: configuration, clientId: hubConfig.clientId, scopes: nil, redirectURL: redirectURL, responseType: OIDResponseTypeCode, additionalParameters: additionalParameters) return try await withCheckedThrowingContinuation({ continuation in DispatchQueue.main.async { CryptomatorHubAuthenticator.currentAuthorizationFlow = From dcc1c956a0fce0cc7054ec6872fc9538d5971da0 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 22 Aug 2023 15:03:40 +0200 Subject: [PATCH 20/45] fixed unneeded_synthesized_initializer reported by swiftlint --- CryptomatorFileProvider/DatabaseURLProvider.swift | 4 ---- CryptomatorFileProvider/FileProviderItemList.swift | 5 ----- 2 files changed, 9 deletions(-) diff --git a/CryptomatorFileProvider/DatabaseURLProvider.swift b/CryptomatorFileProvider/DatabaseURLProvider.swift index 987b7d22c..554d9a1b1 100644 --- a/CryptomatorFileProvider/DatabaseURLProvider.swift +++ b/CryptomatorFileProvider/DatabaseURLProvider.swift @@ -13,10 +13,6 @@ public struct DatabaseURLProvider { public static let shared = DatabaseURLProvider(documentStorageURLProvider: NSFileProviderManager.default) let documentStorageURLProvider: DocumentStorageURLProvider - init(documentStorageURLProvider: DocumentStorageURLProvider) { - self.documentStorageURLProvider = documentStorageURLProvider - } - public func getDatabaseURL(for domain: NSFileProviderDomain) -> URL { let documentStorageURL = documentStorageURLProvider.documentStorageURL let domainURL = documentStorageURL.appendingPathComponent(domain.pathRelativeToDocumentStorage, isDirectory: true) diff --git a/CryptomatorFileProvider/FileProviderItemList.swift b/CryptomatorFileProvider/FileProviderItemList.swift index fb728d004..5530b2015 100644 --- a/CryptomatorFileProvider/FileProviderItemList.swift +++ b/CryptomatorFileProvider/FileProviderItemList.swift @@ -12,9 +12,4 @@ import Foundation public struct FileProviderItemList { public let items: [FileProviderItem] public let nextPageToken: NSFileProviderPage? - - init(items: [FileProviderItem], nextPageToken: NSFileProviderPage?) { - self.items = items - self.nextPageToken = nextPageToken - } } From d8b289368de0e96e87e0def68dee6219e26ceeb4 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 19 Sep 2023 08:44:00 +0200 Subject: [PATCH 21/45] reverted redirect url to custom scheme, removed associated domain --- Cryptomator/AppDelegate.swift | 9 --------- Cryptomator/Cryptomator.entitlements | 4 ---- .../CryptomatorHubAuthenticator+HubAuthenticating.swift | 8 ++++---- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/Cryptomator/AppDelegate.swift b/Cryptomator/AppDelegate.swift index 4c2922d1e..8635e1f1a 100644 --- a/Cryptomator/AppDelegate.swift +++ b/Cryptomator/AppDelegate.swift @@ -116,8 +116,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { switch userActivity.activityType { case "OpenVaultIntent": return handleOpenInFilesApp(for: userActivity) - case NSUserActivityTypeBrowsingWeb where urlIsHubAuth(userActivity.webpageURL): - return CryptomatorHubAuthenticator.currentAuthorizationFlow?.resumeExternalUserAgentFlow(with: userActivity.webpageURL!) ?? false default: DDLogInfo("Received an unsupported userActivity of type: \(String(describing: userActivity.activityType))") return false @@ -133,13 +131,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - private func urlIsHubAuth(_ url: URL?) -> Bool { - guard let url = url else { - return false - } - return url.scheme == "https" && url.host == "ios.cryptomator.org" && url.path == "/hub/auth" - } - private func cleanup() { _ = VaultDBManager.shared.removeAllUnusedFileProviderDomains() do { diff --git a/Cryptomator/Cryptomator.entitlements b/Cryptomator/Cryptomator.entitlements index d59124a3b..3fc177eb3 100644 --- a/Cryptomator/Cryptomator.entitlements +++ b/Cryptomator/Cryptomator.entitlements @@ -2,10 +2,6 @@ - com.apple.developer.associated-domains - - applinks:ios.cryptomator.org - com.apple.developer.default-data-protection NSFileProtectionComplete com.apple.security.application-groups diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift index ed2ae8377..7e23bcc15 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift @@ -19,7 +19,7 @@ enum HubAuthenticationError: Error { } extension CryptomatorHubAuthenticator: HubAuthenticating { - public static var currentAuthorizationFlow: OIDExternalUserAgentSession? + private static var currentAuthorizationFlow: OIDExternalUserAgentSession? public func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState { guard let authorizationEndpoint = URL(string: hubConfig.authEndpoint) else { @@ -28,13 +28,13 @@ extension CryptomatorHubAuthenticator: HubAuthenticating { guard let tokenEndpoint = URL(string: hubConfig.tokenEndpoint) else { throw HubAuthenticationError.invalidTokenEndpoint } - guard let redirectURL = URL(string: "https://ios.cryptomator.org/hub/auth") else { + guard let redirectURL = URL(string: "hub.org.cryptomator.ios:/auth") else { throw HubAuthenticationError.invalidRedirectURL } let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint, tokenEndpoint: tokenEndpoint) - let additionalParameters = ["prompt": "login"] // Required for redirect to associated domain, so there is always user interaction - let request = OIDAuthorizationRequest(configuration: configuration, clientId: hubConfig.clientId, scopes: nil, redirectURL: redirectURL, responseType: OIDResponseTypeCode, additionalParameters: additionalParameters) + + let request = OIDAuthorizationRequest(configuration: configuration, clientId: hubConfig.clientId, scopes: nil, redirectURL: redirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil) return try await withCheckedThrowingContinuation({ continuation in DispatchQueue.main.async { CryptomatorHubAuthenticator.currentAuthorizationFlow = From 9198b1b14874683a61ef17f27ba514dbba9073bb Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 19 Sep 2023 08:49:19 +0200 Subject: [PATCH 22/45] applied swiftformat --- Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift | 2 +- Cryptomator/Onboarding/OnboardingViewController.swift | 2 +- .../FileProviderXPC/FileProviderConnector.swift | 2 +- .../CryptomatorCommonCore/Manager/VaultPasswordManager.swift | 2 +- .../CryptomatorCommonCore/S3/CryptomatorKeychain+S3.swift | 2 +- CryptomatorFileProvider/FileProviderAdapterError.swift | 2 +- CryptomatorFileProvider/LocalURLProviderType.swift | 2 +- CryptomatorFileProvider/Promise+AllIgnoringResult.swift | 2 +- .../Mocks/CustomCloudProviderMockTests.swift | 2 +- CryptomatorTests/Mocks/IAPManagerMock.swift | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift b/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift index 28318c8c7..7c12f3b90 100644 --- a/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift +++ b/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift @@ -1,5 +1,5 @@ // -// TableViewCellViewModel.swift +// BindableTableViewCellViewModel.swift // Cryptomator // // Created by Philipp Schmid on 29.07.21. diff --git a/Cryptomator/Onboarding/OnboardingViewController.swift b/Cryptomator/Onboarding/OnboardingViewController.swift index 3a28097eb..2f74cca8f 100644 --- a/Cryptomator/Onboarding/OnboardingViewController.swift +++ b/Cryptomator/Onboarding/OnboardingViewController.swift @@ -1,5 +1,5 @@ // -// OnboardingWelcomeViewController.swift +// OnboardingViewController.swift // Cryptomator // // Created by Tobias Hagemann on 08.09.21. diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift index c394a04b6..60eb802f6 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift @@ -1,5 +1,5 @@ // -// File.swift +// FileProviderConnector.swift // CryptomatorCommonCore // // Created by Philipp Schmid on 26.07.21. diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift index 43503810b..d63b54831 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift @@ -1,5 +1,5 @@ // -// VaultPasswordKeychainManager.swift +// VaultPasswordManager.swift // CryptomatorCommonCore // // Created by Philipp Schmid on 09.07.21. diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/CryptomatorKeychain+S3.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/CryptomatorKeychain+S3.swift index dc58d48bc..fcabacf2b 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/CryptomatorKeychain+S3.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/CryptomatorKeychain+S3.swift @@ -1,5 +1,5 @@ // -// CryptomatorKeychain+S3.swift.swift +// CryptomatorKeychain+S3.swift // // // Created by Philipp Schmid on 29.06.22. diff --git a/CryptomatorFileProvider/FileProviderAdapterError.swift b/CryptomatorFileProvider/FileProviderAdapterError.swift index 3e427f9a1..a2e23434f 100644 --- a/CryptomatorFileProvider/FileProviderAdapterError.swift +++ b/CryptomatorFileProvider/FileProviderAdapterError.swift @@ -1,5 +1,5 @@ // -// FileProviderDecoratorError.swift +// FileProviderAdapterError.swift // CryptomatorFileProvider // // Created by Philipp Schmid on 24.06.20. diff --git a/CryptomatorFileProvider/LocalURLProviderType.swift b/CryptomatorFileProvider/LocalURLProviderType.swift index 653dcc0f5..5295b3795 100644 --- a/CryptomatorFileProvider/LocalURLProviderType.swift +++ b/CryptomatorFileProvider/LocalURLProviderType.swift @@ -1,5 +1,5 @@ // -// LocalURLProvider.swift +// LocalURLProviderType.swift // CryptomatorFileProvider // // Created by Philipp Schmid on 03.03.22. diff --git a/CryptomatorFileProvider/Promise+AllIgnoringResult.swift b/CryptomatorFileProvider/Promise+AllIgnoringResult.swift index 23573f67b..057ba025a 100644 --- a/CryptomatorFileProvider/Promise+AllIgnoringResult.swift +++ b/CryptomatorFileProvider/Promise+AllIgnoringResult.swift @@ -1,5 +1,5 @@ // -// Promises+FinishedAll.swift +// Promise+AllIgnoringResult.swift // CryptomatorFileProvider // // Created by Philipp Schmid on 31.03.22. diff --git a/CryptomatorFileProviderTests/Mocks/CustomCloudProviderMockTests.swift b/CryptomatorFileProviderTests/Mocks/CustomCloudProviderMockTests.swift index 3cceb3b4b..38cb1f9c0 100644 --- a/CryptomatorFileProviderTests/Mocks/CustomCloudProviderMockTests.swift +++ b/CryptomatorFileProviderTests/Mocks/CustomCloudProviderMockTests.swift @@ -1,5 +1,5 @@ // -// CloudProviderMockTests.swift +// CustomCloudProviderMockTests.swift // CryptomatorFileProviderTests // // Created by Philipp Schmid on 01.07.20. diff --git a/CryptomatorTests/Mocks/IAPManagerMock.swift b/CryptomatorTests/Mocks/IAPManagerMock.swift index b073d22c4..f3a70b405 100644 --- a/CryptomatorTests/Mocks/IAPManagerMock.swift +++ b/CryptomatorTests/Mocks/IAPManagerMock.swift @@ -1,5 +1,5 @@ // -// IAPManager.swift +// IAPManagerMock.swift // CryptomatorTests // // Created by Philipp Schmid on 26.11.21. From eb74a1434a6d97ae82a9cd7655af9d639ac8499f Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Thu, 21 Sep 2023 23:12:53 +0200 Subject: [PATCH 23/45] updated dependencies --- .../xcshareddata/swiftpm/Package.resolved | 48 +++++++++---------- CryptomatorCommon/Package.swift | 2 +- .../Hub/HubAuthenticationViewModel.swift | 4 +- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a42dc6a6f..cdac65542 100644 --- a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/openid/AppAuth-iOS.git", "state" : { - "revision" : "3d36a58a2b736f7bc499453e996a704929b25080", - "version" : "1.6.0" + "revision" : "71cde449f13d453227e687458144bde372d30fc7", + "version" : "1.6.2" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/aws-amplify/aws-sdk-ios-spm.git", "state" : { - "revision" : "51d99d74be7249ac6444581bd1e394fb60ea86a3", - "version" : "2.30.4" + "revision" : "ca31418963a90bac80538e13f6b7af87ea14d279", + "version" : "2.33.4" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/cryptomator/cloud-access-swift.git", "state" : { - "branch" : "feature/hub-poc", - "revision" : "302afc0f5960fac7dd67cc3c09e52d21ba45f829" + "revision" : "1fe06a85f9ea38d9b22a84fb7dbd8de127c65f82", + "version" : "1.8.1" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/CocoaLumberjack/CocoaLumberjack.git", "state" : { - "revision" : "0188d31089b5881a269e01777be74c7316924346", - "version" : "3.8.0" + "revision" : "67ec5818a757aba4d7c534e21a905d878d128dbf", + "version" : "3.8.1" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/google-api-objectivec-client-for-rest.git", "state" : { - "revision" : "260501c0425e95e038c65436436161266bf548e9", - "version" : "3.0.0" + "revision" : "40930b2c3add6234b8be1a780c08cf88b6a7a1f7", + "version" : "3.2.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/gtm-session-fetcher.git", "state" : { - "revision" : "efda500b6d9858d38a76dbfbfa396bd644692e4a", - "version" : "3.0.0" + "revision" : "d415594121c9e8a4f9d79cecee0965cf35e74dbd", + "version" : "3.1.1" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GTMAppAuth.git", "state" : { - "revision" : "cee3c709307912d040bd1e06ca919875a92339c6", - "version" : "2.0.0" + "revision" : "41aba100f28395ebe842cd66e5d371cdd46c6792", + "version" : "4.0.0" } }, { @@ -113,8 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tobihagemann/JOSESwift.git", "state" : { - "branch" : "feature/JWE-ECDH-GCM", - "revision" : "e851667a4e6f6e8411d21474e77442041025e93c" + "revision" : "11442e7f1f803ef42281909c68f386b38afc5096", + "version" : "2.4.0-cryptomator" } }, { @@ -122,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", "state" : { - "revision" : "31a806298d6aa71b40504e7ebda6d6a8923f0ebf", - "version" : "1.2.5" + "revision" : "35846731c0971694f162b28fe8494c03b615ae74", + "version" : "1.2.16" } }, { @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/promises.git", "state" : { - "revision" : "611337c330350c9c1823ad6d671e7f936af5ee13", - "version" : "2.0.0" + "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", + "version" : "2.3.1" } }, { @@ -167,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "3e3ef75109d6801b2c44504e73f55f0dce6662c9", - "version" : "1.5.1" + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" } }, { @@ -176,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/siteline/SwiftUI-Introspect.git", "state" : { - "revision" : "5b3f3996c7a2a84d5f4ba0e03cd7d584154778f2", - "version" : "0.3.1" + "revision" : "121c146fe591b1320238d054ae35c81ffa45f45a", + "version" : "0.12.0" } }, { diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index c616a5cb0..589ac253c 100644 --- a/CryptomatorCommon/Package.swift +++ b/CryptomatorCommon/Package.swift @@ -26,7 +26,7 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/cryptomator/cloud-access-swift.git", branch: "feature/hub-poc"), + .package(url: "https://github.com/cryptomator/cloud-access-swift.git", .upToNextMinor(from: "1.8.0")), .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", .upToNextMinor(from: "3.8.0")), .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", .upToNextMajor(from: "0.3.0")) ], diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index 3fec23489..1d4d55809 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -49,7 +49,7 @@ public class HubAuthenticationViewModel: ObservableObject { } public func login() async { - guard let hubConfig = vaultConfig.hub else { + guard let hubConfig = vaultConfig.allegedHubConfig else { await setStateToErrorState(with: HubAuthenticationViewModelError.missingHubConfig) return } @@ -64,7 +64,7 @@ public class HubAuthenticationViewModel: ObservableObject { } public func register() async { - guard let hubConfig = vaultConfig.hub else { + guard let hubConfig = vaultConfig.allegedHubConfig else { await setStateToErrorState(with: HubAuthenticationViewModelError.missingHubConfig) return } From 3733c1a4e15378a942cb19f24fccd7eebb70c2a4 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Fri, 22 Sep 2023 10:25:45 +0200 Subject: [PATCH 24/45] Removed unused code --- Cryptomator.xcodeproj/project.pbxproj | 8 ---- .../Hub/DetectedHubVaultViewModel.swift | 21 --------- .../DetectedMasterkeyViewModel.swift | 23 --------- .../OpenExistingVaultCoordinator.swift | 1 - .../CryptomatorSuccessView.swift | 47 ------------------- 5 files changed, 100 deletions(-) delete mode 100644 Cryptomator/AddVault/Hub/DetectedHubVaultViewModel.swift delete mode 100644 Cryptomator/AddVault/OpenExistingVault/DetectedMasterkeyViewModel.swift delete mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSuccessView.swift diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 284c3dd59..1181312b2 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -253,7 +253,6 @@ 4AA782E2282A8FC0001A71E3 /* CachedFileManagerFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782E1282A8FC0001A71E3 /* CachedFileManagerFactoryMock.swift */; }; 4AA782E4282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782E3282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift */; }; 4AA782E6282A91BD001A71E3 /* CacheManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA782E5282A91BD001A71E3 /* CacheManagerMock.swift */; }; - 4AA8613725C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */; }; 4AA8614825C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */; }; 4AA8615125C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA8615025C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift */; }; 4AAD444727E26D1800D16707 /* UploadTaskManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AAD444627E26D1800D16707 /* UploadTaskManagerMock.swift */; }; @@ -290,7 +289,6 @@ 4AC005F327C3D932006FFE87 /* PremiumManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC005F227C3D932006FFE87 /* PremiumManagerMock.swift */; }; 4AC1157627F5BD890023F51B /* Promise+AllIgnoringResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */; }; 4AC1157827F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC1157727F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift */; }; - 4AC49CA029EF3BA6007AF41C /* DetectedHubVaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC49C9F29EF3BA6007AF41C /* DetectedHubVaultViewModel.swift */; }; 4AC86270273598CC00E15BA5 /* UIViewController+ProgressHUDError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AC8626F273598CC00E15BA5 /* UIViewController+ProgressHUDError.swift */; }; 4AD0F61C24AF203F0026B765 /* FileProvider+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD0F61B24AF203F0026B765 /* FileProvider+Actions.swift */; }; 4AD3D7D6282EBDE7008188CD /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AD3D7D5282EBDE7008188CD /* Intents.framework */; }; @@ -785,7 +783,6 @@ 4AA782E1282A8FC0001A71E3 /* CachedFileManagerFactoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedFileManagerFactoryMock.swift; sourceTree = ""; }; 4AA782E3282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSFileProviderDomainProviderMock.swift; sourceTree = ""; }; 4AA782E5282A91BD001A71E3 /* CacheManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManagerMock.swift; sourceTree = ""; }; - 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedMasterkeyViewModel.swift; sourceTree = ""; }; 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingVaultChooseFolderViewController.swift; sourceTree = ""; }; 4AA8615025C1DB5E002A59F5 /* OpenExistingVaultPasswordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenExistingVaultPasswordViewController.swift; sourceTree = ""; }; 4AAD444627E26D1800D16707 /* UploadTaskManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadTaskManagerMock.swift; sourceTree = ""; }; @@ -822,7 +819,6 @@ 4AC005F227C3D932006FFE87 /* PremiumManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumManagerMock.swift; sourceTree = ""; }; 4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+AllIgnoringResult.swift"; sourceTree = ""; }; 4AC1157727F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+AllIgnoringResultsTests.swift"; sourceTree = ""; }; - 4AC49C9F29EF3BA6007AF41C /* DetectedHubVaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectedHubVaultViewModel.swift; sourceTree = ""; }; 4AC8626F273598CC00E15BA5 /* UIViewController+ProgressHUDError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+ProgressHUDError.swift"; sourceTree = ""; }; 4AD0F61B24AF203F0026B765 /* FileProvider+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileProvider+Actions.swift"; sourceTree = ""; }; 4AD3D7D4282EBDE7008188CD /* CryptomatorIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = CryptomatorIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1666,7 +1662,6 @@ 4AA8613F25C1AC4D002A59F5 /* OpenExistingVault */ = { isa = PBXGroup; children = ( - 4AA8613625C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift */, 4A753DB82678A226005F79C1 /* OpenExistingLegacyVaultPasswordViewModel.swift */, 4AA8614725C1C670002A59F5 /* OpenExistingVaultChooseFolderViewController.swift */, 4A2FD08A25B5E437008565C8 /* OpenExistingVaultCoordinator.swift */, @@ -1877,7 +1872,6 @@ isa = PBXGroup; children = ( 4AF9D44A29C293E600EB3822 /* HubAddVaultCoordinator.swift */, - 4AC49C9F29EF3BA6007AF41C /* DetectedHubVaultViewModel.swift */, ); path = Hub; sourceTree = ""; @@ -2808,7 +2802,6 @@ 4A2FD08B25B5E437008565C8 /* OpenExistingVaultCoordinator.swift in Sources */, 7469AD9A266E26B0000DCD45 /* URL+Zip.swift in Sources */, 4AB8539026BA844300555F00 /* Publisher+OptionalAssign.swift in Sources */, - 4AC49CA029EF3BA6007AF41C /* DetectedHubVaultViewModel.swift in Sources */, 4A1C6D58274CE5BF00B41FFF /* LoadingCell.swift in Sources */, 4A3D65642680A4B7000DA764 /* LocalFileSystemAuthenticationViewController.swift in Sources */, 4AB1D4F427D61035009060AB /* AutoHidingLabel.swift in Sources */, @@ -2821,7 +2814,6 @@ 4A447E5625BF1F6A00D9520D /* CloudItemCell.swift in Sources */, 4A5F48EE272AA02A0084135F /* MaintenanceModeError+Localization.swift in Sources */, 4A63E4672742A8CB00026989 /* ListViewController.swift in Sources */, - 4AA8613725C19D4F002A59F5 /* DetectedMasterkeyViewModel.swift in Sources */, 7460FFEF26FCC6FC0018BCC4 /* OnboardingNavigationController.swift in Sources */, 4A1EB0D8268A6DE1006D072B /* AddLocalVaultViewController.swift in Sources */, 4A7B97E525B6F86E0044B7FB /* AccountListPosition.swift in Sources */, diff --git a/Cryptomator/AddVault/Hub/DetectedHubVaultViewModel.swift b/Cryptomator/AddVault/Hub/DetectedHubVaultViewModel.swift deleted file mode 100644 index 38d16c837..000000000 --- a/Cryptomator/AddVault/Hub/DetectedHubVaultViewModel.swift +++ /dev/null @@ -1,21 +0,0 @@ -import CryptomatorCommonCore -import Foundation -import SwiftUI - -struct DetectedHubVaultViewModel { - let backgroundColor = Color(UIColor.cryptomatorBackground) - let buttonColor = Color(UIColor.cryptomatorPrimary) - let description: String = "Detected Hub vault\nDo you want to login?" - let buttonText: String = "Login" - let onButtonTap: () -> Void -} - -extension CryptomatorSuccessView { - init(viewModel: DetectedHubVaultViewModel) { - self.init(text: viewModel.description, - buttonText: viewModel.buttonText, - onButtonTap: viewModel.onButtonTap, - buttonColor: viewModel.buttonColor, - backgroundColor: viewModel.backgroundColor) - } -} diff --git a/Cryptomator/AddVault/OpenExistingVault/DetectedMasterkeyViewModel.swift b/Cryptomator/AddVault/OpenExistingVault/DetectedMasterkeyViewModel.swift deleted file mode 100644 index b07fdc1be..000000000 --- a/Cryptomator/AddVault/OpenExistingVault/DetectedMasterkeyViewModel.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// DetectedMasterkeyViewModel.swift -// Cryptomator -// -// Created by Philipp Schmid on 27.01.21. -// Copyright © 2021 Skymatic GmbH. All rights reserved. -// - -import CryptomatorCloudAccessCore -import CryptomatorCommonCore -import Foundation - -struct DetectedMasterkeyViewModel { - let masterkeyPath: CloudPath - var text: String { - return String(format: LocalizedString.getValue("addVault.openExistingVault.detectedMasterkey.text"), vaultName) - } - - private var vaultName: String { - let masterkeyParentPath = masterkeyPath.deletingLastPathComponent() - return masterkeyParentPath.lastPathComponent - } -} diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift index 92cf84ae9..bc15cc376 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift @@ -214,7 +214,6 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder // MARK: - VaultInstalling func showSuccessfullyAddedVault(withName name: String, vaultUID: String) { - print("showSuccessfullyAddedVault") let child = AddVaultSuccessCoordinator(vaultName: name, vaultUID: vaultUID, navigationController: navigationController) child.parentCoordinator = self childCoordinators.append(child) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSuccessView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSuccessView.swift deleted file mode 100644 index 3c1e2d434..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorSuccessView.swift +++ /dev/null @@ -1,47 +0,0 @@ -import SwiftUI - -public struct CryptomatorSuccessView: View { - let text: String - let buttonText: String - let onButtonTap: () -> Void - let buttonColor: Color - let backgroundColor: Color - - public init(text: String, buttonText: String, onButtonTap: @escaping () -> Void, buttonColor: Color, backgroundColor: Color) { - self.text = text - self.buttonText = buttonText - self.onButtonTap = onButtonTap - self.buttonColor = buttonColor - self.backgroundColor = backgroundColor - } - - public var body: some View { - ZStack { - backgroundColor - VStack(spacing: 32) { - Spacer() - Image("bot-vault") - Text(text) - Spacer() - Button { - onButtonTap() - } label: { - Text(buttonText) - .foregroundColor(.white) - .bold() - .padding() - .frame(maxWidth: .infinity) - .background(buttonColor) - .cornerRadius(8) - .padding(.horizontal) - } - } - } - } -} - -struct CryptomatorSuccessView_Previews: PreviewProvider { - static var previews: some View { - CryptomatorSuccessView(text: "Lorem \nipsum", buttonText: "Continue", onButtonTap: {}, buttonColor: .blue, backgroundColor: .clear) - } -} From 0adf8a7ad113ae92164b3353d8d80b3ef181ff17 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Fri, 22 Sep 2023 10:32:08 +0200 Subject: [PATCH 25/45] Localized OpenExistingVaultCoordinator --- .../OpenExistingVaultCoordinator.swift | 64 ++++++++++--------- SharedResources/en.lproj/Localizable.strings | 1 + 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift index bc15cc376..cf698a9f9 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift @@ -128,40 +128,47 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder handleError(VaultCoordinatorError.wrongItemType, for: navigationController) return } - if vaultItem.isLegacyVault { - showAddExistingLegacyVault(vaultItem) + downloadAndProcessExistingLegacyVault(vaultItem) } else { - let hud = ProgressHUD() - hud.text = "Downloading Vault…" - hud.show(presentingViewController: navigationController) - VaultDBManager.shared.getUnverifiedVaultConfig(delegateAccountUID: account.accountUID, vaultItem: vaultItem).then { downloadedVaultConfig in - all(hud.dismiss(animated: true), Promise(downloadedVaultConfig)) - }.then { _, downloadedVaultConfig in - self.processDownloadedVaultConfig(downloadedVaultConfig, vaultItem: vaultItem) - }.catch { error in - hud.dismiss(animated: true).then { - self.handleError(error, for: self.navigationController) - } - } + downloadAndProcessExistingVault(vaultItem) } } - private func showAddExistingLegacyVault(_ vault: VaultItem) { + private func downloadAndProcessExistingLegacyVault(_ vaultItem: VaultItem) { let hud = ProgressHUD() - hud.text = "Downloading Vault…" + hud.text = LocalizedString.getValue("addVault.openExistingVault.downloadVault.progress") hud.show(presentingViewController: navigationController) - VaultDBManager.shared.downloadMasterkeyFile(delegateAccountUID: account.accountUID, vaultItem: vault).then { downloadedMasterkeyFile in + VaultDBManager.shared.downloadMasterkeyFile(delegateAccountUID: account.accountUID, vaultItem: vaultItem).then { downloadedMasterkeyFile in all(hud.dismiss(animated: true), Promise(downloadedMasterkeyFile)) }.then { _, downloadedMasterkeyFile in - let viewModel = OpenExistingLegacyVaultPasswordViewModel(provider: self.provider, - account: self.account, - vault: vault, - vaultUID: UUID().uuidString, - downloadedMasterkeyFile: downloadedMasterkeyFile) - let passwordVC = OpenExistingVaultPasswordViewController(viewModel: viewModel) - passwordVC.coordinator = self - self.navigationController.pushViewController(passwordVC, animated: true) + self.processDownloadedMasterkeyFile(downloadedMasterkeyFile, vaultItem: vaultItem) + }.catch { error in + hud.dismiss(animated: true).then { + self.handleError(error, for: self.navigationController) + } + } + } + + private func processDownloadedMasterkeyFile(_ downloadedMasterkeyFile: DownloadedMasterkeyFile, vaultItem: VaultItem) { + let viewModel = OpenExistingLegacyVaultPasswordViewModel(provider: provider, + account: account, + vault: vaultItem, + vaultUID: UUID().uuidString, + downloadedMasterkeyFile: downloadedMasterkeyFile) + let passwordVC = OpenExistingVaultPasswordViewController(viewModel: viewModel) + passwordVC.coordinator = self + navigationController.pushViewController(passwordVC, animated: true) + } + + private func downloadAndProcessExistingVault(_ vaultItem: VaultItem) { + let hud = ProgressHUD() + hud.text = LocalizedString.getValue("addVault.openExistingVault.downloadVault.progress") + hud.show(presentingViewController: navigationController) + VaultDBManager.shared.getUnverifiedVaultConfig(delegateAccountUID: account.accountUID, vaultItem: vaultItem).then { downloadedVaultConfig in + all(hud.dismiss(animated: true), Promise(downloadedVaultConfig)) + }.then { _, downloadedVaultConfig in + self.processDownloadedVaultConfig(downloadedVaultConfig, vaultItem: vaultItem) }.catch { error in hud.dismiss(animated: true).then { self.handleError(error, for: self.navigationController) @@ -176,7 +183,7 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder case .hub: handleHubVaultConfig(downloadedVaultConfig, vaultItem: vaultItem) case .unknown: - handleError(error: OpenExistingVaultCoordinatorError.unsupportedVaultConfig) + handleError(error: VaultProviderFactoryError.unsupportedVaultConfig) } } @@ -220,8 +227,3 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder child.start() } } - -enum OpenExistingVaultCoordinatorError: Error { - case unsupportedVaultConfig - // TODO: add Localization -} diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 69293b1d4..a2424694c 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -58,6 +58,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Where is the vault located?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator detected the vault \"%@\".\nWould you like to add this vault?"; "addVault.openExistingVault.detectedMasterkey.add" = "Add This Vault"; +"addVault.openExistingVault.downloadVault.progress" = "Downloading Vault…"; "addVault.openExistingVault.password.footer" = "Enter password for \"%@\"."; "addVault.openExistingVault.progress" = "Adding Vault…"; "addVault.success.info" = "Successfully added vault \"%@\".\nAccess this vault via the Files app."; From eb2c8a61b04bbc540fdbffdb221a52ec20b00390 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Fri, 22 Sep 2023 10:49:04 +0200 Subject: [PATCH 26/45] Prefilled device name --- .../CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index 1d4d55809..e9f809827 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -3,6 +3,7 @@ import CryptoKit import CryptomatorCloudAccessCore import Foundation import JOSESwift +import UIKit public enum HubAuthenticationViewModelError: Error { case missingHubConfig @@ -26,7 +27,7 @@ public class HubAuthenticationViewModel: ObservableObject { } @Published var authenticationFlowState: State = .userLogin - @Published public var deviceName: String = "" + @Published public var deviceName: String = UIDevice.current.name private let vaultConfig: UnverifiedVaultConfig private let deviceRegisteringService: HubDeviceRegistering From 35f5d5b5a203784bcf24364f26eb4d5878edf42d Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Fri, 22 Sep 2023 11:38:58 +0200 Subject: [PATCH 27/45] Localized HubAuthentication --- .../Hub/HubAuthenticationView.swift | 22 +++++++++---------- .../Hub/HubAuthenticationViewModel.swift | 5 ++--- .../HubDeviceRegisteredSuccessfullyView.swift | 4 ++-- .../Hub/HubDeviceRegistrationView.swift | 6 ++--- SharedResources/en.lproj/Localizable.strings | 13 +++++++++++ 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift index e7deae92e..ac150cef4 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift @@ -11,7 +11,7 @@ public struct HubAuthenticationView: View { ZStack { Color.cryptomatorBackground .ignoresSafeArea() - VStack { + VStack(spacing: 20) { switch viewModel.authenticationFlowState { case .deviceRegistration: HubDeviceRegistrationView( @@ -20,21 +20,19 @@ public struct HubAuthenticationView: View { ) case .accessNotGranted: HubAccessNotGrantedView(onRefresh: { Task { await viewModel.refresh() }}) - case .receivedExistingKey: - Text("Received existing key") - case let .loading(text): + case .loading: ProgressView() - Text(text) + Text(LocalizedString.getValue("hubAuthentication.loading")) case .userLogin: HubLoginView(onLogin: { Task { await viewModel.login() }}) case .licenseExceeded: - CryptomatorErrorView(text: "Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license.") - case let .error(info): - CryptomatorErrorView(text: info) + CryptomatorErrorView(text: LocalizedString.getValue("hubAuthentication.licenseExceeded")) + case let .error(description): + CryptomatorErrorView(text: description) } } .padding() - .navigationTitle("Hub Vault") + .navigationTitle(LocalizedString.getValue("hubAuthentication.title")) .alert( isPresented: .init( get: { viewModel.authenticationFlowState == .deviceRegistration(.needsAuthorization) }, @@ -42,9 +40,9 @@ public struct HubAuthenticationView: View { ) ) { Alert( - title: Text("Information"), - message: Text("To access the vault, your device needs to be authorized by the vault owner."), - dismissButton: .default(Text("Continue")) + title: Text(LocalizedString.getValue("hubAuthentication.deviceRegistration.needsAuthorization.alert.title")), + message: Text(LocalizedString.getValue("hubAuthentication.deviceRegistration.needsAuthorization.alert.message")), + dismissButton: .default(Text(LocalizedString.getValue("common.button.ok"))) ) } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index e9f809827..7b2f59031 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -13,11 +13,10 @@ public enum HubAuthenticationViewModelError: Error { public class HubAuthenticationViewModel: ObservableObject { public enum State: Equatable { case userLogin - case receivedExistingKey case accessNotGranted case licenseExceeded case deviceRegistration(DeviceRegistration) - case loading(text: String) + case loading case error(description: String) } @@ -92,7 +91,7 @@ public class HubAuthenticationViewModel: ObservableObject { await setStateToErrorState(with: HubAuthenticationViewModelError.missingAuthState) return } - await setState(to: .loading(text: "Cryptomator is receiving and processing the response from Hub. Please wait.")) + await setState(to: .loading) let authFlow: HubAuthenticationFlow do { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift index 43092e133..c0328584f 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift @@ -5,9 +5,9 @@ struct HubAccessNotGrantedView: View { var body: some View { CryptomatorSimpleButtonView( - buttonTitle: "Refresh", + buttonTitle: LocalizedString.getValue("common.button.refresh"), onButtonTap: onRefresh, - headerTitle: "Your device has not vet been authorized to access this vault. Ask the vault owner to authorize it." + headerTitle: LocalizedString.getValue("hubAuthentication.accessNotGranted") ) } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift index 066fe177c..67260b4ed 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift @@ -14,19 +14,19 @@ struct HubDeviceRegistrationView: View { List { Section { TextField( - "", + LocalizedString.getValue("hubAuthentication.deviceRegistration.deviceName.cells.name"), text: $deviceName, onCommit: onRegisterTap ) .focusedLegacy($field, equals: .deviceName) } footer: { - Text("This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device.") + Text(LocalizedString.getValue("hubAuthentication.deviceRegistration.deviceName.footer.title")) } } .setListBackgroundColor(.cryptomatorBackground) .toolbar { ToolbarItem(placement: .primaryAction) { - Button("Register") { + Button(LocalizedString.getValue("common.button.register")) { onRegisterTap() } } diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index a2424694c..95b58e62b 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Enable"; "common.button.next" = "Next"; "common.button.ok" = "OK"; +"common.button.refresh" = "Refresh"; +"common.button.register" = "Register"; "common.button.remove" = "Remove"; "common.button.retry" = "Retry"; "common.button.signOut" = "Sign Out"; @@ -111,6 +113,17 @@ "getFolderIntent.error.missingPath" = "No path was provided. Please provide a valid path for which a folder should be returned."; "getFolderIntent.error.noVaultSelected" = "No vault has been selected."; + +"hubAuthentication.title" = "Hub Vault"; +"hubAuthentication.loading" = "Cryptomator is receiving and processing the response from Hub. Please wait."; +"hubAuthentication.accessNotGranted" = "Your device has not yet been authorized to access this vault. Ask the vault owner to authorize it."; +"hubAuthentication.licenseExceeded" = "Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license."; +"hubAuthentication.deviceRegistration." = ""; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Device Name"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Register Device Successful"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "To access the vault, your device needs to be authorized by the vault owner."; + "intents.saveFile.missingFile" = "The provided file is not valid."; "intents.saveFile.invalidFolder" = "The provided folder is not valid."; "intents.saveFile.missingTemporaryFolder" = "Failed to create temporary folder."; From e8d87e8ca2d0d1e709fb6b89b91f703ccad72f10 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 25 Oct 2023 22:13:35 +0200 Subject: [PATCH 28/45] Active Hub subscription unlocks full version for corresponding vault --- Cryptomator.xcodeproj/project.pbxproj | 12 ++ .../AddVault/Hub/HubAddVaultCoordinator.swift | 4 +- .../CryptomatorDatabase.swift | 8 +- .../Hub/CryptomatorHubAuthenticator.swift | 7 +- .../Hub/HubAuthenticationFlowDelegate.swift | 8 +- .../Hub/HubAuthenticationViewModel.swift | 36 ++++- .../Hub/HubRepository.swift | 72 +++++++++ .../Hub/HubSubscriptionState.swift | 4 + .../Hub/HubXPCLoginCoordinator.swift | 13 +- .../Mocks/HubRepositoryMock.swift | 53 +++++++ .../Hub/HubDBRepositoryTests.swift | 89 ++++++++++++ .../DB/WorkingSetObserver.swift | 8 +- .../FileProviderAdapter.swift | 1 + .../FileProviderAdapterManager.swift | 27 +++- .../FileProviderItem.swift | 15 +- .../TaskExecutor/DownloadTaskExecutor.swift | 8 +- .../FolderCreationTaskExecutor.swift | 12 +- .../ItemEnumerationTaskExecutor.swift | 10 +- .../TaskExecutor/ReparentTaskExecutor.swift | 8 +- .../TaskExecutor/UploadTaskExecutor.swift | 9 +- .../PermissionProvider.swift | 127 ++++++++++++++++ .../RootFileProviderItem.swift | 13 +- .../Workflow/WorkflowFactory.swift | 2 + ...ileProviderAdapterEnumerateItemTests.swift | 4 + ...leProviderAdapterImportDocumentTests.swift | 4 + .../FileProviderEnumeratorTests.swift | 9 ++ .../FileProviderItemTests.swift | 56 +------ .../FileProviderNotificatorTests.swift | 9 ++ .../ItemEnumerationTaskTests.swift | 9 ++ .../Mocks/PermissionProviderMock.swift | 51 +++++++ .../PermissionProviderImplTests.swift | 137 ++++++++++++++++++ .../CacheManagingServiceSourceTests.swift | 4 + .../WorkingSetObserverTests.swift | 4 + .../FileProviderExtension.swift | 2 +- 34 files changed, 735 insertions(+), 100 deletions(-) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubRepository.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubSubscriptionState.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/HubRepositoryMock.swift create mode 100644 CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubDBRepositoryTests.swift create mode 100644 CryptomatorFileProvider/PermissionProvider.swift create mode 100644 CryptomatorFileProviderTests/Mocks/PermissionProviderMock.swift create mode 100644 CryptomatorFileProviderTests/PermissionProviderImplTests.swift diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 536a03dc8..9f8817d8d 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -18,6 +18,9 @@ 4A09BFC62684D599000E40AB /* VaultDetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09BFC52684D599000E40AB /* VaultDetailItem.swift */; }; 4A09E54C27071F3C0056D32A /* ErrorMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09E54B27071F3C0056D32A /* ErrorMapperTests.swift */; }; 4A09E54E27071F4F0056D32A /* ErrorMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09E54D27071F4F0056D32A /* ErrorMapper.swift */; }; + 4A0AA12B2AB8DB1800CF24FD /* PermissionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */; }; + 4A0AA12D2ABA277800CF24FD /* PermissionProviderImplTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */; }; + 4A0AA12F2ABA2A1600CF24FD /* PermissionProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */; }; 4A0C07E225AC80C100B83211 /* UIView+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0C07E125AC80C100B83211 /* UIView+Preview.swift */; }; 4A0C07EB25AC832900B83211 /* VaultListPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0C07EA25AC832900B83211 /* VaultListPosition.swift */; }; 4A0EAAD2296F604200E27B56 /* SessionTaskRegistratorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */; }; @@ -543,6 +546,9 @@ 4A09BFC52684D599000E40AB /* VaultDetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultDetailItem.swift; sourceTree = ""; }; 4A09E54B27071F3C0056D32A /* ErrorMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMapperTests.swift; sourceTree = ""; }; 4A09E54D27071F4F0056D32A /* ErrorMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMapper.swift; sourceTree = ""; }; + 4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProvider.swift; sourceTree = ""; }; + 4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProviderImplTests.swift; sourceTree = ""; }; + 4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProviderMock.swift; sourceTree = ""; }; 4A0C07E125AC80C100B83211 /* UIView+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Preview.swift"; sourceTree = ""; }; 4A0C07EA25AC832900B83211 /* VaultListPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListPosition.swift; sourceTree = ""; }; 4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTaskRegistratorMock.swift; sourceTree = ""; }; @@ -1192,6 +1198,7 @@ 4A9C8DFC27A007C2000063E4 /* FileProviderNotificatorTests.swift */, 4AFBFA19282946BF00E30818 /* InMemoryProgressManagerTests.swift */, 4AB1D4EF27D20420009060AB /* LocalURLProviderTests.swift */, + 4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */, 4AC1157727F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift */, 4ADC66C427A7F6D6002E6CC7 /* UnlockMonitorTests.swift */, 4A4F47F224B875070033328B /* URL+NameCollisionExtensionTests.swift */, @@ -1717,6 +1724,7 @@ 4AEECD3E279EC48200C6E2B5 /* NSFileProviderChangeObserverMock.swift */, 4AA782E3282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift */, 4AEECD3A279EB24300C6E2B5 /* NSFileProviderEnumerationObserverMock.swift */, + 4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */, 4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */, 4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */, 4ADC66C627A95E67002E6CC7 /* UnlockMonitorTaskExecutorMock.swift */, @@ -1901,6 +1909,7 @@ 4AB1D4EB27D0E027009060AB /* LocalURLProviderType.swift */, 4AA782DD282A8250001A71E3 /* NSFileProviderDomainProvider.swift */, 4AEE6EE02822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift */, + 4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */, 4AEE6EE92825716400E1B35E /* ProgressManager.swift */, 4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */, 4ADD233F26737CD400374E4E /* RootFileProviderItem.swift */, @@ -2537,6 +2546,7 @@ 4AFBFA1628293FE200E30818 /* UploadRetryingServiceSourceTests.swift in Sources */, 4AB1C33C265E9DBC00DC7A49 /* CloudTaskExecutorTestCase.swift in Sources */, 4AE5196727F495BF00BA6E4A /* WorkflowDependencyTasksCollectionMock.swift in Sources */, + 4A0AA12F2ABA2A1600CF24FD /* PermissionProviderMock.swift in Sources */, 4AC1157827F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift in Sources */, 4AE5196527F48D6600BA6E4A /* WorkflowDependencyFactoryTests.swift in Sources */, 4A49FABE271ECDE80069A0CC /* ItemEnumerationTaskManagerTests.swift in Sources */, @@ -2570,6 +2580,7 @@ 4ADC66C527A7F6D6002E6CC7 /* UnlockMonitorTests.swift in Sources */, 4ABC08D7250D1EB600E3CEDC /* DeletionTaskManagerTests.swift in Sources */, 4A511D45265EB13B000A0E01 /* ItemEnumerationTaskTests.swift in Sources */, + 4A0AA12D2ABA277800CF24FD /* PermissionProviderImplTests.swift in Sources */, 4A2F373724B47DB800460FD3 /* UploadTaskManagerTests.swift in Sources */, 4A248221266B8D37002D9F59 /* FileProviderAdapterImportDocumentTests.swift in Sources */, 4A511D5326615439000A0E01 /* ReparentTaskExecutorTests.swift in Sources */, @@ -2935,6 +2946,7 @@ 4A511D5D26668E47000A0E01 /* ReparentTaskRecord.swift in Sources */, 747F2F272587BC250072FB30 /* ReparentTask.swift in Sources */, 747F2F282587BC250072FB30 /* ReparentTaskDBManager.swift in Sources */, + 4A0AA12B2AB8DB1800CF24FD /* PermissionProvider.swift in Sources */, 4AB1D4EC27D0E027009060AB /* LocalURLProviderType.swift in Sources */, 4A511D4E2660FF9E000A0E01 /* WorkflowScheduler.swift in Sources */, 4AD9481A2909A66900072110 /* MaintenanceModeHelperServiceSource.swift in Sources */, diff --git a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift index e96a17901..9b8591d88 100644 --- a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift +++ b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift @@ -54,7 +54,9 @@ class AddHubVaultCoordinator: Coordinator { } extension AddHubVaultCoordinator: HubAuthenticationFlowDelegate { - func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async { + func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async { + let jwe = response.jwe + let privateKey = response.privateKey let hubVault = ExistingHubVault(vaultUID: vaultUID, delegateAccountUID: accountUID, jweData: jwe.compactSerializedData, diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift index 0da86df3c..e8fcd36ad 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift @@ -199,13 +199,9 @@ public class CryptomatorDatabase { } class func initialHubSupportMigration(_ db: Database) throws { - try db.create(table: "hubAccountInfo", body: { table in - table.column("userID", .text).primaryKey() - }) try db.create(table: "hubVaultAccount", body: { table in - table.column("id", .integer).primaryKey() - table.column("vaultUID", .text).notNull().unique().references("vaultAccounts", onDelete: .cascade) - table.column("hubUserID", .text).notNull().references("hubAccountInfo", onDelete: .cascade) + table.column("vaultUID", .text).primaryKey().references("vaultAccounts", onDelete: .cascade) + table.column("subscriptionState", .text).notNull() }) } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 736a1b684..d7bea476a 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -12,7 +12,7 @@ import CryptomatorCloudAccessCore import Foundation public enum HubAuthenticationFlow { - case receivedExistingKey(Data) + case success(Data, [AnyHashable: Any]) case accessNotGranted case needsDeviceRegistration case licenseExceeded @@ -53,9 +53,10 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving var urlRequest = URLRequest(url: url) urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] let (data, response) = try await URLSession.shared.data(with: urlRequest) - switch (response as? HTTPURLResponse)?.statusCode { + let httpResponse = response as? HTTPURLResponse + switch httpResponse?.statusCode { case 200: - return .receivedExistingKey(data) + return .success(data, httpResponse?.allHeaderFields ?? [:]) case 402: return .licenseExceeded case 403: diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift index 1e37d9d2b..8269b4726 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationFlowDelegate.swift @@ -2,5 +2,11 @@ import CryptoKit import JOSESwift public protocol HubAuthenticationFlowDelegate: AnyObject { - func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async + func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async +} + +public struct HubUnlockResponse { + public let jwe: JWE + public let privateKey: P384.KeyAgreement.PrivateKey + public let subscriptionState: HubSubscriptionState } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index 7b2f59031..449b10bd3 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -1,4 +1,5 @@ import AppAuthCore +import CocoaLumberjackSwift import CryptoKit import CryptomatorCloudAccessCore import Foundation @@ -8,6 +9,8 @@ import UIKit public enum HubAuthenticationViewModelError: Error { case missingHubConfig case missingAuthState + case missingSubscriptionHeader + case unexpectedSubscriptionHeader } public class HubAuthenticationViewModel: ObservableObject { @@ -25,6 +28,10 @@ public class HubAuthenticationViewModel: ObservableObject { case needsAuthorization } + private enum Constants { + static var subscriptionState: String { "hub-subscription-state" } + } + @Published var authenticationFlowState: State = .userLogin @Published public var deviceName: String = UIDevice.current.name @@ -101,8 +108,8 @@ public class HubAuthenticationViewModel: ObservableObject { return } switch authFlow { - case let .receivedExistingKey(data): - await receivedExistingKey(data: data) + case let .success(data, header): + await receivedExistingKey(data: data, header: header) case .accessNotGranted: await setState(to: .accessNotGranted) case .needsDeviceRegistration: @@ -112,17 +119,22 @@ public class HubAuthenticationViewModel: ObservableObject { } } - private func receivedExistingKey(data: Data) async { + private func receivedExistingKey(data: Data, header: [AnyHashable: Any]) async { let privateKey: P384.KeyAgreement.PrivateKey let jwe: JWE + let subscriptionState: HubSubscriptionState do { privateKey = try CryptomatorHubKeyProvider.shared.getPrivateKey() jwe = try JWE(compactSerialization: data) + subscriptionState = try getSubscriptionState(from: header) } catch { await setStateToErrorState(with: error) return } - await delegate?.receivedExistingKey(jwe: jwe, privateKey: privateKey) + let response = HubUnlockResponse(jwe: jwe, + privateKey: privateKey, + subscriptionState: subscriptionState) + await delegate?.didSuccessfullyRemoteUnlock(response) } @MainActor @@ -133,4 +145,20 @@ public class HubAuthenticationViewModel: ObservableObject { private func setStateToErrorState(with error: Error) async { await setState(to: .error(description: error.localizedDescription)) } + + private func getSubscriptionState(from header: [AnyHashable: Any]) throws -> HubSubscriptionState { + guard let subscriptionStateValue = header[Constants.subscriptionState] as? String else { + DDLogError("Can't retrieve hub subscription state from header -> missing value") + throw HubAuthenticationViewModelError.missingSubscriptionHeader + } + switch subscriptionStateValue { + case "ACTIVE": + return .active + case "INACTIVE": + return .inactive + default: + DDLogError("Can't retrieve hub subscription state from header -> unexpected value") + throw HubAuthenticationViewModelError.unexpectedSubscriptionHeader + } + } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubRepository.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubRepository.swift new file mode 100644 index 000000000..f44ee2488 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubRepository.swift @@ -0,0 +1,72 @@ +import Dependencies +import Foundation +import GRDB + +public protocol HubRepository { + func save(_ vault: HubVault) throws + func getHubVault(vaultID: String) throws -> HubVault? +} + +public struct HubVault: Equatable { + public let vaultUID: String + public let subscriptionState: HubSubscriptionState +} + +private struct HubVaultRow: Codable, Equatable, PersistableRecord, FetchableRecord { + public static let databaseTableName = "hubVaultAccount" + + let vaultUID: String + let subscriptionState: HubSubscriptionState + + init(from vault: HubVault) { + self.vaultUID = vault.vaultUID + self.subscriptionState = vault.subscriptionState + } + + func toHubVault() -> HubVault { + HubVault(vaultUID: vaultUID, subscriptionState: subscriptionState) + } + + enum Columns: String, ColumnExpression { + case vaultUID, subscriptionState + } + + public func encode(to container: inout PersistenceContainer) { + container[Columns.vaultUID] = vaultUID + container[Columns.subscriptionState] = subscriptionState + } +} + +extension HubSubscriptionState: DatabaseValueConvertible {} + +public extension DependencyValues { + var hubRepository: HubRepository { + get { self[HubRepositoryKey.self] } + set { self[HubRepositoryKey.self] = newValue } + } +} + +private enum HubRepositoryKey: DependencyKey { + static var liveValue: HubRepository = HubDBRepository() + #if DEBUG + static var testValue: HubRepository = HubRepositoryMock() + #endif +} + +public class HubDBRepository: HubRepository { + @Dependency(\.database) private var database + + public func save(_ vault: HubVault) throws { + let row = HubVaultRow(from: vault) + try database.write { db in + try row.save(db) + } + } + + public func getHubVault(vaultID: String) throws -> HubVault? { + let row = try database.read { db in + try HubVaultRow.fetchOne(db, key: vaultID) + } + return row?.toHubVault() + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubSubscriptionState.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubSubscriptionState.swift new file mode 100644 index 000000000..daf4d3185 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubSubscriptionState.swift @@ -0,0 +1,4 @@ +public enum HubSubscriptionState: String, Codable { + case active + case inactive +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift index 9ecb8dbd3..9249a61ec 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift @@ -2,6 +2,7 @@ import AppAuthCore import CryptoKit import CryptomatorCloudAccessCore import CryptomatorCryptoLib +import Dependencies import JOSESwift import SwiftUI import UIKit @@ -15,6 +16,7 @@ public final class HubXPCLoginCoordinator: Coordinator { let hubAuthenticator: HubAuthenticating public let onUnlocked: () -> Void public let onErrorAlertDismissed: () -> Void + @Dependency(\.hubRepository) private var hubRepository public init(navigationController: UINavigationController, domain: NSFileProviderDomain, @@ -42,25 +44,26 @@ public final class HubXPCLoginCoordinator: Coordinator { } extension HubXPCLoginCoordinator: HubAuthenticationFlowDelegate { - public func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async { + public func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async { let masterkey: Masterkey do { - masterkey = try JWEHelper.decrypt(jwe: jwe, with: privateKey) + masterkey = try JWEHelper.decrypt(jwe: response.jwe, with: response.privateKey) } catch { handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed) return } - let xpc: XPC do { - xpc = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) + let xpc: XPC = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) defer { fileProviderConnector.invalidateXPC(xpc) } try await xpc.proxy.unlockVault(rawKey: masterkey.rawKey).getValue() - fileProviderConnector.invalidateXPC(xpc) + let hubVault = HubVault(vaultUID: domain.identifier.rawValue, subscriptionState: response.subscriptionState) + try hubRepository.save(hubVault) onUnlocked() } catch { handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed) + return } } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/HubRepositoryMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/HubRepositoryMock.swift new file mode 100644 index 000000000..92e0d7896 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/HubRepositoryMock.swift @@ -0,0 +1,53 @@ +import Foundation + +#if DEBUG + +// MARK: - HubRepositoryMock - + +final class HubRepositoryMock: HubRepository { + // MARK: - save + + var saveThrowableError: Error? + var saveCallsCount = 0 + var saveCalled: Bool { + saveCallsCount > 0 + } + + var saveReceivedVault: HubVault? + var saveReceivedInvocations: [HubVault] = [] + var saveClosure: ((HubVault) throws -> Void)? + + func save(_ vault: HubVault) throws { + if let error = saveThrowableError { + throw error + } + saveCallsCount += 1 + saveReceivedVault = vault + saveReceivedInvocations.append(vault) + try saveClosure?(vault) + } + + // MARK: - getHubVault + + var getHubVaultVaultIDThrowableError: Error? + var getHubVaultVaultIDCallsCount = 0 + var getHubVaultVaultIDCalled: Bool { + getHubVaultVaultIDCallsCount > 0 + } + + var getHubVaultVaultIDReceivedVaultID: String? + var getHubVaultVaultIDReceivedInvocations: [String] = [] + var getHubVaultVaultIDReturnValue: HubVault? + var getHubVaultVaultIDClosure: ((String) throws -> HubVault?)? + + func getHubVault(vaultID: String) throws -> HubVault? { + if let error = getHubVaultVaultIDThrowableError { + throw error + } + getHubVaultVaultIDCallsCount += 1 + getHubVaultVaultIDReceivedVaultID = vaultID + getHubVaultVaultIDReceivedInvocations.append(vaultID) + return try getHubVaultVaultIDClosure.map({ try $0(vaultID) }) ?? getHubVaultVaultIDReturnValue + } +} +#endif diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubDBRepositoryTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubDBRepositoryTests.swift new file mode 100644 index 000000000..211b2f87f --- /dev/null +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubDBRepositoryTests.swift @@ -0,0 +1,89 @@ +import GRDB +import XCTest +@testable import CryptomatorCommonCore + +final class HubDBRepositoryTests: XCTestCase { + private var inMemoryDB: DatabaseQueue! + private var repository: HubDBRepository! + private var vaultAccountManager: VaultAccountManager! + private var cloudAccountManager: CloudProviderAccountManager! + + override func setUpWithError() throws { + repository = HubDBRepository() + vaultAccountManager = VaultAccountDBManager() + cloudAccountManager = CloudProviderAccountDBManager() + } + + func testSaveAndRetrieve() throws { + // GIVEN + // a cloud account has been created + let cloudAccount = CloudProviderAccount(accountUID: "", cloudProviderType: .dropbox) + try cloudAccountManager.saveNewAccount(cloudAccount) + + // and a vault account has been created + let vaultID = "123456789" + let vaultAccount = VaultAccount(vaultUID: vaultID, delegateAccountUID: "", vaultPath: .init(""), vaultName: "") + try vaultAccountManager.saveNewAccount(vaultAccount) + + // WHEN + // saving a hub vault + let vault = HubVault(vaultUID: vaultID, subscriptionState: .active) + try repository.save(vault) + + // THEN + // it can be retrieved + let retrievedVault = try repository.getHubVault(vaultID: vaultID) + XCTAssertEqual(vault, retrievedVault) + } + + func testSaveToUpdate() throws { + // GIVEN + // a cloud account has been created + let cloudAccount = CloudProviderAccount(accountUID: "", cloudProviderType: .dropbox) + try cloudAccountManager.saveNewAccount(cloudAccount) + + // and a vault account has been created + let vaultID = "123456789" + let vaultAccount = VaultAccount(vaultUID: vaultID, delegateAccountUID: "", vaultPath: .init(""), vaultName: "") + try vaultAccountManager.saveNewAccount(vaultAccount) + + // WHEN + // saving a hub vault + let initialVault = HubVault(vaultUID: vaultID, subscriptionState: .active) + try repository.save(initialVault) + + // and saving the hub vault with the same vault ID but a changed subscription state + let updatedVault = HubVault(vaultUID: vaultID, subscriptionState: .inactive) + try repository.save(updatedVault) + + // THEN + // it the updated version can be retrieved + let retrievedVault = try repository.getHubVault(vaultID: vaultID) + XCTAssertEqual(updatedVault, retrievedVault) + } + + func testDeleteVaultAccountAlsoDeletesHubVault() throws { + // GIVEN + // a cloud account has been created + let cloudAccount = CloudProviderAccount(accountUID: "", cloudProviderType: .dropbox) + try cloudAccountManager.saveNewAccount(cloudAccount) + + // and a vault account has been created + let vaultID = "123456789" + let vaultAccount = VaultAccount(vaultUID: vaultID, delegateAccountUID: "", vaultPath: .init(""), vaultName: "") + try vaultAccountManager.saveNewAccount(vaultAccount) + + // and a hub vault has been created for the vault id + let vault = HubVault(vaultUID: vaultID, subscriptionState: .active) + try repository.save(vault) + + // WHEN + // the vault account gets deleted + try vaultAccountManager.removeAccount(with: vaultID) + + // THEN + // the hub vault account has been deleted and can not be retrieved + let retrievedVault = try repository.getHubVault(vaultID: vaultID) + XCTAssertNil(retrievedVault) + } +} diff --git a/CryptomatorFileProvider/DB/WorkingSetObserver.swift b/CryptomatorFileProvider/DB/WorkingSetObserver.swift index 8b35d1d62..b4f411807 100644 --- a/CryptomatorFileProvider/DB/WorkingSetObserver.swift +++ b/CryptomatorFileProvider/DB/WorkingSetObserver.swift @@ -7,6 +7,7 @@ // import CocoaLumberjackSwift +import Dependencies import FileProvider import Foundation import GRDB @@ -23,8 +24,13 @@ class WorkingSetObserver: WorkingSetObserving { private let notificator: FileProviderNotificatorType private var currentWorkingSetItems = Set() private let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, database: DatabaseReader, notificator: FileProviderNotificatorType, uploadTaskManager: UploadTaskManager, cachedFileManager: CachedFileManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + database: DatabaseReader, + notificator: FileProviderNotificatorType, + uploadTaskManager: UploadTaskManager, + cachedFileManager: CachedFileManager) { self.domainIdentifier = domainIdentifier self.database = database self.notificator = notificator diff --git a/CryptomatorFileProvider/FileProviderAdapter.swift b/CryptomatorFileProvider/FileProviderAdapter.swift index ce2564f5d..2ff7e112c 100644 --- a/CryptomatorFileProvider/FileProviderAdapter.swift +++ b/CryptomatorFileProvider/FileProviderAdapter.swift @@ -74,6 +74,7 @@ public class FileProviderAdapter: FileProviderAdapterType { private let domainIdentifier: NSFileProviderDomainIdentifier private let fileCoordinator: NSFileCoordinator private let taskRegistrator: SessionTaskRegistrator + @Dependency(\.permissionProvider) private var permissionProvider init(domainIdentifier: NSFileProviderDomainIdentifier, uploadTaskManager: UploadTaskManager, diff --git a/CryptomatorFileProvider/FileProviderAdapterManager.swift b/CryptomatorFileProvider/FileProviderAdapterManager.swift index 660bf9626..d53e08185 100644 --- a/CryptomatorFileProvider/FileProviderAdapterManager.swift +++ b/CryptomatorFileProvider/FileProviderAdapterManager.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import GRDB @@ -32,12 +33,27 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { private let notificatorManager: FileProviderNotificatorManagerType private let queue = DispatchQueue(label: "FileProviderAdapterManager", qos: .userInitiated) private let providerIdentifier: String + @Dependency(\.permissionProvider) private var permissionProvider convenience init() { - self.init(masterkeyCacheManager: MasterkeyCacheKeychainManager.shared, vaultKeepUnlockedHelper: VaultKeepUnlockedManager.shared, vaultKeepUnlockedSettings: VaultKeepUnlockedManager.shared, vaultManager: VaultDBManager.shared, adapterCache: FileProviderAdapterCache(), notificatorManager: FileProviderNotificatorManager.shared, unlockMonitor: UnlockMonitor(), providerIdentifier: NSFileProviderManager.default.providerIdentifier) + self.init(masterkeyCacheManager: MasterkeyCacheKeychainManager.shared, + vaultKeepUnlockedHelper: VaultKeepUnlockedManager.shared, + vaultKeepUnlockedSettings: VaultKeepUnlockedManager.shared, + vaultManager: VaultDBManager.shared, + adapterCache: FileProviderAdapterCache(), + notificatorManager: FileProviderNotificatorManager.shared, + unlockMonitor: UnlockMonitor(), + providerIdentifier: NSFileProviderManager.default.providerIdentifier) } - init(masterkeyCacheManager: MasterkeyCacheManager, vaultKeepUnlockedHelper: VaultKeepUnlockedHelper, vaultKeepUnlockedSettings: VaultKeepUnlockedSettings, vaultManager: VaultManager, adapterCache: FileProviderAdapterCacheType, notificatorManager: FileProviderNotificatorManagerType, unlockMonitor: UnlockMonitorType, providerIdentifier: String) { + init(masterkeyCacheManager: MasterkeyCacheManager, + vaultKeepUnlockedHelper: VaultKeepUnlockedHelper, + vaultKeepUnlockedSettings: VaultKeepUnlockedSettings, + vaultManager: VaultManager, + adapterCache: FileProviderAdapterCacheType, + notificatorManager: FileProviderNotificatorManagerType, + unlockMonitor: UnlockMonitorType, + providerIdentifier: String) { self.masterkeyCacheManager = masterkeyCacheManager self.vaultKeepUnlockedHelper = vaultKeepUnlockedHelper self.vaultKeepUnlockedSettings = vaultKeepUnlockedSettings @@ -190,7 +206,12 @@ public class FileProviderAdapterManager: FileProviderAdapterProviding { notificator: notificator, localURLProvider: delegate, taskRegistrator: taskRegistrator) - let workingSetObserver = WorkingSetObserver(domainIdentifier: domainIdentifier, database: database, notificator: notificator, uploadTaskManager: uploadTaskManager, cachedFileManager: cachedFileManager) + + let workingSetObserver = WorkingSetObserver(domainIdentifier: domainIdentifier, + database: database, + notificator: notificator, + uploadTaskManager: uploadTaskManager, + cachedFileManager: cachedFileManager) workingSetObserver.startObservation() return AdapterCacheItem(adapter: adapter, maintenanceManager: maintenanceManager, workingSetObserver: workingSetObserver) } diff --git a/CryptomatorFileProvider/FileProviderItem.swift b/CryptomatorFileProvider/FileProviderItem.swift index 64c822b6f..2b0bc5955 100644 --- a/CryptomatorFileProvider/FileProviderItem.swift +++ b/CryptomatorFileProvider/FileProviderItem.swift @@ -24,6 +24,7 @@ public class FileProviderItem: NSObject, NSFileProviderItem { let localURL: URL? let domainIdentifier: NSFileProviderDomainIdentifier @Dependency(\.fullVersionChecker) private var fullVersionChecker + @Dependency(\.permissionProvider) private var permissionProvider init(metadata: ItemMetadata, domainIdentifier: NSFileProviderDomainIdentifier, newestVersionLocallyCached: Bool = false, localURL: URL? = nil, error: Error? = nil) { self.metadata = metadata @@ -50,19 +51,7 @@ public class FileProviderItem: NSObject, NSFileProviderItem { } public var capabilities: NSFileProviderItemCapabilities { - if metadata.statusCode == .uploadError { - return .allowsDeleting - } - if !fullVersionChecker.isFullVersion { - return FileProviderItem.readOnlyCapabilities - } - if metadata.type == .folder { - return [.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] - } - if metadata.statusCode == .isUploading { - return .allowsReading - } - return [.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + return permissionProvider.getPermissions(for: metadata, at: domainIdentifier) } public var filename: String { diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift index c3feed4e7..6e1c23446 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/DownloadTaskExecutor.swift @@ -8,6 +8,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore +import Dependencies import Foundation import Promises @@ -30,8 +31,13 @@ class DownloadTaskExecutor: WorkflowMiddleware { private let downloadTaskManager: DownloadTaskManager private let provider: CloudProvider private let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager, downloadTaskManager: DownloadTaskManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + itemMetadataManager: ItemMetadataManager, + cachedFileManager: CachedFileManager, + downloadTaskManager: DownloadTaskManager) { self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift index 23235e3b6..fd2623508 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/FolderCreationTaskExecutor.swift @@ -29,7 +29,9 @@ class FolderCreationTaskExecutor: WorkflowMiddleware { private let provider: CloudProvider private let domainIdentifier: NSFileProviderDomainIdentifier - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + itemMetadataManager: ItemMetadataManager) { self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager @@ -53,11 +55,13 @@ class FolderCreationTaskExecutor: WorkflowMiddleware { assert(itemMetadata.id != nil) assert(itemMetadata.type == .folder) - return provider.createFolder(at: itemMetadata.cloudPath).then { _ -> FileProviderItem in + return provider.createFolder(at: itemMetadata.cloudPath).then { [domainIdentifier, itemMetadataManager] _ -> FileProviderItem in itemMetadata.statusCode = .isUploaded itemMetadata.isPlaceholderItem = false - try self.itemMetadataManager.updateMetadata(itemMetadata) - return FileProviderItem(metadata: itemMetadata, domainIdentifier: self.domainIdentifier, newestVersionLocallyCached: true) + try itemMetadataManager.updateMetadata(itemMetadata) + return FileProviderItem(metadata: itemMetadata, + domainIdentifier: domainIdentifier, + newestVersionLocallyCached: true) } } } diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift index db91a48d7..5d4e96be5 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/ItemEnumerationTaskExecutor.swift @@ -37,7 +37,15 @@ class ItemEnumerationTaskExecutor: WorkflowMiddleware { private let provider: CloudProvider private let domainIdentifier: NSFileProviderDomainIdentifier - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager, uploadTaskManager: UploadTaskManager, reparentTaskManager: ReparentTaskManager, deletionTaskManager: DeletionTaskManager, itemEnumerationTaskManager: ItemEnumerationTaskManager, deleteItemHelper: DeleteItemHelper) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + itemMetadataManager: ItemMetadataManager, + cachedFileManager: CachedFileManager, + uploadTaskManager: UploadTaskManager, + reparentTaskManager: ReparentTaskManager, + deletionTaskManager: DeletionTaskManager, + itemEnumerationTaskManager: ItemEnumerationTaskManager, + deleteItemHelper: DeleteItemHelper) { self.domainIdentifier = domainIdentifier self.provider = provider self.itemMetadataManager = itemMetadataManager diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift index 6593c372a..7b85d9a3c 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/ReparentTaskExecutor.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import Dependencies import FileProvider import Foundation import Promises @@ -30,8 +31,13 @@ class ReparentTaskExecutor: WorkflowMiddleware { private let itemMetadataManager: ItemMetadataManager private let cachedFileManager: CachedFileManager private let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, reparentTaskManager: ReparentTaskManager, itemMetadataManager: ItemMetadataManager, cachedFileManager: CachedFileManager) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + reparentTaskManager: ReparentTaskManager, + itemMetadataManager: ItemMetadataManager, + cachedFileManager: CachedFileManager) { self.domainIdentifier = domainIdentifier self.provider = provider self.reparentTaskManager = reparentTaskManager diff --git a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift index a670cc153..2fc5abf38 100644 --- a/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift +++ b/CryptomatorFileProvider/Middleware/TaskExecutor/UploadTaskExecutor.swift @@ -8,6 +8,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore +import Dependencies import FileProvider import Foundation import Promises @@ -32,8 +33,14 @@ class UploadTaskExecutor: WorkflowMiddleware { let uploadTaskManager: UploadTaskManager let domainIdentifier: NSFileProviderDomainIdentifier let progressManager: ProgressManager + @Dependency(\.permissionProvider) private var permissionProvider - init(domainIdentifier: NSFileProviderDomainIdentifier, provider: CloudProvider, cachedFileManager: CachedFileManager, itemMetadataManager: ItemMetadataManager, uploadTaskManager: UploadTaskManager, progressManager: ProgressManager = InMemoryProgressManager.shared) { + init(domainIdentifier: NSFileProviderDomainIdentifier, + provider: CloudProvider, + cachedFileManager: CachedFileManager, + itemMetadataManager: ItemMetadataManager, + uploadTaskManager: UploadTaskManager, + progressManager: ProgressManager = InMemoryProgressManager.shared) { self.domainIdentifier = domainIdentifier self.provider = provider self.cachedFileManager = cachedFileManager diff --git a/CryptomatorFileProvider/PermissionProvider.swift b/CryptomatorFileProvider/PermissionProvider.swift new file mode 100644 index 000000000..bcdbee887 --- /dev/null +++ b/CryptomatorFileProvider/PermissionProvider.swift @@ -0,0 +1,127 @@ +// +// PermissionProvider.swift +// CryptomatorFileProvider +// +// Created by Philipp Schmid on 18.09.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import CocoaLumberjackSwift +import CryptomatorCommonCore +import Dependencies +import FileProvider +import Foundation + +public protocol PermissionProvider { + /** + Returns the permission for a given `item` at a given `domain`. + + The following restrictions can apply to any item: + - in case of an upload error it's only allowed to delete the item. + - in case of a free version only reading is allowed, except if the vault belongs to Cryptomator Hub and it has an active subscription state. + + The following capabilities hold for files: + - reading + - adding sub items + - content enumerating + - deleting + - renaming + - reparenting + + - Note: In case of an running upload, i.e. a creation of the folder in the cloud, the capabilities do not get restricted except if something listed above restricts all items of the vault. + + The following capabilities hold for files: + - reading + - writing + - deleting + - renaming + - reparenting + - Note: In case of an running upload for a file it's only allowed to read the item. To prevent additional modifications. + + */ + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities +} + +private enum PermissionProviderKey: DependencyKey { + static let liveValue: PermissionProvider = PermissionProviderImpl() + #if DEBUG + static let testValue: PermissionProvider = UnimplementedPermissionProvider() + #endif +} + +extension DependencyValues { + var permissionProvider: PermissionProvider { + get { self[PermissionProviderKey.self] } + set { self[PermissionProviderKey.self] = newValue } + } +} + +struct PermissionProviderImpl: PermissionProvider { + @Dependency(\.fullVersionChecker) private var fullVersionChecker + @Dependency(\.hubRepository) private var hubRepository + + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities { + if item.statusCode == .uploadError { + return .allowsDeleting + } + + let vaultID = domain.rawValue + let hubSubscriptionState: HubSubscriptionState? + do { + let hubVault = try hubRepository.getHubVault(vaultID: vaultID) + hubSubscriptionState = hubVault?.subscriptionState + } catch { + hubSubscriptionState = nil + DDLogError("Failed to retrieve possible hub vault for with id: \(vaultID)") + } + + if !fullVersionChecker.isFullVersion && hubSubscriptionState != .active { + return FileProviderItem.readOnlyCapabilities + } + if item.type == .folder { + return [.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + } + if item.statusCode == .isUploading { + return FileProviderItem.readOnlyCapabilities + } + return [.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + } + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities { + if fullVersionChecker.isFullVersion { + return [.allowsAll] + } + guard let domain else { + return FileProviderItem.readOnlyCapabilities + } + let vaultID = domain.rawValue + let hubSubscriptionState: HubSubscriptionState? + do { + let hubVault = try hubRepository.getHubVault(vaultID: vaultID) + hubSubscriptionState = hubVault?.subscriptionState + } catch { + hubSubscriptionState = nil + DDLogError("Failed to retrieve possible hub vault for with id: \(vaultID)") + } + switch hubSubscriptionState { + case .active: + return [.allowsAll] + case .inactive, nil: + return FileProviderItem.readOnlyCapabilities + } + } +} + +#if DEBUG +struct UnimplementedPermissionProvider: PermissionProvider { + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities { + unimplemented("\(Self.self).getPermissions", placeholder: .allowsReading) + } + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities { + unimplemented("\(Self.self).getPermissionsForRootItem", placeholder: .allowsReading) + } +} +#endif diff --git a/CryptomatorFileProvider/RootFileProviderItem.swift b/CryptomatorFileProvider/RootFileProviderItem.swift index c46984e8b..fafb4dafb 100644 --- a/CryptomatorFileProvider/RootFileProviderItem.swift +++ b/CryptomatorFileProvider/RootFileProviderItem.swift @@ -19,12 +19,13 @@ public class RootFileProviderItem: NSObject, NSFileProviderItem { public let typeIdentifier = kUTTypeFolder as String public let documentSize: NSNumber? = nil public var capabilities: NSFileProviderItemCapabilities { - if fullVersionChecker.isFullVersion { - return [.allowsAll] - } else { - return FileProviderItem.readOnlyCapabilities - } + return permissionProvider.getPermissionsForRootItem(at: domain?.identifier) } - @Dependency(\.fullVersionChecker) private var fullVersionChecker + private let domain: NSFileProviderDomain? + @Dependency(\.permissionProvider) private var permissionProvider + + public init(domain: NSFileProviderDomain?) { + self.domain = domain + } } diff --git a/CryptomatorFileProvider/Workflow/WorkflowFactory.swift b/CryptomatorFileProvider/Workflow/WorkflowFactory.swift index 2ebe1387e..17a92c359 100644 --- a/CryptomatorFileProvider/Workflow/WorkflowFactory.swift +++ b/CryptomatorFileProvider/Workflow/WorkflowFactory.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import Dependencies import FileProvider import Foundation @@ -21,6 +22,7 @@ struct WorkflowFactory { let downloadTaskManager: DownloadTaskManager let dependencyFactory = WorkflowDependencyFactory() let domainIdentifier: NSFileProviderDomainIdentifier + @Dependency(\.permissionProvider) private var permissionProvider func createWorkflow(for deletionTask: DeletionTask) -> Workflow { let taskExecutor = DeletionTaskExecutor(provider: provider, itemMetadataManager: itemMetadataManager) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift index a7882b36f..ca98e991a 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterEnumerateItemTests.swift @@ -9,6 +9,7 @@ import CryptomatorCloudAccessCore import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderAdapterEnumerateItemTests: FileProviderAdapterTestCase { override func setUpWithError() throws { @@ -34,6 +35,9 @@ class FileProviderAdapterEnumerateItemTests: FileProviderAdapterTestCase { ItemMetadata(id: 3, name: "TestFolder", type: .file, size: nil, parentID: 4, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Foo/TestFolder"), isPlaceholderItem: false, isCandidateForCacheCleanup: false, favoriteRank: 1, tagData: nil) ] metadataManagerMock.workingSetMetadata = mockMetadata + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let expectation = XCTestExpectation() adapter.enumerateItems(for: .workingSet, withPageToken: nil).then { itemList in XCTAssertEqual(mockMetadata.map { FileProviderItem(metadata: $0, domainIdentifier: .test) }, itemList.items) diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift index 2c83892f7..e2f4fb8cd 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterImportDocumentTests.swift @@ -12,6 +12,7 @@ import Promises import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { let itemID: Int64 = 2 @@ -26,6 +27,9 @@ class FileProviderAdapterImportDocumentTests: FileProviderAdapterTestCase { // MARK: LocalItemImport func testLocalItemImport() throws { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let fileURL = tmpDirectory.appendingPathComponent("ItemToBeImported.txt", isDirectory: false) let fileContent = "TestContent" try fileContent.write(to: fileURL, atomically: true, encoding: .utf8) diff --git a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift index 131daf148..912b7bc19 100644 --- a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift +++ b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift @@ -13,6 +13,7 @@ import Promises import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderEnumeratorTestCase: XCTestCase { var enumerationObserverMock: NSFileProviderEnumerationObserverMock! @@ -50,6 +51,10 @@ class FileProviderEnumeratorTestCase: XCTestCase { } func assertChangeObserverUpdated(deletedItems: [NSFileProviderItemIdentifier], updatedItems: [FileProviderItem], currentSyncAnchor: NSFileProviderSyncAnchor) { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + XCTAssertEqual([deletedItems], changeObserverMock.didDeleteItemsWithIdentifiersReceivedInvocations) let receivedUpdatedItems = changeObserverMock.didUpdateReceivedInvocations as? [[FileProviderItem]] XCTAssertEqual([updatedItems], receivedUpdatedItems) @@ -179,6 +184,10 @@ class FileProviderEnumeratorTests: FileProviderEnumeratorTestCase { } private func assertEnumerateItemObserverSucceeded(itemList: FileProviderItemList) { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + XCTAssertEqual([itemList.nextPageToken], enumerationObserverMock.finishEnumeratingUpToReceivedInvocations) let receivedInvocations = enumerationObserverMock.didEnumerateReceivedInvocations as? [[FileProviderItem]] XCTAssertEqual([items], receivedInvocations) diff --git a/CryptomatorFileProviderTests/FileProviderItemTests.swift b/CryptomatorFileProviderTests/FileProviderItemTests.swift index 1c4af1510..e8829ef6c 100644 --- a/CryptomatorFileProviderTests/FileProviderItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderItemTests.swift @@ -108,59 +108,19 @@ class FileProviderItemTests: XCTestCase { // MARK: Capabilities - func testUploadingItemRestrictsCapabilityToRead() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = true - DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) + func testCapabilitiesArePassedThroughFromPermissionProvider() { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) let cloudPath = CloudPath("/test.txt") let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, item.capabilities) - } - - func testUploadingFolderDoesNotRestrictCapabilities() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = true - DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) - - let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) - XCTAssertEqual([.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], item.capabilities) - } - - func testCapabilitiesForRestrictedVersion() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = false - DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) - - let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, item.capabilities) - } - - func testFailedUploadItemCapabilitiesForRestrictedVersion() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = false - DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) - let cloudPath = CloudPath("/test.txt") - let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, item.capabilities) - } - - func testFailedUploadFolderCapabilitiesForRestrictedVersion() { - let fullVersionCheckerMock = FullVersionCheckerMock() - fullVersionCheckerMock.isFullVersion = false - DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) - - let cloudPath = CloudPath("/test") - let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) - XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, item.capabilities) + let capabilities: [NSFileProviderItemCapabilities] = [.allowsAddingSubItems, .allowsContentEnumerating, .allowsDeleting, .allowsReading, .allowsReparenting, .allowsWriting] + for capability in capabilities { + permissionProviderMock.getPermissionsForAtReturnValue = capability + XCTAssertEqual(capability, item.capabilities) + } } // MARK: Evict File From Cache Action diff --git a/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift b/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift index 650d54507..4e54026a2 100644 --- a/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift +++ b/CryptomatorFileProviderTests/FileProviderNotificatorTests.swift @@ -9,6 +9,7 @@ import CryptomatorCloudAccessCore import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies @available(iOS 14.0, *) class FileProviderNotificatorTests: XCTestCase { @@ -97,6 +98,11 @@ class FileProviderNotificatorTests: XCTestCase { }) let actualItems = notificator.popUpdateContainerItems() as? [FileProviderItem] + + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + XCTAssertEqual([updatedItem], actualItems?.sorted()) XCTAssert(notificator.popUpdateWorkingSetItems().isEmpty) XCTAssert(notificator.getItemIdentifiersToDeleteFromWorkingSet().isEmpty) @@ -109,6 +115,9 @@ class FileProviderNotificatorTests: XCTestCase { } private func assertUpdateWorkingSetHasUpdatedItems() { + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let actualItems = notificator.popUpdateWorkingSetItems() as? [FileProviderItem] XCTAssertEqual(updatedItems.sorted(), actualItems?.sorted()) } diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift index 05dc2be97..1c4ca9b14 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/ItemEnumerationTaskTests.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import Promises import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { override func setUpWithError() throws { @@ -201,6 +202,7 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { // MARK: Folder + // swiftlint:disable:next function_body_length func testFolderEnumeration() throws { let expectation = XCTestExpectation(description: "Folder Enumeration") @@ -222,6 +224,9 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { let expectedSubFolderFileProviderItems = expectedItemMetadataInsideSubFolder.map { FileProviderItem(metadata: $0, domainIdentifier: .test) } let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading taskExecutor.execute(task: enumerationTask).then { fileProviderItemList -> FileProviderItem in XCTAssertEqual(5, fileProviderItemList.items.count) @@ -283,6 +288,10 @@ class ItemEnumerationTaskTests: CloudTaskExecutorTestCase { FileProviderItem(metadata: ItemMetadata(id: 6, name: "File 4", type: .file, size: 14, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/File 4"), isPlaceholderItem: false), domainIdentifier: .test), FileProviderItem(metadata: ItemMetadata(id: 7, name: "NewFileFromCloud", type: .file, size: 24, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/NewFileFromCloud"), isPlaceholderItem: false), domainIdentifier: .test)] + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading + let taskExecutor = ItemEnumerationTaskExecutor(domainIdentifier: .test, provider: cloudProviderMock, itemMetadataManager: metadataManagerMock, cachedFileManager: cachedFileManagerMock, uploadTaskManager: uploadTaskManagerMock, reparentTaskManager: reparentTaskManagerMock, deletionTaskManager: deletionTaskManagerMock, itemEnumerationTaskManager: itemEnumerationTaskManagerMock, deleteItemHelper: deleteItemHelper) taskExecutor.execute(task: enumerationTask).then { fileProviderItemList -> Promise in diff --git a/CryptomatorFileProviderTests/Mocks/PermissionProviderMock.swift b/CryptomatorFileProviderTests/Mocks/PermissionProviderMock.swift new file mode 100644 index 000000000..7571ceee7 --- /dev/null +++ b/CryptomatorFileProviderTests/Mocks/PermissionProviderMock.swift @@ -0,0 +1,51 @@ +// +// PermissionProviderMock.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 19.09.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import CryptomatorFileProvider +import FileProvider +import Foundation + +final class PermissionProviderMock: PermissionProvider { + // MARK: - getPermissions + + var getPermissionsForAtCallsCount = 0 + var getPermissionsForAtCalled: Bool { + getPermissionsForAtCallsCount > 0 + } + + var getPermissionsForAtReceivedArguments: (item: ItemMetadata, domain: NSFileProviderDomainIdentifier)? + var getPermissionsForAtReceivedInvocations: [(item: ItemMetadata, domain: NSFileProviderDomainIdentifier)] = [] + var getPermissionsForAtReturnValue: NSFileProviderItemCapabilities! + var getPermissionsForAtClosure: ((ItemMetadata, NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities)? + + func getPermissions(for item: ItemMetadata, at domain: NSFileProviderDomainIdentifier) -> NSFileProviderItemCapabilities { + getPermissionsForAtCallsCount += 1 + getPermissionsForAtReceivedArguments = (item: item, domain: domain) + getPermissionsForAtReceivedInvocations.append((item: item, domain: domain)) + return getPermissionsForAtClosure.map({ $0(item, domain) }) ?? getPermissionsForAtReturnValue + } + + // MARK: - getPermissionsForRootItem + + var getPermissionsForRootItemAtCallsCount = 0 + var getPermissionsForRootItemAtCalled: Bool { + getPermissionsForRootItemAtCallsCount > 0 + } + + var getPermissionsForRootItemAtReceivedDomain: NSFileProviderDomainIdentifier? + var getPermissionsForRootItemAtReceivedInvocations: [NSFileProviderDomainIdentifier?] = [] + var getPermissionsForRootItemAtReturnValue: NSFileProviderItemCapabilities! + var getPermissionsForRootItemAtClosure: ((NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities)? + + func getPermissionsForRootItem(at domain: NSFileProviderDomainIdentifier?) -> NSFileProviderItemCapabilities { + getPermissionsForRootItemAtCallsCount += 1 + getPermissionsForRootItemAtReceivedDomain = domain + getPermissionsForRootItemAtReceivedInvocations.append(domain) + return getPermissionsForRootItemAtClosure.map({ $0(domain) }) ?? getPermissionsForRootItemAtReturnValue + } +} diff --git a/CryptomatorFileProviderTests/PermissionProviderImplTests.swift b/CryptomatorFileProviderTests/PermissionProviderImplTests.swift new file mode 100644 index 000000000..67fdb5a8e --- /dev/null +++ b/CryptomatorFileProviderTests/PermissionProviderImplTests.swift @@ -0,0 +1,137 @@ +// +// PermissionProviderImplTests.swift +// CryptomatorFileProviderTests +// +// Created by Philipp Schmid on 19.09.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import XCTest +@testable import CryptomatorCommonCore +@testable import CryptomatorFileProvider +@testable import Dependencies + +final class PermissionProviderImplTests: XCTestCase { + private static let defaultFolderCapabilities: NSFileProviderItemCapabilities = [.allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting] + private var fullVersionCheckerMock: FullVersionCheckerMock! + private var hubRepositoryMock: HubRepositoryMock! + private var permissionProvider: PermissionProviderImpl! + + override func setUpWithError() throws { + fullVersionCheckerMock = FullVersionCheckerMock() + hubRepositoryMock = HubRepositoryMock() + DependencyValues.mockDependency(\.hubRepository, with: hubRepositoryMock) + DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) + permissionProvider = PermissionProviderImpl() + } + + // MARK: Full Version + + func testUploadingItemRestrictsCapabilityToRead() { + fullVersionCheckerMock.isFullVersion = true + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testUploadingFolderDoesNotRestrictCapabilities() { + fullVersionCheckerMock.isFullVersion = true + + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) + } + + func testCapabilitiesForRestrictedVersion() { + fullVersionCheckerMock.isFullVersion = false + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testFailedUploadItemCapabilitiesForRestrictedVersion() { + fullVersionCheckerMock.isFullVersion = false + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, actualCapabilities) + } + + func testFailedUploadFolderCapabilitiesForRestrictedVersion() { + fullVersionCheckerMock.isFullVersion = false + + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .uploadError, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, actualCapabilities) + } + + func testFullVersionNoActiveHubScriptionReturnsFullPermissionsForFile() { + fullVersionCheckerMock.isFullVersion = true + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .inactive) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual([.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], actualCapabilities) + } + + // MARK: Cryptomator Hub + + func testUploadingItemRestrictsCapabilityToReadWithActiveHubSubscription() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testNoFullVersionNoActiveHubSubscriptionRestrictsToReadOnly() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .inactive) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(NSFileProviderItemCapabilities.allowsReading, actualCapabilities) + } + + func testFolderCapabilitiesNoFullVersionActiveHubSubscription() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .folder, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) + } + + func testUploadingFolderDoesNotRestrictCapabilitiesForActiveHubSubsription() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test") + let metadata = ItemMetadata(id: 2, name: "test", type: .folder, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual(Self.defaultFolderCapabilities, actualCapabilities) + } + + func testNoFullVersionActiveHubScriptionReturnsFullPermissionsForFile() { + fullVersionCheckerMock.isFullVersion = false + hubRepositoryMock.getHubVaultVaultIDReturnValue = .init(vaultUID: "12345", subscriptionState: .active) + + let cloudPath = CloudPath("/test.txt") + let metadata = ItemMetadata(id: 2, name: "test.txt", type: .file, size: 100, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: cloudPath, isPlaceholderItem: false) + let actualCapabilities = permissionProvider.getPermissions(for: metadata, at: .test) + XCTAssertEqual([.allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting], actualCapabilities) + } +} diff --git a/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift index 2772c4de2..eee1b96de 100644 --- a/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift +++ b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift @@ -11,6 +11,7 @@ import Promises import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class CacheManagingServiceSourceTests: XCTestCase { var serviceSource: CacheManagingServiceSource! @@ -57,6 +58,9 @@ class CacheManagingServiceSourceTests: XCTestCase { let expectation = XCTestExpectation() let cacheManagerMock = CachedFileManagerMock() cacheManagerFactoryMock.createCachedFileManagerForReturnValue = cacheManagerMock + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading let domainIdentifier = NSFileProviderDomainIdentifier("Test-Domain") let itemID: Int64 = 2 let itemIdentifier = NSFileProviderItemIdentifier(domainIdentifier: domainIdentifier, itemID: itemID) diff --git a/CryptomatorFileProviderTests/WorkingSetObserverTests.swift b/CryptomatorFileProviderTests/WorkingSetObserverTests.swift index 034a0bca1..728b31357 100644 --- a/CryptomatorFileProviderTests/WorkingSetObserverTests.swift +++ b/CryptomatorFileProviderTests/WorkingSetObserverTests.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import GRDB import XCTest @testable import CryptomatorFileProvider +@testable import Dependencies class WorkingSetObserverTests: XCTestCase { var observer: WorkingSetObserver! @@ -31,6 +32,9 @@ class WorkingSetObserverTests: XCTestCase { XCTAssertEqual(1, notificatorMock.updateWorkingSetItemsCallsCount) let actualUpdatedItems = notificatorMock.updateWorkingSetItemsReceivedItems as? [FileProviderItem] + let permissionProviderMock = PermissionProviderMock() + DependencyValues.mockDependency(\.permissionProvider, with: permissionProviderMock) + permissionProviderMock.getPermissionsForAtReturnValue = .allowsReading XCTAssertEqual(updatedItems.sorted(), actualUpdatedItems?.sorted()) XCTAssertEqual(1, notificatorMock.refreshWorkingSetCallsCount) } diff --git a/FileProviderExtension/FileProviderExtension.swift b/FileProviderExtension/FileProviderExtension.swift index ee8f385b9..2458a4f48 100644 --- a/FileProviderExtension/FileProviderExtension.swift +++ b/FileProviderExtension/FileProviderExtension.swift @@ -69,7 +69,7 @@ class FileProviderExtension: NSFileProviderExtension { // resolve the given identifier to a record in the model DDLogDebug("FPExt: item(for: \(identifier)) called") if identifier == .rootContainer || identifier.rawValue == "File Provider Storage" || identifier.rawValue == domain?.identifier.rawValue { - return RootFileProviderItem() + return RootFileProviderItem(domain: domain) } let adapter = try getAdapterWithWrappedError() return try adapter.item(for: identifier) From 9921496bc927053c349e254d6218e81072d46ca8 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 15 Nov 2023 00:12:11 +0100 Subject: [PATCH 29/45] Updated hub authentication flow - moved business logic out of the coordinators - show ProgressHUD instead of custom loading screen - removed unnecessary login screen - show navigation bar title for all hub authentication screens --- .../AddVault/Hub/HubAddVaultCoordinator.swift | 56 +++++----- .../Hub/HubAuthenticationCoordinator.swift | 105 ++++++++++++++++++ .../Hub/HubAuthenticationView.swift | 7 +- .../Hub/HubAuthenticationViewController.swift | 22 +++- .../Hub/HubAuthenticationViewModel.swift | 61 +++++----- .../Hub/HubUserAuthenticator.swift | 17 +++ .../Hub/HubXPCLoginCoordinator.swift | 61 +++++----- .../AddHubVaultUnlockHandler.swift | 41 +++++++ .../UnlockHandler/HubVaultUnlockHandler.swift | 12 ++ .../HubXPCVaultUnlockHandler.swift | 41 +++++++ 10 files changed, 317 insertions(+), 106 deletions(-) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserAuthenticator.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/AddHubVaultUnlockHandler.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubVaultUnlockHandler.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubXPCVaultUnlockHandler.swift diff --git a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift index 9b8591d88..74a4aa367 100644 --- a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift +++ b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift @@ -45,42 +45,40 @@ class AddHubVaultCoordinator: Coordinator { } func start() { - let viewModel = HubAuthenticationViewModel(vaultConfig: downloadedVaultConfig.vaultConfig, - hubUserAuthenticator: self, - delegate: self) - let viewController = HubAuthenticationViewController(viewModel: viewModel) - navigationController.pushViewController(viewController, animated: true) + let unlockHandler = AddHubVaultUnlockHandler(vaultUID: vaultUID, + accountUID: accountUID, vaultItem: vaultItem, + downloadedVaultConfig: downloadedVaultConfig, + vaultManager: vaultManager, + delegate: self) + let child = HubAuthenticationCoordinator(navigationController: navigationController, + vaultConfig: downloadedVaultConfig.vaultConfig, + hubAuthenticator: hubAuthenticator, + unlockHandler: unlockHandler, + parent: self, + delegate: self) + childCoordinators.append(child) + child.start() } } -extension AddHubVaultCoordinator: HubAuthenticationFlowDelegate { - func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async { - let jwe = response.jwe - let privateKey = response.privateKey - let hubVault = ExistingHubVault(vaultUID: vaultUID, - delegateAccountUID: accountUID, - jweData: jwe.compactSerializedData, - privateKey: privateKey, - vaultItem: vaultItem, - downloadedVaultConfig: downloadedVaultConfig) - do { - try await vaultManager.addExistingHubVault(hubVault).getValue() - childDidFinish(self) - await showSuccessfullyAddedVault() - } catch { - DDLogError("Add existing Hub vault failed: \(error)") - handleError(error, for: navigationController) - } +extension AddHubVaultCoordinator: HubVaultUnlockHandlerDelegate { + func successfullyProcessedUnlockedVault() { + delegate?.showSuccessfullyAddedVault(withName: vaultItem.name, vaultUID: vaultUID) } - @MainActor - private func showSuccessfullyAddedVault() { - delegate?.showSuccessfullyAddedVault(withName: vaultItem.name, vaultUID: vaultUID) + func failedToProcessUnlockedVault(error: Error) { + handleError(error, for: navigationController, onOKTapped: { [weak self] in + self?.parentCoordinator?.childDidFinish(self) + }) } } -extension AddHubVaultCoordinator: HubUserLogin { - public func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState { - try await hubAuthenticator.authenticate(with: hubConfig, from: navigationController) +extension AddHubVaultCoordinator: HubAuthenticationCoordinatorDelegate { + func userDidCancelHubAuthentication() { + // do nothing as the user already sees the login screen again + } + + func userDismissedHubAuthenticationErrorMessage() { + // do nothing as the user already sees the login screen again } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift new file mode 100644 index 000000000..66f5d50f6 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift @@ -0,0 +1,105 @@ +import AppAuthCore +import CryptomatorCloudAccessCore +import SwiftUI +import UIKit + +public protocol HubAuthenticationCoordinatorDelegate: AnyObject { + @MainActor + func userDidCancelHubAuthentication() + + @MainActor + func userDismissedHubAuthenticationErrorMessage() +} + +public final class HubAuthenticationCoordinator: Coordinator { + public var childCoordinators = [Coordinator]() + public var navigationController: UINavigationController + public weak var parent: Coordinator? + + private let vaultConfig: UnverifiedVaultConfig + private let hubAuthenticator: HubAuthenticating + private var progressHUD: ProgressHUD? + private let unlockHandler: HubVaultUnlockHandler + private weak var delegate: HubAuthenticationCoordinatorDelegate? + + public init(navigationController: UINavigationController, + vaultConfig: UnverifiedVaultConfig, + hubAuthenticator: HubAuthenticating, + unlockHandler: HubVaultUnlockHandler, + parent: Coordinator?, + delegate: HubAuthenticationCoordinatorDelegate) { + self.navigationController = navigationController + self.vaultConfig = vaultConfig + self.hubAuthenticator = hubAuthenticator + self.unlockHandler = unlockHandler + self.parent = parent + self.delegate = delegate + } + + public func start() { + guard let hubConfig = vaultConfig.allegedHubConfig else { + handleError(HubAuthenticationViewModelError.missingHubConfig, for: navigationController, onOKTapped: { [weak self] in + guard let self else { return } + parent?.childDidFinish(self) + }) + return + } + Task { @MainActor in + let authenticator = HubUserAuthenticator(hubAuthenticator: hubAuthenticator, viewController: navigationController) + let authState: OIDAuthState + do { + authState = try await authenticator.authenticate(with: hubConfig) + } catch let error as NSError where error.domain == OIDGeneralErrorDomain && error.code == OIDErrorCode.userCanceledAuthorizationFlow.rawValue { + // do not show alert if user canceled it on purpose + delegate?.userDidCancelHubAuthentication() + parent?.childDidFinish(self) + return + } catch { + handleError(error, for: navigationController, onOKTapped: { [weak self] in + guard let self else { return } + delegate?.userDismissedHubAuthenticationErrorMessage() + parent?.childDidFinish(self) + }) + return + } + let viewModel = HubAuthenticationViewModel(authState: authState, + vaultConfig: vaultConfig, + unlockHandler: unlockHandler, + delegate: self) + await viewModel.continueToAccessCheck() + guard !viewModel.isLoggedIn else { + // Do not show the authentication view if the user already authenticated successfully + return + } + navigationController.setNavigationBarHidden(false, animated: false) + let viewController = HubAuthenticationViewController(viewModel: viewModel) + navigationController.pushViewController(viewController, animated: true) + } + } + + private func showProgressHUD() { + assert(progressHUD == nil, "showProgressHUD called although one is already shown") + progressHUD = ProgressHUD() + progressHUD?.show(presentingViewController: navigationController) + progressHUD?.showLoadingIndicator() + } + + private func hideProgressHUD() async { + await withCheckedContinuation { continuation in + progressHUD?.dismiss(animated: true, completion: { [weak self] in + continuation.resume() + self?.progressHUD = nil + }) + } + } +} + +extension HubAuthenticationCoordinator: HubAuthenticationViewModelDelegate { + public func hubAuthenticationViewModelWantsToShowLoadingIndicator() { + showProgressHUD() + } + + public func hubAuthenticationViewModelWantsToHideLoadingIndicator() async { + await hideProgressHUD() + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift index ac150cef4..adc7c43ff 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift @@ -20,15 +20,12 @@ public struct HubAuthenticationView: View { ) case .accessNotGranted: HubAccessNotGrantedView(onRefresh: { Task { await viewModel.refresh() }}) - case .loading: - ProgressView() - Text(LocalizedString.getValue("hubAuthentication.loading")) - case .userLogin: - HubLoginView(onLogin: { Task { await viewModel.login() }}) case .licenseExceeded: CryptomatorErrorView(text: LocalizedString.getValue("hubAuthentication.licenseExceeded")) case let .error(description): CryptomatorErrorView(text: description) + case .none: + EmptyView() } } .padding() diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewController.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewController.swift index e08a46a17..25152feb3 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewController.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewController.swift @@ -24,13 +24,9 @@ public class HubAuthenticationViewController: UIViewController { override public func viewDidLoad() { super.viewDidLoad() + title = LocalizedString.getValue("hubAuthentication.title") - viewModel.$authenticationFlowState - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] in - self?.updateToolbar(state: $0) - }) - .store(in: &cancellables) + setupToolBar() setupSwiftUIView() } @@ -43,6 +39,20 @@ public class HubAuthenticationViewController: UIViewController { NSLayoutConstraint.activate(child.view.constraints(equalTo: view)) } + private func setupToolBar() { + if let initialState = viewModel.authenticationFlowState { + updateToolbar(state: initialState) + } + + viewModel.$authenticationFlowState + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] in + self?.updateToolbar(state: $0) + }) + .store(in: &cancellables) + } + /** Updates the `UINavigationItem` based on the given `state`. - Note: This solution is far from ideal as we need to update the content of the tool bar in two places, i.e. in this method and inside the SwiftUI itself. Otherwise the behavior can differ when used inside a UINavigationController and a "SwiftUI native" `NavigationView`/ `NavigationStackView`. diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index 449b10bd3..4c060bccd 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -13,13 +13,19 @@ public enum HubAuthenticationViewModelError: Error { case unexpectedSubscriptionHeader } -public class HubAuthenticationViewModel: ObservableObject { +public protocol HubAuthenticationViewModelDelegate: AnyObject { + @MainActor + func hubAuthenticationViewModelWantsToShowLoadingIndicator() + + @MainActor + func hubAuthenticationViewModelWantsToHideLoadingIndicator() async +} + +public final class HubAuthenticationViewModel: ObservableObject { public enum State: Equatable { - case userLogin case accessNotGranted case licenseExceeded case deviceRegistration(DeviceRegistration) - case loading case error(description: String) } @@ -32,53 +38,37 @@ public class HubAuthenticationViewModel: ObservableObject { static var subscriptionState: String { "hub-subscription-state" } } - @Published var authenticationFlowState: State = .userLogin + @Published var authenticationFlowState: State? @Published public var deviceName: String = UIDevice.current.name + private(set) var isLoggedIn = false private let vaultConfig: UnverifiedVaultConfig private let deviceRegisteringService: HubDeviceRegistering private let hubKeyService: HubKeyReceiving - private let hubUserAuthenticator: HubUserLogin - private var authState: OIDAuthState? - private weak var delegate: HubAuthenticationFlowDelegate? + private let authState: OIDAuthState + private let unlockHandler: HubVaultUnlockHandler + private weak var delegate: HubAuthenticationViewModelDelegate? - public init(vaultConfig: UnverifiedVaultConfig, + public init(authState: OIDAuthState, + vaultConfig: UnverifiedVaultConfig, deviceRegisteringService: HubDeviceRegistering = CryptomatorHubAuthenticator.shared, - hubUserAuthenticator: HubUserLogin, hubKeyService: HubKeyReceiving = CryptomatorHubAuthenticator.shared, - delegate: HubAuthenticationFlowDelegate?) { + unlockHandler: HubVaultUnlockHandler, + delegate: HubAuthenticationViewModelDelegate) { + self.authState = authState self.vaultConfig = vaultConfig self.deviceRegisteringService = deviceRegisteringService - self.hubUserAuthenticator = hubUserAuthenticator self.hubKeyService = hubKeyService + self.unlockHandler = unlockHandler self.delegate = delegate } - public func login() async { - guard let hubConfig = vaultConfig.allegedHubConfig else { - await setStateToErrorState(with: HubAuthenticationViewModelError.missingHubConfig) - return - } - do { - authState = try await hubUserAuthenticator.authenticate(with: hubConfig) - await continueToAccessCheck() - } catch let error as NSError where error.domain == OIDGeneralErrorDomain && error.code == OIDErrorCode.userCanceledAuthorizationFlow.rawValue { - // ignore user cancellation - } catch { - await setStateToErrorState(with: error) - } - } - public func register() async { guard let hubConfig = vaultConfig.allegedHubConfig else { await setStateToErrorState(with: HubAuthenticationViewModelError.missingHubConfig) return } - guard let authState = authState else { - await setStateToErrorState(with: HubAuthenticationViewModelError.missingAuthState) - return - } do { try await deviceRegisteringService.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState) @@ -94,11 +84,7 @@ public class HubAuthenticationViewModel: ObservableObject { } public func continueToAccessCheck() async { - guard let authState = authState else { - await setStateToErrorState(with: HubAuthenticationViewModelError.missingAuthState) - return - } - await setState(to: .loading) + await delegate?.hubAuthenticationViewModelWantsToShowLoadingIndicator() let authFlow: HubAuthenticationFlow do { @@ -107,6 +93,8 @@ public class HubAuthenticationViewModel: ObservableObject { await setStateToErrorState(with: error) return } + await delegate?.hubAuthenticationViewModelWantsToHideLoadingIndicator() + switch authFlow { case let .success(data, header): await receivedExistingKey(data: data, header: header) @@ -134,7 +122,8 @@ public class HubAuthenticationViewModel: ObservableObject { let response = HubUnlockResponse(jwe: jwe, privateKey: privateKey, subscriptionState: subscriptionState) - await delegate?.didSuccessfullyRemoteUnlock(response) + await MainActor.run { isLoggedIn = true } + await unlockHandler.didSuccessfullyRemoteUnlock(response) } @MainActor diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserAuthenticator.swift new file mode 100644 index 000000000..d8f144599 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubUserAuthenticator.swift @@ -0,0 +1,17 @@ +import AppAuthCore +import CryptomatorCloudAccessCore +import UIKit + +struct HubUserAuthenticator: HubUserLogin { + private let hubAuthenticator: HubAuthenticating + private let viewController: UIViewController + + init(hubAuthenticator: HubAuthenticating, viewController: UIViewController) { + self.hubAuthenticator = hubAuthenticator + self.viewController = viewController + } + + func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState { + try await hubAuthenticator.authenticate(with: hubConfig, from: viewController) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift index 9249a61ec..9c336c72c 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift @@ -35,41 +35,42 @@ public final class HubXPCLoginCoordinator: Coordinator { } public func start() { - let viewModel = HubAuthenticationViewModel(vaultConfig: vaultConfig, - hubUserAuthenticator: self, - delegate: self) - let viewController = HubAuthenticationViewController(viewModel: viewModel) - navigationController.pushViewController(viewController, animated: true) + let unlockHandler = HubXPCVaultUnlockHandler(fileProviderConnector: fileProviderConnector, domain: domain, delegate: self) + prepareNavigationControllerForLogin() + let child = HubAuthenticationCoordinator(navigationController: navigationController, + vaultConfig: vaultConfig, + hubAuthenticator: hubAuthenticator, + unlockHandler: unlockHandler, + parent: self, + delegate: self) + childCoordinators.append(child) + child.start() + } + + /// Prepares the `UINavigationController` for the hub authentication flow. + /// + /// As the FileProviderExtensionUI is always shown as a sheet and the login is initially just a alert which asks the user to open a website, we want to hide the navigation bar initially. + private func prepareNavigationControllerForLogin() { + navigationController.setNavigationBarHidden(true, animated: false) } } -extension HubXPCLoginCoordinator: HubAuthenticationFlowDelegate { - public func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async { - let masterkey: Masterkey - do { - masterkey = try JWEHelper.decrypt(jwe: response.jwe, with: response.privateKey) - } catch { - handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed) - return - } - do { - let xpc: XPC = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) - defer { - fileProviderConnector.invalidateXPC(xpc) - } - try await xpc.proxy.unlockVault(rawKey: masterkey.rawKey).getValue() - let hubVault = HubVault(vaultUID: domain.identifier.rawValue, subscriptionState: response.subscriptionState) - try hubRepository.save(hubVault) - onUnlocked() - } catch { - handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed) - return - } +extension HubXPCLoginCoordinator: HubVaultUnlockHandlerDelegate { + public func successfullyProcessedUnlockedVault() { + onUnlocked() + } + + public func failedToProcessUnlockedVault(error: Error) { + handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed) } } -extension HubXPCLoginCoordinator: HubUserLogin { - public func authenticate(with hubConfig: HubConfig) async throws -> OIDAuthState { - try await hubAuthenticator.authenticate(with: hubConfig, from: navigationController) +extension HubXPCLoginCoordinator: HubAuthenticationCoordinatorDelegate { + public func userDidCancelHubAuthentication() { + onErrorAlertDismissed() + } + + public func userDismissedHubAuthenticationErrorMessage() { + onErrorAlertDismissed() } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/AddHubVaultUnlockHandler.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/AddHubVaultUnlockHandler.swift new file mode 100644 index 000000000..8be0234a6 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/AddHubVaultUnlockHandler.swift @@ -0,0 +1,41 @@ +import Foundation + +public struct AddHubVaultUnlockHandler: HubVaultUnlockHandler { + private let vaultUID: String + private let accountUID: String + private let vaultItem: VaultItem + private let downloadedVaultConfig: DownloadedVaultConfig + private let vaultManager: VaultManager + private weak var delegate: HubVaultUnlockHandlerDelegate? + + public init(vaultUID: String, + accountUID: String, + vaultItem: VaultItem, + downloadedVaultConfig: DownloadedVaultConfig, + vaultManager: VaultManager, + delegate: HubVaultUnlockHandlerDelegate?) { + self.vaultUID = vaultUID + self.accountUID = accountUID + self.vaultItem = vaultItem + self.downloadedVaultConfig = downloadedVaultConfig + self.vaultManager = vaultManager + self.delegate = delegate + } + + public func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async { + let jwe = response.jwe + let privateKey = response.privateKey + let hubVault = ExistingHubVault(vaultUID: vaultUID, + delegateAccountUID: accountUID, + jweData: jwe.compactSerializedData, + privateKey: privateKey, + vaultItem: vaultItem, + downloadedVaultConfig: downloadedVaultConfig) + do { + try await vaultManager.addExistingHubVault(hubVault).getValue() + await delegate?.successfullyProcessedUnlockedVault() + } catch { + await delegate?.failedToProcessUnlockedVault(error: error) + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubVaultUnlockHandler.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubVaultUnlockHandler.swift new file mode 100644 index 000000000..bec6336b5 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubVaultUnlockHandler.swift @@ -0,0 +1,12 @@ +import Foundation + +public protocol HubVaultUnlockHandler { + func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async +} + +public protocol HubVaultUnlockHandlerDelegate: AnyObject { + @MainActor + func successfullyProcessedUnlockedVault() + @MainActor + func failedToProcessUnlockedVault(error: Error) +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubXPCVaultUnlockHandler.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubXPCVaultUnlockHandler.swift new file mode 100644 index 000000000..4e78362c7 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubXPCVaultUnlockHandler.swift @@ -0,0 +1,41 @@ +import CryptomatorCryptoLib +import Dependencies +import FileProvider + +struct HubXPCVaultUnlockHandler: HubVaultUnlockHandler { + private let fileProviderConnector: FileProviderConnector + private let domain: NSFileProviderDomain + private weak var delegate: HubVaultUnlockHandlerDelegate? + @Dependency(\.hubRepository) private var hubRepository + + init(fileProviderConnector: FileProviderConnector, + domain: NSFileProviderDomain, + delegate: HubVaultUnlockHandlerDelegate) { + self.fileProviderConnector = fileProviderConnector + self.domain = domain + self.delegate = delegate + } + + func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async { + let masterkey: Masterkey + do { + masterkey = try JWEHelper.decrypt(jwe: response.jwe, with: response.privateKey) + } catch { + await delegate?.failedToProcessUnlockedVault(error: error) + return + } + do { + let xpc: XPC = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain) + defer { + fileProviderConnector.invalidateXPC(xpc) + } + try await xpc.proxy.unlockVault(rawKey: masterkey.rawKey).getValue() + let hubVault = HubVault(vaultUID: domain.identifier.rawValue, subscriptionState: response.subscriptionState) + try hubRepository.save(hubVault) + await delegate?.successfullyProcessedUnlockedVault() + } catch { + await delegate?.failedToProcessUnlockedVault(error: error) + return + } + } +} From 7fb8e0487dcaf917506e77f2604929dfcb75056f Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sun, 19 Nov 2023 13:33:16 +0100 Subject: [PATCH 30/45] Add unit tests for AddHubVaultUnlockHandler --- .../Hub/AddHubVaultUnlockHandlerTests.swift | 110 ++++++++++++++++++ .../HubVaultUnlockHandlerDelegateMock.swift | 45 +++++++ .../Manager/VaultManagerTests.swift | 2 +- 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/AddHubVaultUnlockHandlerTests.swift create mode 100644 CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubVaultUnlockHandlerDelegateMock.swift diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/AddHubVaultUnlockHandlerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/AddHubVaultUnlockHandlerTests.swift new file mode 100644 index 000000000..1f2601b0e --- /dev/null +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/AddHubVaultUnlockHandlerTests.swift @@ -0,0 +1,110 @@ +// +// AddHubVaultUnlockHandlerTests.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 19.11.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. + +import JOSESwift +import Promises +import XCTest +@testable import CryptomatorCloudAccessCore +@testable import CryptomatorCommonCore +@testable import CryptomatorCryptoLib + +final class AddHubVaultUnlockHandlerTests: XCTestCase { + private let vaultUID = "vault-123456789" + private let accountUID = "account-123456789" + private var vaultManagerMock: VaultManagerMock! + private var unlockHandlerDelegateMock: HubVaultUnlockHandlerDelegateMock! + + override func setUpWithError() throws { + vaultManagerMock = VaultManagerMock() + unlockHandlerDelegateMock = HubVaultUnlockHandlerDelegateMock() + } + + func testDidSuccessfullyRemoteUnlock() async throws { + let vaultConfig = VaultConfig(id: "ABB9F673-F3E8-41A7-A43B-D29F5DA65068", format: 8, cipherCombo: .sivCtrMac, shorteningThreshold: 220) + let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) + + let token = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: masterkey.rawKey) + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: token) + let metadata = CloudItemMetadata(name: "masterkey.cryptomator", + cloudPath: .init("/masterkey.cryptomator"), + itemType: .file, + lastModifiedDate: nil, + size: nil) + let jwe = try JWE(compactSerialization: "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJjcnYiOiJQLTM4NCIsImV4dCI6dHJ1ZSwia2V5X29wcyI6W10sImt0eSI6IkVDIiwieCI6Im9DLWlIcDhjZzVsUy1Qd3JjRjZxS0NzbWxfMFJzaEtCV0JJTUYzVjhuTGg2NGlCWTdsX0VsZ3Fjd0JZLXNsR3IiLCJ5IjoiVWozVzdYYVBQakJiMFRwWUFHeXlweVRIR3ByQU1hRXdWTk5Gb05tNEJuNjZuVkNKLU9pUUJYN3RhaVUtby1yWSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ.._r7LC8HLc00jk2SI.ooeI0-E29jryMJ_wbGWKVc_IfHOh3Mlfh5geRYEmLTA4GKHItRYmDdZvGsCj9pJRoNORyHdmlAMxXXIXq_v9ZocoCwZrN7EsaB8A3Kukka35i1sr7kpNbksk3G_COsGRmwQ.GJCKBE-OZ7Nm5RMf_9UwVg") + + let privateKeyPemRepresentation = "-----BEGIN PRIVATE KEY-----\nMIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDqcfr7I2SUcaYK/QHn\njhDGMpoAI1VBqzlGQ+QENqkwmGsk7N/mIQ3IJp5o7avKNJehZANiAASbOrmxoDPp\nb4AuVnUCyE1nw9KzDluGH8rozjUrteMS8ntzNlzK218iJgpRi6I3rLs8IoWTHrGE\nkfgDMgV4fk+7OC8AlKdofJudF/YcBsC00bhQ2lhlEP+PtcpgkkcJbAI=\n-----END PRIVATE KEY-----" + + let downloadedVaultConfig = DownloadedVaultConfig(vaultConfig: unverifiedVaultConfig, + token: token, + metadata: metadata) + let unlockHandler = AddHubVaultUnlockHandler(vaultUID: vaultUID, + accountUID: accountUID, vaultItem: VaultItemStub(), downloadedVaultConfig: downloadedVaultConfig, + vaultManager: vaultManagerMock, + delegate: unlockHandlerDelegateMock) + vaultManagerMock.addExistingHubVaultReturnValue = Promise(()) + + // WHEN + // calling didSuccessfullyRemoteUnlock + try await unlockHandler.didSuccessfullyRemoteUnlock(.init(jwe: jwe, privateKey: .init(pemRepresentation: privateKeyPemRepresentation), subscriptionState: .active)) + + // THEN + // the hub vault has been added as an existing one + let savedHubVault = vaultManagerMock.addExistingHubVaultReceivedVault + XCTAssertEqual(savedHubVault?.vaultUID, vaultUID) + XCTAssertEqual(savedHubVault?.delegateAccountUID, accountUID) + XCTAssertEqual(savedHubVault?.jweData, jwe.compactSerializedData) + XCTAssertEqual(savedHubVault?.downloadedVaultConfig.token, token) + + // and the delegate gets informed that the handler successfully processed the unlocked vault + XCTAssertEqual(unlockHandlerDelegateMock.successfullyProcessedUnlockedVaultCallsCount, 1) + XCTAssertFalse(unlockHandlerDelegateMock.failedToProcessUnlockedVaultErrorCalled) + } + + func testDidSuccessfullyRemoteUnlock_fails_informsDelegateAboutFailure() async throws { + let vaultConfig = VaultConfig(id: "ABB9F673-F3E8-41A7-A43B-D29F5DA65068", format: 8, cipherCombo: .sivCtrMac, shorteningThreshold: 220) + let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) + + let token = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: masterkey.rawKey) + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: token) + let metadata = CloudItemMetadata(name: "masterkey.cryptomator", + cloudPath: .init("/masterkey.cryptomator"), + itemType: .file, + lastModifiedDate: nil, + size: nil) + let jwe = try JWE(compactSerialization: "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJjcnYiOiJQLTM4NCIsImV4dCI6dHJ1ZSwia2V5X29wcyI6W10sImt0eSI6IkVDIiwieCI6Im9DLWlIcDhjZzVsUy1Qd3JjRjZxS0NzbWxfMFJzaEtCV0JJTUYzVjhuTGg2NGlCWTdsX0VsZ3Fjd0JZLXNsR3IiLCJ5IjoiVWozVzdYYVBQakJiMFRwWUFHeXlweVRIR3ByQU1hRXdWTk5Gb05tNEJuNjZuVkNKLU9pUUJYN3RhaVUtby1yWSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ.._r7LC8HLc00jk2SI.ooeI0-E29jryMJ_wbGWKVc_IfHOh3Mlfh5geRYEmLTA4GKHItRYmDdZvGsCj9pJRoNORyHdmlAMxXXIXq_v9ZocoCwZrN7EsaB8A3Kukka35i1sr7kpNbksk3G_COsGRmwQ.GJCKBE-OZ7Nm5RMf_9UwVg") + + let privateKeyPemRepresentation = "-----BEGIN PRIVATE KEY-----\nMIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDqcfr7I2SUcaYK/QHn\njhDGMpoAI1VBqzlGQ+QENqkwmGsk7N/mIQ3IJp5o7avKNJehZANiAASbOrmxoDPp\nb4AuVnUCyE1nw9KzDluGH8rozjUrteMS8ntzNlzK218iJgpRi6I3rLs8IoWTHrGE\nkfgDMgV4fk+7OC8AlKdofJudF/YcBsC00bhQ2lhlEP+PtcpgkkcJbAI=\n-----END PRIVATE KEY-----" + + let downloadedVaultConfig = DownloadedVaultConfig(vaultConfig: unverifiedVaultConfig, + token: token, + metadata: metadata) + let unlockHandler = AddHubVaultUnlockHandler(vaultUID: vaultUID, + accountUID: accountUID, vaultItem: VaultItemStub(), downloadedVaultConfig: downloadedVaultConfig, + vaultManager: vaultManagerMock, + delegate: unlockHandlerDelegateMock) + // GIVEN + // the existing hub vault can't be added due to an error + vaultManagerMock.addExistingHubVaultReturnValue = Promise(TestError()) + + // WHEN + // calling didSuccessfullyRemoteUnlock + try await unlockHandler.didSuccessfullyRemoteUnlock(.init(jwe: jwe, privateKey: .init(pemRepresentation: privateKeyPemRepresentation), subscriptionState: .active)) + + // THEN + // the delegate gets informed that the handler failed to process the unlocked vault + XCTAssertEqual(unlockHandlerDelegateMock.failedToProcessUnlockedVaultErrorCallsCount, 1) + XCTAssert(unlockHandlerDelegateMock.failedToProcessUnlockedVaultErrorReceivedError is TestError) + XCTAssertFalse(unlockHandlerDelegateMock.successfullyProcessedUnlockedVaultCalled) + } + + private struct VaultItemStub: VaultItem { + let name = "name" + let vaultPath = CloudPath("/name") + } + + private struct TestError: Error {} +} diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubVaultUnlockHandlerDelegateMock.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubVaultUnlockHandlerDelegateMock.swift new file mode 100644 index 000000000..aa6ec4fb1 --- /dev/null +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubVaultUnlockHandlerDelegateMock.swift @@ -0,0 +1,45 @@ +// +// HubVaultUnlockHandlerDelegateMock.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 19.11.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. + +import Foundation +@testable import CryptomatorCommonCore +// swiftlint:disable all +final class HubVaultUnlockHandlerDelegateMock: HubVaultUnlockHandlerDelegate { + // MARK: - successfullyProcessedUnlockedVault + + var successfullyProcessedUnlockedVaultCallsCount = 0 + var successfullyProcessedUnlockedVaultCalled: Bool { + successfullyProcessedUnlockedVaultCallsCount > 0 + } + + var successfullyProcessedUnlockedVaultClosure: (() -> Void)? + + func successfullyProcessedUnlockedVault() { + successfullyProcessedUnlockedVaultCallsCount += 1 + successfullyProcessedUnlockedVaultClosure?() + } + + // MARK: - failedToProcessUnlockedVault + + var failedToProcessUnlockedVaultErrorCallsCount = 0 + var failedToProcessUnlockedVaultErrorCalled: Bool { + failedToProcessUnlockedVaultErrorCallsCount > 0 + } + + var failedToProcessUnlockedVaultErrorReceivedError: Error? + var failedToProcessUnlockedVaultErrorReceivedInvocations: [Error] = [] + var failedToProcessUnlockedVaultErrorClosure: ((Error) -> Void)? + + func failedToProcessUnlockedVault(error: Error) { + failedToProcessUnlockedVaultErrorCallsCount += 1 + failedToProcessUnlockedVaultErrorReceivedError = error + failedToProcessUnlockedVaultErrorReceivedInvocations.append(error) + failedToProcessUnlockedVaultErrorClosure?(error) + } +} + +// swiftlint:enable all diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift index 2a75305a2..946e967e7 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift @@ -15,7 +15,7 @@ import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorCryptoLib -class VaultManagerMock: VaultDBManager { +private final class VaultManagerMock: VaultDBManager { var removedVaultUIDs = [String]() var addedFileProviderDomainDisplayName = [String: String]() From 49cec825dae585ceffba10494158d98618fe22b5 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sun, 19 Nov 2023 14:22:21 +0100 Subject: [PATCH 31/45] Move FileProviderConnector to dependency values --- Cryptomator/Settings/SettingsViewModel.swift | 7 +++-- .../ChangePasswordViewModel.swift | 6 ++-- .../VaultKeepUnlockedViewModel.swift | 6 ++-- .../MoveVault/MoveVaultViewModel.swift | 7 ++--- .../RenameVault/RenameVaultViewModel.swift | 8 ++--- .../VaultDetail/VaultDetailViewModel.swift | 8 ++--- .../VaultList/VaultCellViewModel.swift | 6 ++-- .../VaultList/VaultListViewModel.swift | 8 ++--- .../FileProviderConnector.swift | 31 +++++++++++++++++-- .../Hub/HubXPCLoginCoordinator.swift | 4 +-- .../GetFolderIntentHandler.swift | 8 +++-- .../IsVaultUnlockedIntentHandler.swift | 8 +++-- .../LockVaultIntentHandler.swift | 8 +++-- .../SaveFileIntentHandler.swift | 9 ++++-- .../ChangePasswordViewModelTests.swift | 8 +++-- .../MoveVaultViewModelTests.swift | 8 +++-- .../RenameVaultViewModelTests.swift | 9 ++++-- CryptomatorTests/SettingsViewModelTests.swift | 4 ++- .../VaultKeepUnlockedViewModelTests.swift | 5 +-- .../VaultListViewModelTests.swift | 12 ++++--- .../RootViewController.swift | 21 +++++++------ .../UnlockVaultViewModel.swift | 7 ++--- 22 files changed, 123 insertions(+), 75 deletions(-) diff --git a/Cryptomator/Settings/SettingsViewModel.swift b/Cryptomator/Settings/SettingsViewModel.swift index 3fa630c9f..d761538c1 100644 --- a/Cryptomator/Settings/SettingsViewModel.swift +++ b/Cryptomator/Settings/SettingsViewModel.swift @@ -9,6 +9,7 @@ import Combine import CryptomatorCommonCore import CryptomatorFileProvider +import Dependencies import Foundation import Promises import StoreKit @@ -90,13 +91,13 @@ class SettingsViewModel: TableViewModel { return viewModel }() - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector + private var subscribers = Set() private lazy var showDebugModeWarningPublisher = PassthroughSubject() - init(cryptomatorSettings: CryptomatorSettings = CryptomatorUserDefaults.shared, fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + init(cryptomatorSettings: CryptomatorSettings = CryptomatorUserDefaults.shared) { self.cryptomatorSettings = cryptomatorSettings - self.fileProviderConnector = fileProviderConnector } func refreshCacheSize() -> Promise { diff --git a/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift b/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift index fbe93f71b..265e6350e 100644 --- a/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift +++ b/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift @@ -10,6 +10,7 @@ import CocoaLumberjackSwift import Combine import CryptomatorCommonCore import CryptomatorCryptoLib +import Dependencies import FileProvider import Foundation import Promises @@ -80,7 +81,7 @@ class ChangePasswordViewModel: TableViewModel, ChangePass private let vaultAccount: VaultAccount private let domain: NSFileProviderDomain private let vaultManager: VaultManager - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector private let oldPasswordCellViewModel = TextFieldCellViewModel(type: .password, isInitialFirstResponder: true) private let newPasswordCellViewModel = TextFieldCellViewModel(type: .password) @@ -100,11 +101,10 @@ class ChangePasswordViewModel: TableViewModel, ChangePass private lazy var subscribers = Set() - init(vaultAccount: VaultAccount, domain: NSFileProviderDomain, vaultManager: VaultManager = VaultDBManager.shared, fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + init(vaultAccount: VaultAccount, domain: NSFileProviderDomain, vaultManager: VaultManager = VaultDBManager.shared) { self.vaultAccount = vaultAccount self.domain = domain self.vaultManager = vaultManager - self.fileProviderConnector = fileProviderConnector super.init() } diff --git a/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift b/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift index 08e1f36d3..8ed08501e 100644 --- a/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift +++ b/Cryptomator/VaultDetail/KeepUnlocked/VaultKeepUnlockedViewModel.swift @@ -8,6 +8,7 @@ import Combine import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import Promises @@ -40,7 +41,7 @@ class VaultKeepUnlockedViewModel: TableViewModel, Vaul private(set) var keepUnlockedItems = [KeepUnlockedDurationItem]() private let vaultKeepUnlockedSettings: VaultKeepUnlockedSettings private let masterkeyCacheManager: MasterkeyCacheManager - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector private let vaultInfo: VaultInfo private let currentKeepUnlockedDuration: Bindable private var subscriber: AnyCancellable? @@ -48,11 +49,10 @@ class VaultKeepUnlockedViewModel: TableViewModel, Vaul return vaultInfo.vaultUID } - init(currentKeepUnlockedDuration: Bindable, vaultInfo: VaultInfo, vaultKeepUnlockedSettings: VaultKeepUnlockedSettings = VaultKeepUnlockedManager.shared, masterkeyCacheManager: MasterkeyCacheManager = MasterkeyCacheKeychainManager.shared, fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + init(currentKeepUnlockedDuration: Bindable, vaultInfo: VaultInfo, vaultKeepUnlockedSettings: VaultKeepUnlockedSettings = VaultKeepUnlockedManager.shared, masterkeyCacheManager: MasterkeyCacheManager = MasterkeyCacheKeychainManager.shared) { self.vaultInfo = vaultInfo self.vaultKeepUnlockedSettings = vaultKeepUnlockedSettings self.masterkeyCacheManager = masterkeyCacheManager - self.fileProviderConnector = fileProviderConnector self.currentKeepUnlockedDuration = currentKeepUnlockedDuration self.keepUnlockedItems = KeepUnlockedDuration.allCases.map { diff --git a/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift b/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift index cffb4c33a..48861fcc1 100644 --- a/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift +++ b/Cryptomator/VaultDetail/MoveVault/MoveVaultViewModel.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import GRDB @@ -28,19 +29,17 @@ class MoveVaultViewModel: ChooseFolderViewModel, MoveVaultViewModelProtocol { private let vaultManager: VaultManager private let vaultInfo: VaultInfo private let domain: NSFileProviderDomain - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector init(provider: CloudProvider, currentFolderChoosingCloudPath: CloudPath, vaultInfo: VaultInfo, domain: NSFileProviderDomain, cloudProviderManager: CloudProviderManager = CloudProviderDBManager.shared, - vaultManager: VaultManager = VaultDBManager.shared, - fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + vaultManager: VaultManager = VaultDBManager.shared) { self.vaultInfo = vaultInfo self.domain = domain self.vaultManager = vaultManager - self.fileProviderConnector = fileProviderConnector super.init(canCreateFolder: true, cloudPath: currentFolderChoosingCloudPath, provider: provider) } diff --git a/Cryptomator/VaultDetail/RenameVault/RenameVaultViewModel.swift b/Cryptomator/VaultDetail/RenameVault/RenameVaultViewModel.swift index 4e36d213f..244328848 100644 --- a/Cryptomator/VaultDetail/RenameVault/RenameVaultViewModel.swift +++ b/Cryptomator/VaultDetail/RenameVault/RenameVaultViewModel.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import GRDB @@ -31,19 +32,18 @@ class RenameVaultViewModel: SetVaultNameViewModel, RenameVaultViewModelProtcol { // swiftlint:disable:next weak_delegate private let delegate: MoveVaultViewModel private let vaultInfo: VaultInfo + @Dependency(\.fileProviderConnector) private var fileProviderConnector init(provider: CloudProvider, vaultInfo: VaultInfo, domain: NSFileProviderDomain, - vaultManager: VaultManager = VaultDBManager.shared, - fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + vaultManager: VaultManager = VaultDBManager.shared) { self.delegate = MoveVaultViewModel( provider: provider, currentFolderChoosingCloudPath: CloudPath("/"), vaultInfo: vaultInfo, domain: domain, - vaultManager: vaultManager, - fileProviderConnector: fileProviderConnector + vaultManager: vaultManager ) self.vaultInfo = vaultInfo } diff --git a/Cryptomator/VaultDetail/VaultDetailViewModel.swift b/Cryptomator/VaultDetail/VaultDetailViewModel.swift index 3d098fb69..196707819 100644 --- a/Cryptomator/VaultDetail/VaultDetailViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailViewModel.swift @@ -10,6 +10,7 @@ import CocoaLumberjackSwift import Combine import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import GRDB import LocalAuthentication import Promises @@ -73,7 +74,7 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { private let vaultInfo: VaultInfo private let vaultManager: VaultManager - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector private let context = LAContext() private let vaultKeepUnlockedSettings: VaultKeepUnlockedSettings private let passwordManager: VaultPasswordManager @@ -156,13 +157,12 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { private var observation: DatabaseCancellable? convenience init(vaultInfo: VaultInfo) { - self.init(vaultInfo: vaultInfo, vaultManager: VaultDBManager.shared, fileProviderConnector: FileProviderXPCConnector.shared, passwordManager: VaultPasswordKeychainManager(), dbManager: DatabaseManager.shared, vaultKeepUnlockedSettings: VaultKeepUnlockedManager.shared) + self.init(vaultInfo: vaultInfo, vaultManager: VaultDBManager.shared, passwordManager: VaultPasswordKeychainManager(), dbManager: DatabaseManager.shared, vaultKeepUnlockedSettings: VaultKeepUnlockedManager.shared) } - init(vaultInfo: VaultInfo, vaultManager: VaultManager, fileProviderConnector: FileProviderConnector, passwordManager: VaultPasswordManager, dbManager: DatabaseManager, vaultKeepUnlockedSettings: VaultKeepUnlockedSettings) { + init(vaultInfo: VaultInfo, vaultManager: VaultManager, passwordManager: VaultPasswordManager, dbManager: DatabaseManager, vaultKeepUnlockedSettings: VaultKeepUnlockedSettings) { self.vaultInfo = vaultInfo self.vaultManager = vaultManager - self.fileProviderConnector = fileProviderConnector self.passwordManager = passwordManager self.title = Bindable(vaultInfo.vaultName) self.vaultKeepUnlockedSettings = vaultKeepUnlockedSettings diff --git a/Cryptomator/VaultList/VaultCellViewModel.swift b/Cryptomator/VaultList/VaultCellViewModel.swift index 0119693f8..a97e41c98 100644 --- a/Cryptomator/VaultList/VaultCellViewModel.swift +++ b/Cryptomator/VaultList/VaultCellViewModel.swift @@ -8,6 +8,7 @@ import Combine import CryptomatorCommonCore +import Dependencies import Promises import UIKit @@ -33,11 +34,10 @@ class VaultCellViewModel: TableViewCellViewModel, VaultCellViewModelProtocol { let vault: VaultInfo private lazy var errorPublisher = PassthroughSubject() - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector - init(vault: VaultInfo, fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared) { + init(vault: VaultInfo) { self.vault = vault - self.fileProviderConnector = fileProviderConnector } func lockVault() -> Promise { diff --git a/Cryptomator/VaultList/VaultListViewModel.swift b/Cryptomator/VaultList/VaultListViewModel.swift index ed39128ab..4c6254a58 100644 --- a/Cryptomator/VaultList/VaultListViewModel.swift +++ b/Cryptomator/VaultList/VaultListViewModel.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import Combine import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import GRDB @@ -27,7 +28,7 @@ class VaultListViewModel: ViewModel, VaultListViewModelProtocol { var vaultCellViewModels: [VaultCellViewModel] private let dbManager: DatabaseManager private let vaultManager: VaultDBManager - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector private var observation: DatabaseCancellable? private lazy var subscribers = Set() private lazy var errorPublisher = PassthroughSubject() @@ -35,13 +36,12 @@ class VaultListViewModel: ViewModel, VaultListViewModelProtocol { private var removedRow = false convenience init() { - self.init(dbManager: DatabaseManager.shared, vaultManager: VaultDBManager.shared, fileProviderConnector: FileProviderXPCConnector.shared) + self.init(dbManager: DatabaseManager.shared, vaultManager: VaultDBManager.shared) } - init(dbManager: DatabaseManager, vaultManager: VaultDBManager, fileProviderConnector: FileProviderConnector) { + init(dbManager: DatabaseManager, vaultManager: VaultDBManager) { self.dbManager = dbManager self.vaultManager = vaultManager - self.fileProviderConnector = fileProviderConnector self.vaultCellViewModels = [VaultCellViewModel]() } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift index 60eb802f6..3b9bb4fc7 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FileProviderXPC/FileProviderConnector.swift @@ -7,6 +7,7 @@ // import CocoaLumberjackSwift +import Dependencies import FileProvider import Foundation import Promises @@ -46,6 +47,20 @@ public extension FileProviderConnector { } } +private enum FileProviderConnectorKey: DependencyKey { + static var liveValue: FileProviderConnector { FileProviderXPCConnector() } + #if DEBUG + static var testValue: FileProviderConnector = UnimplementedFileProviderConnector() + #endif +} + +public extension DependencyValues { + var fileProviderConnector: FileProviderConnector { + get { self[FileProviderConnectorKey.self] } + set { self[FileProviderConnectorKey.self] = newValue } + } +} + public struct XPC { public let proxy: T let doneHandler: () -> Void @@ -69,8 +84,6 @@ public class FileProviderXPCConnector: FileProviderConnector { } } - public static let shared = FileProviderXPCConnector() - public func getXPC(serviceName: NSFileProviderServiceName, domain: NSFileProviderDomain?) -> Promise> { var url = NSFileProviderManager.default.documentStorageURL if let domain = domain { @@ -119,3 +132,17 @@ public extension XPC { self.init(proxy: proxy, doneHandler: {}) } } + +#if DEBUG +private struct UnimplementedFileProviderConnector: FileProviderConnector { + func getXPC(serviceName: NSFileProviderServiceName, domain: NSFileProviderDomain?) -> Promise> { + unimplemented("\(Self.self).getXPC(serviceName:domain:) not implemented", placeholder: Promise(UnimplementedError())) + } + + func getXPC(serviceName: NSFileProviderServiceName, domainIdentifier: NSFileProviderDomainIdentifier) -> Promise> { + unimplemented("\(Self.self).getXPC(serviceName:domainIdentifier:) not implemented", placeholder: Promise(UnimplementedError())) + } + + private struct UnimplementedError: Error {} +} +#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift index 9c336c72c..ffcaaeddd 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift @@ -12,23 +12,21 @@ public final class HubXPCLoginCoordinator: Coordinator { public var navigationController: UINavigationController let domain: NSFileProviderDomain let vaultConfig: UnverifiedVaultConfig - let fileProviderConnector: FileProviderConnector let hubAuthenticator: HubAuthenticating public let onUnlocked: () -> Void public let onErrorAlertDismissed: () -> Void @Dependency(\.hubRepository) private var hubRepository + @Dependency(\.fileProviderConnector) private var fileProviderConnector public init(navigationController: UINavigationController, domain: NSFileProviderDomain, vaultConfig: UnverifiedVaultConfig, - fileProviderConnector: FileProviderConnector = FileProviderXPCConnector.shared, hubAuthenticator: HubAuthenticating, onUnlocked: @escaping () -> Void, onErrorAlertDismissed: @escaping () -> Void) { self.navigationController = navigationController self.domain = domain self.vaultConfig = vaultConfig - self.fileProviderConnector = fileProviderConnector self.hubAuthenticator = hubAuthenticator self.onUnlocked = onUnlocked self.onErrorAlertDismissed = onErrorAlertDismissed diff --git a/CryptomatorIntents/GetFolderIntentHandler.swift b/CryptomatorIntents/GetFolderIntentHandler.swift index 0b822f0b5..14653985f 100644 --- a/CryptomatorIntents/GetFolderIntentHandler.swift +++ b/CryptomatorIntents/GetFolderIntentHandler.swift @@ -9,12 +9,14 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import Foundation import Intents import Promises class GetFolderIntentHandler: NSObject, GetFolderIntentHandling { let vaultOptionsProvider: VaultOptionsProvider + @Dependency(\.fileProviderConnector) private var fileProviderConnector init(vaultOptionsProvider: VaultOptionsProvider) { self.vaultOptionsProvider = vaultOptionsProvider @@ -69,7 +71,7 @@ class GetFolderIntentHandler: NSObject, GetFolderIntentHandling { // MARK: Internal private func getIdentifierForFolder(at cloudPath: CloudPath, domainIdentifier: NSFileProviderDomainIdentifier) async throws -> String { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .fileImporting, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .fileImporting, domainIdentifier: domainIdentifier) return try await withCheckedThrowingContinuation({ continuation in getXPCPromise.then { xpc in xpc.proxy.getIdentifierForItem(at: cloudPath.path) @@ -77,8 +79,8 @@ class GetFolderIntentHandler: NSObject, GetFolderIntentHandling { continuation.resume(returning: $0 as String) }.catch { continuation.resume(throwing: $0) - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } }) } diff --git a/CryptomatorIntents/IsVaultUnlockedIntentHandler.swift b/CryptomatorIntents/IsVaultUnlockedIntentHandler.swift index 0e21a8c3a..dd5aa81f2 100644 --- a/CryptomatorIntents/IsVaultUnlockedIntentHandler.swift +++ b/CryptomatorIntents/IsVaultUnlockedIntentHandler.swift @@ -7,6 +7,7 @@ // import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import Intents @@ -14,6 +15,7 @@ import Promises class IsVaultUnlockedIntentHandler: NSObject, IsVaultUnlockedIntentHandling { let vaultOptionsProvider: VaultOptionsProvider + @Dependency(\.fileProviderConnector) private var fileProviderConnector init(vaultOptionsProvider: VaultOptionsProvider) { self.vaultOptionsProvider = vaultOptionsProvider @@ -46,7 +48,7 @@ class IsVaultUnlockedIntentHandler: NSObject, IsVaultUnlockedIntentHandling { // MARK: Internal private func getIsUnlockedVault(domainIdentifier: NSFileProviderDomainIdentifier) async throws -> Bool { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) return try await withCheckedThrowingContinuation({ continuation in getXPCPromise.then { xpc in xpc.proxy.getIsUnlockedVault(domainIdentifier: domainIdentifier) @@ -54,8 +56,8 @@ class IsVaultUnlockedIntentHandler: NSObject, IsVaultUnlockedIntentHandling { continuation.resume(returning: $0) }.catch { continuation.resume(throwing: $0) - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } }) } diff --git a/CryptomatorIntents/LockVaultIntentHandler.swift b/CryptomatorIntents/LockVaultIntentHandler.swift index aea7e3ead..a3065ce08 100644 --- a/CryptomatorIntents/LockVaultIntentHandler.swift +++ b/CryptomatorIntents/LockVaultIntentHandler.swift @@ -8,12 +8,14 @@ import CocoaLumberjackSwift import CryptomatorCommonCore +import Dependencies import Foundation import Intents import Promises class LockVaultIntentHandler: NSObject, LockVaultIntentHandling { let vaultOptionsProvider: VaultOptionsProvider + @Dependency(\.fileProviderConnector) private var fileProviderConnector init(vaultOptionsProvider: VaultOptionsProvider) { self.vaultOptionsProvider = vaultOptionsProvider @@ -45,7 +47,7 @@ class LockVaultIntentHandler: NSObject, LockVaultIntentHandling { // MARK: Internal private func lockVault(with domainIdentifier: NSFileProviderDomainIdentifier) async throws { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .vaultLocking, domainIdentifier: domainIdentifier) return try await withCheckedThrowingContinuation({ continuation in getXPCPromise.then { xpc in xpc.proxy.gracefulLockVault(domainIdentifier: domainIdentifier) @@ -53,8 +55,8 @@ class LockVaultIntentHandler: NSObject, LockVaultIntentHandling { continuation.resume(returning: ()) }.catch { continuation.resume(throwing: $0) - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } }) } diff --git a/CryptomatorIntents/SaveFileIntentHandler.swift b/CryptomatorIntents/SaveFileIntentHandler.swift index 400f1c064..00dafa1d9 100644 --- a/CryptomatorIntents/SaveFileIntentHandler.swift +++ b/CryptomatorIntents/SaveFileIntentHandler.swift @@ -9,12 +9,15 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import Intents import Promises class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { + @Dependency(\.fileProviderConnector) private var fileProviderConnector + func handle(intent: SaveFileIntent) async -> SaveFileIntentResponse { guard let vaultFolder = intent.folder, let vaultIdentifier = vaultFolder.vaultIdentifier, let folderIdentifier = vaultFolder.identifier else { return SaveFileIntentResponse(failureReason: LocalizedString.getValue("intents.saveFile.invalidFolder")) @@ -85,7 +88,7 @@ class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { } private func importFile(at localURL: URL, toParentItemIdentifier parentItemIdentifier: String, domainIdentifier: NSFileProviderDomainIdentifier) async throws { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .fileImporting, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .fileImporting, domainIdentifier: domainIdentifier) try await withCheckedThrowingContinuation({ continuation in getXPCPromise.then { xpc in xpc.proxy.importFile(at: localURL, toParentItemIdentifier: parentItemIdentifier) @@ -93,8 +96,8 @@ class SaveFileIntentHandler: NSObject, SaveFileIntentHandling { continuation.resume() }.catch { continuation.resume(throwing: $0) - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } }) } diff --git a/CryptomatorTests/ChangePasswordViewModelTests.swift b/CryptomatorTests/ChangePasswordViewModelTests.swift index bf12f6add..0210d8267 100644 --- a/CryptomatorTests/ChangePasswordViewModelTests.swift +++ b/CryptomatorTests/ChangePasswordViewModelTests.swift @@ -14,6 +14,7 @@ import Promises import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class ChangePasswordViewModelTests: XCTestCase { private var vaultManagerMock: VaultManagerMock! @@ -27,7 +28,8 @@ class ChangePasswordViewModelTests: XCTestCase { setupMocks() vaultAccount = VaultAccount(vaultUID: UUID().uuidString, delegateAccountUID: UUID().uuidString, vaultPath: CloudPath("/Foo/Bar"), vaultName: "Bar") let domain = NSFileProviderDomain(vaultUID: vaultAccount.vaultUID, displayName: vaultAccount.vaultName) - viewModel = ChangePasswordViewModel(vaultAccount: vaultAccount, domain: domain, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) + viewModel = ChangePasswordViewModel(vaultAccount: vaultAccount, domain: domain, vaultManager: vaultManagerMock) } private func setupMocks() { @@ -70,7 +72,7 @@ class ChangePasswordViewModelTests: XCTestCase { try await viewModel.changePassword() - wait(for: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) + await fulfillment(of: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) XCTAssertEqual(1, vaultManagerMock.changePassphraseOldPassphraseNewPassphraseForVaultUIDCallsCount) XCTAssertEqual(oldPassword, vaultManagerMock.changePassphraseOldPassphraseNewPassphraseForVaultUIDReceivedArguments?.oldPassphrase) @@ -125,7 +127,7 @@ class ChangePasswordViewModelTests: XCTestCase { XCTAssertEqual(1, vaultLockingMock.lockedVaults.count) XCTAssertTrue(vaultLockingMock.lockedVaults.contains(NSFileProviderDomainIdentifier(vaultAccount.vaultUID))) - wait(for: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) + await fulfillment(of: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) } func testChangePasswordFailForEmptyOldPassword() async throws { diff --git a/CryptomatorTests/MoveVaultViewModelTests.swift b/CryptomatorTests/MoveVaultViewModelTests.swift index d68a9da4c..a0d1a4b09 100644 --- a/CryptomatorTests/MoveVaultViewModelTests.swift +++ b/CryptomatorTests/MoveVaultViewModelTests.swift @@ -13,6 +13,7 @@ import Promises import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class MoveVaultViewModelTests: XCTestCase { private var vaultManagerMock: VaultManagerMock! @@ -36,6 +37,8 @@ class MoveVaultViewModelTests: XCTestCase { maintenanceHelperMock = MaintenanceModeHelperMock() vaultLockingMock = VaultLockingMock() + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) + fileProviderConnectorMock.getXPCServiceNameDomainClosure = { serviceName, _ in switch serviceName { case .maintenanceModeHelper: @@ -71,7 +74,7 @@ class MoveVaultViewModelTests: XCTestCase { XCTAssertEqual(1, vaultLockingMock.lockedVaults.count) XCTAssertTrue(vaultLockingMock.lockedVaults.contains(NSFileProviderDomainIdentifier(vaultAccount.vaultUID))) - wait(for: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) + await fulfillment(of: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) } func testRejectVaultsInTheLocalFileSystem() async throws { @@ -173,7 +176,6 @@ class MoveVaultViewModelTests: XCTestCase { currentFolderChoosingCloudPath: currentFolderChoosingCloudPath, vaultInfo: vaultInfo, domain: domain, - vaultManager: vaultManagerMock, - fileProviderConnector: fileProviderConnectorMock) + vaultManager: vaultManagerMock) } } diff --git a/CryptomatorTests/RenameVaultViewModelTests.swift b/CryptomatorTests/RenameVaultViewModelTests.swift index 59390496d..331c51270 100644 --- a/CryptomatorTests/RenameVaultViewModelTests.swift +++ b/CryptomatorTests/RenameVaultViewModelTests.swift @@ -13,6 +13,7 @@ import Promises import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class RenameVaultViewModelTests: SetVaultNameViewModelTests { private var vaultManagerMock: VaultManagerMock! @@ -32,6 +33,8 @@ class RenameVaultViewModelTests: SetVaultNameViewModelTests { maintenanceHelperMock = MaintenanceModeHelperMock() vaultLockingMock = VaultLockingMock() + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) + fileProviderConnectorMock.getXPCServiceNameDomainClosure = { serviceName, _ in switch serviceName { case .maintenanceModeHelper: @@ -101,7 +104,7 @@ class RenameVaultViewModelTests: SetVaultNameViewModelTests { XCTAssertEqual(1, vaultLockingMock.lockedVaults.count) XCTAssertTrue(vaultLockingMock.lockedVaults.contains(NSFileProviderDomainIdentifier(vaultAccount.vaultUID))) - wait(for: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) + await fulfillment(of: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) } func testRenameVaultWithOldNameAsSubstring() async throws { @@ -191,7 +194,7 @@ class RenameVaultViewModelTests: SetVaultNameViewModelTests { XCTAssertEqual(1, vaultLockingMock.lockedVaults.count) XCTAssertTrue(vaultLockingMock.lockedVaults.contains(NSFileProviderDomainIdentifier(vaultAccount.vaultUID))) XCTAssertFalse(vaultManagerMock.moveVaultAccountToCalled) - wait(for: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) + await fulfillment(of: [maintenanceModeEnabled, maintenanceModeDisabled], timeout: 1.0, enforceOrder: true) } private func createViewModel(vaultAccount: VaultAccount, cloudProviderType: CloudProviderType, viewControllerTitle: String? = nil) -> RenameVaultViewModel { @@ -199,7 +202,7 @@ class RenameVaultViewModelTests: SetVaultNameViewModelTests { let vaultListPosition = VaultListPosition(id: 1, position: 1, vaultUID: vaultAccount.vaultUID) let vaultInfo = VaultInfo(vaultAccount: vaultAccount, cloudProviderAccount: cloudProviderAccount, vaultListPosition: vaultListPosition) let domain = NSFileProviderDomain(vaultUID: vaultInfo.vaultUID, displayName: vaultInfo.vaultName) - return RenameVaultViewModel(provider: CloudProviderMock(), vaultInfo: vaultInfo, domain: domain, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + return RenameVaultViewModel(provider: CloudProviderMock(), vaultInfo: vaultInfo, domain: domain, vaultManager: vaultManagerMock) } private func checkMaintenanceModeEnabledThenDisabled() { diff --git a/CryptomatorTests/SettingsViewModelTests.swift b/CryptomatorTests/SettingsViewModelTests.swift index 848651685..f2ed8ff85 100644 --- a/CryptomatorTests/SettingsViewModelTests.swift +++ b/CryptomatorTests/SettingsViewModelTests.swift @@ -11,6 +11,7 @@ import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class SettingsViewModelTests: XCTestCase { private var cryptomatorSettingsMock: CryptomatorSettingsMock! @@ -25,7 +26,8 @@ class SettingsViewModelTests: XCTestCase { } cryptomatorSettingsMock = CryptomatorSettingsMock() fileProviderConnectorMock = FileProviderConnectorMock() - settingsViewModel = SettingsViewModel(cryptomatorSettings: cryptomatorSettingsMock, fileProviderConnector: fileProviderConnectorMock) + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) + settingsViewModel = SettingsViewModel(cryptomatorSettings: cryptomatorSettingsMock) } // - MARK: Cache Section diff --git a/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift b/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift index d3284f401..7ddb6d789 100644 --- a/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift +++ b/CryptomatorTests/VaultKeepUnlockedViewModelTests.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class VaultKeepUnlockedViewModelTests: XCTestCase { var vaultKeepUnlockedSettingsMock: VaultKeepUnlockedSettingsMock! @@ -26,6 +27,7 @@ class VaultKeepUnlockedViewModelTests: XCTestCase { masterkeyCacheManagerMock = MasterkeyCacheManagerMock() fileProviderConnectorMock = FileProviderConnectorMock() vaultLockingMock = VaultLockingMock() + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) } func testDefaultConfiguration() throws { @@ -203,8 +205,7 @@ class VaultKeepUnlockedViewModelTests: XCTestCase { return VaultKeepUnlockedViewModel(currentKeepUnlockedDuration: currentKeepUnlockedDuration, vaultInfo: vaultInfo, vaultKeepUnlockedSettings: vaultKeepUnlockedSettingsMock, - masterkeyCacheManager: masterkeyCacheManagerMock, - fileProviderConnector: fileProviderConnectorMock) + masterkeyCacheManager: masterkeyCacheManagerMock) } private func assertSectionsAreCorrect(selectedKeepUnlockedDuration: KeepUnlockedDuration, viewModel: VaultKeepUnlockedViewModel) { diff --git a/CryptomatorTests/VaultListViewModelTests.swift b/CryptomatorTests/VaultListViewModelTests.swift index afcac5d0f..0decfa5c1 100644 --- a/CryptomatorTests/VaultListViewModelTests.swift +++ b/CryptomatorTests/VaultListViewModelTests.swift @@ -13,6 +13,7 @@ import Promises import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class VaultListViewModelTests: XCTestCase { private var vaultManagerMock: VaultDBManagerMock! @@ -28,11 +29,12 @@ class VaultListViewModelTests: XCTestCase { vaultCacheMock = VaultCacheMock() vaultManagerMock = VaultDBManagerMock(providerManager: cloudProviderManager, vaultAccountManager: vaultAccountManagerMock, vaultCache: vaultCacheMock, passwordManager: passwordManagerMock, masterkeyCacheManager: MasterkeyCacheManagerMock(), masterkeyCacheHelper: MasterkeyCacheHelperMock()) fileProviderConnectorMock = FileProviderConnectorMock() + DependencyValues.mockDependency(\.fileProviderConnector, with: fileProviderConnectorMock) } func testRefreshVaultsIsSorted() throws { let dbManagerMock = DatabaseManagerMock() - let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock) XCTAssert(vaultListViewModel.getVaults().isEmpty) try vaultListViewModel.refreshItems() XCTAssertEqual(2, vaultListViewModel.getVaults().count) @@ -45,7 +47,7 @@ class VaultListViewModelTests: XCTestCase { func testMoveRow() throws { let dbManagerMock = DatabaseManagerMock() - let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock) try vaultListViewModel.refreshItems() XCTAssertEqual(0, vaultListViewModel.getVaults()[0].listPosition) @@ -68,7 +70,7 @@ class VaultListViewModelTests: XCTestCase { try vaultCacheMock.cache(cachedVault) let dbManagerMock = DatabaseManagerMock() - let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock) try vaultListViewModel.refreshItems() XCTAssertEqual(0, vaultListViewModel.getVaults()[0].listPosition) @@ -91,7 +93,7 @@ class VaultListViewModelTests: XCTestCase { func testLockVault() throws { let expectation = XCTestExpectation() let dbManagerMock = DatabaseManagerMock() - let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock) let vaultInfo = VaultInfo(vaultAccount: VaultAccount(vaultUID: "vault1", delegateAccountUID: "1", vaultPath: CloudPath("/vault1"), vaultName: "vault1"), cloudProviderAccount: CloudProviderAccount(accountUID: "1", cloudProviderType: .dropbox), vaultListPosition: VaultListPosition(position: 1, vaultUID: "vault1")) @@ -117,7 +119,7 @@ class VaultListViewModelTests: XCTestCase { func testRefreshVaultLockedStates() throws { let expectation = XCTestExpectation() let dbManagerMock = DatabaseManagerMock() - let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) + let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock) try vaultListViewModel.refreshItems() XCTAssertTrue(vaultListViewModel.getVaults().allSatisfy({ !$0.vaultIsUnlocked.value })) diff --git a/FileProviderExtensionUI/RootViewController.swift b/FileProviderExtensionUI/RootViewController.swift index 1da46a054..99f427bbf 100644 --- a/FileProviderExtensionUI/RootViewController.swift +++ b/FileProviderExtensionUI/RootViewController.swift @@ -10,6 +10,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore import CryptomatorFileProvider +import Dependencies import FileProviderUI import MSAL import Promises @@ -24,6 +25,8 @@ class RootViewController: FPUIActionExtensionViewController { #endif }() + @Dependency(\.fileProviderConnector) private var fileProviderConnector + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) NotificationCenter.default.addObserver(self, @@ -72,7 +75,7 @@ class RootViewController: FPUIActionExtensionViewController { }() func retryUpload(for itemIdentifiers: [NSFileProviderItemIdentifier], domainIdentifier: NSFileProviderDomainIdentifier) { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .uploadRetryingService, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .uploadRetryingService, domainIdentifier: domainIdentifier) getXPCPromise.then { xpc in return wrap { xpc.proxy.retryUpload(for: itemIdentifiers, reply: $0) @@ -85,8 +88,8 @@ class RootViewController: FPUIActionExtensionViewController { }.catch { error in DDLogError("Retry upload failed with error: \(error)") self.extensionContext.cancelRequest(withError: NSError(domain: FPUIErrorDomain, code: Int(FPUIExtensionErrorCode.failed.rawValue), userInfo: nil)) - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } } @@ -98,7 +101,7 @@ class RootViewController: FPUIActionExtensionViewController { } func showUploadProgressAlert(for itemIdentifiers: [NSFileProviderItemIdentifier], domainIdentifier: NSFileProviderDomainIdentifier) { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .uploadRetryingService, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .uploadRetryingService, domainIdentifier: domainIdentifier) let progressAlert = RetryUploadAlertControllerFactory.createUploadProgressAlert(dismissAction: { [weak self] in self?.cancel() }, retryAction: { [weak self] in @@ -108,9 +111,9 @@ class RootViewController: FPUIActionExtensionViewController { let observeProgressPromise = progressAlert.observeProgress(itemIdentifier: itemIdentifiers[0], proxy: xpc.proxy) let alertActionPromise = progressAlert.alertActionTriggered return race([observeProgressPromise, alertActionPromise]) - }.always { + }.always { [fileProviderConnector] in self.extensionContext.completeRequest() - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + fileProviderConnector.invalidateXPC(getXPCPromise) } present(progressAlert, animated: true) } @@ -135,7 +138,7 @@ class RootViewController: FPUIActionExtensionViewController { } func evictFilesFromCache(with itemIdentifiers: [NSFileProviderItemIdentifier], domainIdentifier: NSFileProviderDomainIdentifier) { - let getXPCPromise: Promise> = FileProviderXPCConnector.shared.getXPC(serviceName: .cacheManaging, domainIdentifier: domainIdentifier) + let getXPCPromise: Promise> = fileProviderConnector.getXPC(serviceName: .cacheManaging, domainIdentifier: domainIdentifier) getXPCPromise.then { xpc in xpc.proxy.evictFilesFromCache(with: itemIdentifiers) }.catch { error in @@ -150,8 +153,8 @@ class RootViewController: FPUIActionExtensionViewController { self.present(alertController, animated: true) }.then { self.extensionContext.completeRequest() - }.always { - FileProviderXPCConnector.shared.invalidateXPC(getXPCPromise) + }.always { [fileProviderConnector] in + fileProviderConnector.invalidateXPC(getXPCPromise) } } diff --git a/FileProviderExtensionUI/UnlockVaultViewModel.swift b/FileProviderExtensionUI/UnlockVaultViewModel.swift index 1654283a4..5e4a87c5f 100644 --- a/FileProviderExtensionUI/UnlockVaultViewModel.swift +++ b/FileProviderExtensionUI/UnlockVaultViewModel.swift @@ -11,6 +11,7 @@ import CryptomatorCloudAccessCore import CryptomatorCommonCore import CryptomatorCryptoLib import CryptomatorFileProvider +import Dependencies import FileProvider import FileProviderUI import Foundation @@ -106,7 +107,7 @@ class UnlockVaultViewModel { } }() - private let fileProviderConnector: FileProviderConnector + @Dependency(\.fileProviderConnector) private var fileProviderConnector private let vaultAccountManager: VaultAccountManager private let providerManager: CloudProviderManager private let vaultCache: VaultCache @@ -115,17 +116,15 @@ class UnlockVaultViewModel { public convenience init(domain: NSFileProviderDomain, wrongBiometricalPassword: Bool) { self.init(domain: domain, wrongBiometricalPassword: wrongBiometricalPassword, - fileProviderConnector: FileProviderXPCConnector.shared, passwordManager: VaultPasswordKeychainManager(), vaultAccountManager: VaultAccountDBManager.shared, providerManager: CloudProviderDBManager.shared, vaultCache: VaultDBCache()) } - init(domain: NSFileProviderDomain, wrongBiometricalPassword: Bool, fileProviderConnector: FileProviderConnector, passwordManager: VaultPasswordManager, vaultAccountManager: VaultAccountManager, providerManager: CloudProviderManager, vaultCache: VaultCache) { + init(domain: NSFileProviderDomain, wrongBiometricalPassword: Bool, passwordManager: VaultPasswordManager, vaultAccountManager: VaultAccountManager, providerManager: CloudProviderManager, vaultCache: VaultCache) { self.domain = domain self.wrongBiometricalPassword = wrongBiometricalPassword - self.fileProviderConnector = fileProviderConnector let context = LAContext() if #unavailable(iOS 16) { // Remove fallback title because "Enter password" also closes FileProviderExtensionUI (prior to iOS 16) and does not display the password input From 325491da82b40783cba4a474174745b89dd3faee Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sun, 19 Nov 2023 16:31:35 +0100 Subject: [PATCH 32/45] Moved CryptomatorHubAuthenticator related dependencies to DependencyValues --- .../AddVault/Hub/HubAddVaultCoordinator.swift | 4 -- .../OpenExistingVaultCoordinator.swift | 3 +- ...orHubAuthenticator+HubAuthenticating.swift | 5 ++ .../Hub/CryptomatorHubAuthenticator.swift | 11 +-- .../Hub/HubAuthenticating.swift | 18 +++++ .../Hub/HubAuthenticationCoordinator.swift | 11 +-- .../Hub/HubAuthenticationViewModel.swift | 10 +-- .../Hub/HubDeviceRegisteringService.swift | 67 +++++++++++++++++++ .../Hub/HubKeyService.swift | 65 ++++++++++++++++++ .../Hub/HubXPCLoginCoordinator.swift | 4 -- .../FileProviderCoordinator.swift | 1 - 11 files changed, 168 insertions(+), 31 deletions(-) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteringService.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubKeyService.swift diff --git a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift index 74a4aa367..92cb4af15 100644 --- a/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift +++ b/Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift @@ -23,7 +23,6 @@ class AddHubVaultCoordinator: Coordinator { let vaultUID: String let accountUID: String let vaultItem: VaultItem - let hubAuthenticator: HubAuthenticating let vaultManager: VaultManager weak var parentCoordinator: Coordinator? weak var delegate: (VaultInstalling & AnyObject)? @@ -33,14 +32,12 @@ class AddHubVaultCoordinator: Coordinator { vaultUID: String, accountUID: String, vaultItem: VaultItem, - hubAuthenticator: HubAuthenticating, vaultManager: VaultManager = VaultDBManager.shared) { self.navigationController = navigationController self.downloadedVaultConfig = downloadedVaultConfig self.vaultUID = vaultUID self.accountUID = accountUID self.vaultItem = vaultItem - self.hubAuthenticator = hubAuthenticator self.vaultManager = vaultManager } @@ -52,7 +49,6 @@ class AddHubVaultCoordinator: Coordinator { delegate: self) let child = HubAuthenticationCoordinator(navigationController: navigationController, vaultConfig: downloadedVaultConfig.vaultConfig, - hubAuthenticator: hubAuthenticator, unlockHandler: unlockHandler, parent: self, delegate: self) diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift index cf698a9f9..aa2d9be69 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift @@ -201,8 +201,7 @@ private class AuthenticatedOpenExistingVaultCoordinator: VaultInstalling, Folder downloadedVaultConfig: downloadedVaultConfig, vaultUID: UUID().uuidString, accountUID: account.accountUID, - vaultItem: vaultItem, - hubAuthenticator: CryptomatorHubAuthenticator.shared) + vaultItem: vaultItem) child.parentCoordinator = self child.delegate = self childCoordinators.append(child) diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift index 7e23bcc15..f34c331bd 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift @@ -10,6 +10,7 @@ import Base32 import CryptoKit import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import UIKit enum HubAuthenticationError: Error { @@ -52,3 +53,7 @@ extension CryptomatorHubAuthenticator: HubAuthenticating { }) } } + +extension HubAuthenticatingKey: DependencyKey { + public static var liveValue: HubAuthenticating = CryptomatorHubAuthenticator() +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index d7bea476a..fac25e574 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -18,14 +18,6 @@ public enum HubAuthenticationFlow { case licenseExceeded } -public protocol HubDeviceRegistering { - func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws -} - -public protocol HubKeyReceiving { - func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow -} - public enum CryptomatorHubAuthenticatorError: Error { case unexpectedError case unexpectedResponse @@ -38,7 +30,8 @@ public enum CryptomatorHubAuthenticatorError: Error { public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving { private static let scheme = "hub+" - public static let shared = CryptomatorHubAuthenticator() + + public init() {} public func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { guard let baseURL = createBaseURL(vaultConfig: vaultConfig) else { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticating.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticating.swift index 2b074a44b..c43627ce5 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticating.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticating.swift @@ -1,7 +1,25 @@ import AppAuthCore import CryptomatorCloudAccessCore +import Dependencies import UIKit public protocol HubAuthenticating { func authenticate(with hubConfig: HubConfig, from viewController: UIViewController) async throws -> OIDAuthState } + +public enum HubAuthenticatingKey: TestDependencyKey { + public static var testValue: HubAuthenticating = UnimplementedHubAuthenticatingService() +} + +public extension DependencyValues { + var hubAuthenticationService: HubAuthenticating { + get { self[HubAuthenticatingKey.self] } + set { self[HubAuthenticatingKey.self] = newValue } + } +} + +struct UnimplementedHubAuthenticatingService: HubAuthenticating { + func authenticate(with hubConfig: CryptomatorCloudAccessCore.HubConfig, from viewController: UIViewController) async throws -> OIDAuthState { + unimplemented(placeholder: OIDAuthState(authorizationResponse: .init(request: .init(configuration: .init(authorizationEndpoint: URL(string: "example.com")!, tokenEndpoint: URL(string: "example.com")!), clientId: "", scopes: nil, redirectURL: URL(string: "example.com")!, responseType: "code", additionalParameters: nil), parameters: [:]))) + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift index 66f5d50f6..9e700e326 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift @@ -1,5 +1,6 @@ import AppAuthCore import CryptomatorCloudAccessCore +import Dependencies import SwiftUI import UIKit @@ -17,20 +18,18 @@ public final class HubAuthenticationCoordinator: Coordinator { public weak var parent: Coordinator? private let vaultConfig: UnverifiedVaultConfig - private let hubAuthenticator: HubAuthenticating private var progressHUD: ProgressHUD? private let unlockHandler: HubVaultUnlockHandler + @Dependency(\.hubAuthenticationService) var hubAuthenticator private weak var delegate: HubAuthenticationCoordinatorDelegate? public init(navigationController: UINavigationController, vaultConfig: UnverifiedVaultConfig, - hubAuthenticator: HubAuthenticating, unlockHandler: HubVaultUnlockHandler, parent: Coordinator?, delegate: HubAuthenticationCoordinatorDelegate) { self.navigationController = navigationController self.vaultConfig = vaultConfig - self.hubAuthenticator = hubAuthenticator self.unlockHandler = unlockHandler self.parent = parent self.delegate = delegate @@ -86,7 +85,11 @@ public final class HubAuthenticationCoordinator: Coordinator { private func hideProgressHUD() async { await withCheckedContinuation { continuation in - progressHUD?.dismiss(animated: true, completion: { [weak self] in + guard let progressHUD else { + continuation.resume() + return + } + progressHUD.dismiss(animated: true, completion: { [weak self] in continuation.resume() self?.progressHUD = nil }) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index 4c060bccd..775ee658d 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -2,6 +2,7 @@ import AppAuthCore import CocoaLumberjackSwift import CryptoKit import CryptomatorCloudAccessCore +import Dependencies import Foundation import JOSESwift import UIKit @@ -43,23 +44,18 @@ public final class HubAuthenticationViewModel: ObservableObject { private(set) var isLoggedIn = false private let vaultConfig: UnverifiedVaultConfig - private let deviceRegisteringService: HubDeviceRegistering - private let hubKeyService: HubKeyReceiving - private let authState: OIDAuthState private let unlockHandler: HubVaultUnlockHandler + @Dependency(\.hubDeviceRegisteringService) var deviceRegisteringService + @Dependency(\.hubKeyService) var hubKeyService private weak var delegate: HubAuthenticationViewModelDelegate? public init(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig, - deviceRegisteringService: HubDeviceRegistering = CryptomatorHubAuthenticator.shared, - hubKeyService: HubKeyReceiving = CryptomatorHubAuthenticator.shared, unlockHandler: HubVaultUnlockHandler, delegate: HubAuthenticationViewModelDelegate) { self.authState = authState self.vaultConfig = vaultConfig - self.deviceRegisteringService = deviceRegisteringService - self.hubKeyService = hubKeyService self.unlockHandler = unlockHandler self.delegate = delegate } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteringService.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteringService.swift new file mode 100644 index 000000000..b1bd034f5 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteringService.swift @@ -0,0 +1,67 @@ +// +// HubDeviceRegisteringService.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 19.11.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import AppAuthCore +import CryptomatorCloudAccessCore +import Dependencies +import Foundation +import XCTestDynamicOverlay + +public protocol HubDeviceRegistering { + func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws +} + +private enum HubDeviceRegisteringKey: DependencyKey { + static var liveValue: HubDeviceRegistering = CryptomatorHubAuthenticator() + #if DEBUG + static var testValue: HubDeviceRegistering = UnimplementedHubDeviceRegisteringService() + #endif +} + +extension DependencyValues { + var hubDeviceRegisteringService: HubDeviceRegistering { + get { self[HubDeviceRegisteringKey.self] } + set { self[HubDeviceRegisteringKey.self] = newValue } + } +} + +#if DEBUG +final class UnimplementedHubDeviceRegisteringService: HubDeviceRegistering { + func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws { + XCTFail("\(Self.self).registerDevice is unimplemented.") + } +} + +// MARK: - HubDeviceRegisteringMock - + +// swiftlint: disable all +final class HubDeviceRegisteringMock: HubDeviceRegistering { + // MARK: - registerDevice + + var registerDeviceWithNameHubConfigAuthStateThrowableError: Error? + var registerDeviceWithNameHubConfigAuthStateCallsCount = 0 + var registerDeviceWithNameHubConfigAuthStateCalled: Bool { + registerDeviceWithNameHubConfigAuthStateCallsCount > 0 + } + + var registerDeviceWithNameHubConfigAuthStateReceivedArguments: (name: String, hubConfig: HubConfig, authState: OIDAuthState)? + var registerDeviceWithNameHubConfigAuthStateReceivedInvocations: [(name: String, hubConfig: HubConfig, authState: OIDAuthState)] = [] + var registerDeviceWithNameHubConfigAuthStateClosure: ((String, HubConfig, OIDAuthState) throws -> Void)? + + func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) throws { + if let error = registerDeviceWithNameHubConfigAuthStateThrowableError { + throw error + } + registerDeviceWithNameHubConfigAuthStateCallsCount += 1 + registerDeviceWithNameHubConfigAuthStateReceivedArguments = (name: name, hubConfig: hubConfig, authState: authState) + registerDeviceWithNameHubConfigAuthStateReceivedInvocations.append((name: name, hubConfig: hubConfig, authState: authState)) + try registerDeviceWithNameHubConfigAuthStateClosure?(name, hubConfig, authState) + } +} +// swiftlint: enable all +#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubKeyService.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubKeyService.swift new file mode 100644 index 000000000..d156d04fb --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubKeyService.swift @@ -0,0 +1,65 @@ +// +// HubKeyService.swift +// CryptomatorCommonCore +// +// Created by Philipp Schmid on 19.11.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. +// + +import AppAuthCore +import CryptomatorCloudAccessCore +import Dependencies +import Foundation + +public protocol HubKeyReceiving { + func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow +} + +private enum HubKeyReceivingDependencyKey: DependencyKey { + static let liveValue: HubKeyReceiving = CryptomatorHubAuthenticator() + #if DEBUG + static let testValue: HubKeyReceiving = UnimplementedHubKeyReceivingService() + #endif +} + +extension DependencyValues { + var hubKeyService: HubKeyReceiving { + get { self[HubKeyReceivingDependencyKey.self] } + set { self[HubKeyReceivingDependencyKey.self] = newValue } + } +} + +#if DEBUG +final class UnimplementedHubKeyReceivingService: HubKeyReceiving { + func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { + unimplemented(placeholder: .accessNotGranted) + } +} + +// MARK: - HubKeyReceivingMock - + +final class HubKeyReceivingMock: HubKeyReceiving { + // MARK: - receiveKey + + var receiveKeyAuthStateVaultConfigThrowableError: Error? + var receiveKeyAuthStateVaultConfigCallsCount = 0 + var receiveKeyAuthStateVaultConfigCalled: Bool { + receiveKeyAuthStateVaultConfigCallsCount > 0 + } + + var receiveKeyAuthStateVaultConfigReceivedArguments: (authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig)? + var receiveKeyAuthStateVaultConfigReceivedInvocations: [(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig)] = [] + var receiveKeyAuthStateVaultConfigReturnValue: HubAuthenticationFlow! + var receiveKeyAuthStateVaultConfigClosure: ((OIDAuthState, UnverifiedVaultConfig) throws -> HubAuthenticationFlow)? + + func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) throws -> HubAuthenticationFlow { + if let error = receiveKeyAuthStateVaultConfigThrowableError { + throw error + } + receiveKeyAuthStateVaultConfigCallsCount += 1 + receiveKeyAuthStateVaultConfigReceivedArguments = (authState: authState, vaultConfig: vaultConfig) + receiveKeyAuthStateVaultConfigReceivedInvocations.append((authState: authState, vaultConfig: vaultConfig)) + return try receiveKeyAuthStateVaultConfigClosure.map({ try $0(authState, vaultConfig) }) ?? receiveKeyAuthStateVaultConfigReturnValue + } +} +#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift index ffcaaeddd..3f58f2a85 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubXPCLoginCoordinator.swift @@ -12,7 +12,6 @@ public final class HubXPCLoginCoordinator: Coordinator { public var navigationController: UINavigationController let domain: NSFileProviderDomain let vaultConfig: UnverifiedVaultConfig - let hubAuthenticator: HubAuthenticating public let onUnlocked: () -> Void public let onErrorAlertDismissed: () -> Void @Dependency(\.hubRepository) private var hubRepository @@ -21,13 +20,11 @@ public final class HubXPCLoginCoordinator: Coordinator { public init(navigationController: UINavigationController, domain: NSFileProviderDomain, vaultConfig: UnverifiedVaultConfig, - hubAuthenticator: HubAuthenticating, onUnlocked: @escaping () -> Void, onErrorAlertDismissed: @escaping () -> Void) { self.navigationController = navigationController self.domain = domain self.vaultConfig = vaultConfig - self.hubAuthenticator = hubAuthenticator self.onUnlocked = onUnlocked self.onErrorAlertDismissed = onErrorAlertDismissed } @@ -37,7 +34,6 @@ public final class HubXPCLoginCoordinator: Coordinator { prepareNavigationControllerForLogin() let child = HubAuthenticationCoordinator(navigationController: navigationController, vaultConfig: vaultConfig, - hubAuthenticator: hubAuthenticator, unlockHandler: unlockHandler, parent: self, delegate: self) diff --git a/FileProviderExtensionUI/FileProviderCoordinator.swift b/FileProviderExtensionUI/FileProviderCoordinator.swift index 9683ca67d..40bc0ff09 100644 --- a/FileProviderExtensionUI/FileProviderCoordinator.swift +++ b/FileProviderExtensionUI/FileProviderCoordinator.swift @@ -163,7 +163,6 @@ class FileProviderCoordinator: Coordinator { let child = HubXPCLoginCoordinator(navigationController: navigationController, domain: domain, vaultConfig: vaultConfig, - hubAuthenticator: CryptomatorHubAuthenticator.shared, onUnlocked: { [weak self] in self?.done() }, onErrorAlertDismissed: { [weak self] in self?.done() }) childCoordinators.append(child) From c4d84700a4f8ab7b0e22c7f9a225bf46b7293c86 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Mon, 20 Nov 2023 00:42:24 +0100 Subject: [PATCH 33/45] Add HubAuthenticationViewModelTests --- .../Hub/CryptomatorHubAuthenticator.swift | 6 +- .../Hub/CryptomatorHubKeyProvider.swift | 72 ++++- .../Hub/HubAuthenticationViewModel.swift | 4 +- .../UnlockHandler/HubVaultUnlockHandler.swift | 26 ++ .../Hub/HubAuthenticationViewModelTests.swift | 290 ++++++++++++++++++ 5 files changed, 393 insertions(+), 5 deletions(-) create mode 100644 CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index fac25e574..f32906807 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -9,6 +9,7 @@ import AppAuthCore import CryptoKit import CryptomatorCloudAccessCore +import Dependencies import Foundation public enum HubAuthenticationFlow { @@ -30,6 +31,7 @@ public enum CryptomatorHubAuthenticatorError: Error { public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving { private static let scheme = "hub+" + @Dependency(\.cryptomatorHubKeyProvider) private var cryptomatorHubKeyProvider public init() {} @@ -63,7 +65,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving public func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws { let deviceID = try getDeviceID() - let publicKey = try CryptomatorHubKeyProvider.shared.getPublicKey() + let publicKey = try cryptomatorHubKeyProvider.getPublicKey() let derPubKey = publicKey.derRepresentation let dto = CreateDeviceDto(id: deviceID, name: name, type: "MOBILE", publicKey: derPubKey.base64URLEncodedString()) guard let devicesResourceURL = URL(string: hubConfig.devicesResourceUrl) else { @@ -99,7 +101,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving } func getDeviceID() throws -> String { - let publicKey = try CryptomatorHubKeyProvider.shared.getPublicKey() + let publicKey = try cryptomatorHubKeyProvider.getPublicKey() let digest = SHA256.hash(data: publicKey.derRepresentation) return digest.data.base16EncodedString } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubKeyProvider.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubKeyProvider.swift index eb87de68f..845a96232 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubKeyProvider.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubKeyProvider.swift @@ -7,10 +7,31 @@ // import CryptoKit +import Dependencies import Foundation -public struct CryptomatorHubKeyProvider { - public static let shared: CryptomatorHubKeyProvider = .init(keychain: CryptomatorKeychain.hub) +protocol CryptomatorHubKeyProvider { + func getPublicKey() throws -> P384.KeyAgreement.PublicKey + + func getPrivateKey() throws -> P384.KeyAgreement.PrivateKey +} + +private enum CryptomatorHubKeyProviderKey: DependencyKey { + static let liveValue: CryptomatorHubKeyProvider = CryptomatorHubKeyProviderImpl(keychain: CryptomatorKeychain.hub) + #if DEBUG + static let testValue: CryptomatorHubKeyProvider = CryptomatorHubKeyProviderMock() + #endif +} + +extension DependencyValues { + var cryptomatorHubKeyProvider: CryptomatorHubKeyProvider { + get { self[CryptomatorHubKeyProviderKey.self] } + set { self[CryptomatorHubKeyProviderKey.self] = newValue } + } +} + +public struct CryptomatorHubKeyProviderImpl: CryptomatorHubKeyProvider { + public static let shared: CryptomatorHubKeyProviderImpl = .init(keychain: CryptomatorKeychain.hub) let keychain: CryptomatorKeychainType private let keychainKey = "privateKey" @@ -38,3 +59,50 @@ public struct CryptomatorHubKeyProvider { try? keychain.delete(keychainKey) } } + +#if DEBUG + +// MARK: - CryptomatorHubKeyProviderMock - + +// swiftlint: disable all +final class CryptomatorHubKeyProviderMock: CryptomatorHubKeyProvider { + // MARK: - getPublicKey + + var getPublicKeyThrowableError: Error? + var getPublicKeyCallsCount = 0 + var getPublicKeyCalled: Bool { + getPublicKeyCallsCount > 0 + } + + var getPublicKeyReturnValue: P384.KeyAgreement.PublicKey! + var getPublicKeyClosure: (() throws -> P384.KeyAgreement.PublicKey)? + + func getPublicKey() throws -> P384.KeyAgreement.PublicKey { + if let error = getPublicKeyThrowableError { + throw error + } + getPublicKeyCallsCount += 1 + return try getPublicKeyClosure.map({ try $0() }) ?? getPublicKeyReturnValue + } + + // MARK: - getPrivateKey + + var getPrivateKeyThrowableError: Error? + var getPrivateKeyCallsCount = 0 + var getPrivateKeyCalled: Bool { + getPrivateKeyCallsCount > 0 + } + + var getPrivateKeyReturnValue: P384.KeyAgreement.PrivateKey! + var getPrivateKeyClosure: (() throws -> P384.KeyAgreement.PrivateKey)? + + func getPrivateKey() throws -> P384.KeyAgreement.PrivateKey { + if let error = getPrivateKeyThrowableError { + throw error + } + getPrivateKeyCallsCount += 1 + return try getPrivateKeyClosure.map({ try $0() }) ?? getPrivateKeyReturnValue + } +} +// swiftlint: enable all +#endif diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index 775ee658d..197f351f3 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -48,6 +48,7 @@ public final class HubAuthenticationViewModel: ObservableObject { private let unlockHandler: HubVaultUnlockHandler @Dependency(\.hubDeviceRegisteringService) var deviceRegisteringService @Dependency(\.hubKeyService) var hubKeyService + @Dependency(\.cryptomatorHubKeyProvider) var cryptomatorHubKeyProvider private weak var delegate: HubAuthenticationViewModelDelegate? public init(authState: OIDAuthState, @@ -108,7 +109,7 @@ public final class HubAuthenticationViewModel: ObservableObject { let jwe: JWE let subscriptionState: HubSubscriptionState do { - privateKey = try CryptomatorHubKeyProvider.shared.getPrivateKey() + privateKey = try cryptomatorHubKeyProvider.getPrivateKey() jwe = try JWE(compactSerialization: data) subscriptionState = try getSubscriptionState(from: header) } catch { @@ -128,6 +129,7 @@ public final class HubAuthenticationViewModel: ObservableObject { } private func setStateToErrorState(with error: Error) async { + await delegate?.hubAuthenticationViewModelWantsToHideLoadingIndicator() await setState(to: .error(description: error.localizedDescription)) } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubVaultUnlockHandler.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubVaultUnlockHandler.swift index bec6336b5..b99e7fc77 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubVaultUnlockHandler.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubVaultUnlockHandler.swift @@ -10,3 +10,29 @@ public protocol HubVaultUnlockHandlerDelegate: AnyObject { @MainActor func failedToProcessUnlockedVault(error: Error) } + +// MARK: - HubVaultUnlockHandlerMock - + +#if DEBUG +// swiftlint: disable all +final class HubVaultUnlockHandlerMock: HubVaultUnlockHandler { + // MARK: - didSuccessfullyRemoteUnlock + + var didSuccessfullyRemoteUnlockCallsCount = 0 + var didSuccessfullyRemoteUnlockCalled: Bool { + didSuccessfullyRemoteUnlockCallsCount > 0 + } + + var didSuccessfullyRemoteUnlockReceivedResponse: HubUnlockResponse? + var didSuccessfullyRemoteUnlockReceivedInvocations: [HubUnlockResponse] = [] + var didSuccessfullyRemoteUnlockClosure: ((HubUnlockResponse) -> Void)? + + func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) { + didSuccessfullyRemoteUnlockCallsCount += 1 + didSuccessfullyRemoteUnlockReceivedResponse = response + didSuccessfullyRemoteUnlockReceivedInvocations.append(response) + didSuccessfullyRemoteUnlockClosure?(response) + } +} +// / swiftlint: enable all +#endif diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift new file mode 100644 index 000000000..1714cdd82 --- /dev/null +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift @@ -0,0 +1,290 @@ +// +// HubAuthenticationViewModelTests.swift +// +// +// Created by Philipp Schmid on 19.11.23. +// + +import AppAuthCore +import CryptoKit +import XCTest +@testable import CryptomatorCloudAccessCore +@testable import CryptomatorCommonCore +@testable import CryptomatorCryptoLib +@testable import Dependencies + +final class HubAuthenticationViewModelTests: XCTestCase { + private var unlockHandlerMock: HubVaultUnlockHandlerMock! + private var delegateMock: HubAuthenticationViewModelDelegateMock! + private var hubKeyServiceMock: HubKeyReceivingMock! + private var viewModel: HubAuthenticationViewModel! + + override func setUpWithError() throws { + unlockHandlerMock = HubVaultUnlockHandlerMock() + delegateMock = HubAuthenticationViewModelDelegateMock() + hubKeyServiceMock = HubKeyReceivingMock() + + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: validHubVaultConfig()) + + viewModel = HubAuthenticationViewModel(authState: .stub, + vaultConfig: unverifiedVaultConfig, + unlockHandler: unlockHandlerMock, + delegate: delegateMock) + } + + // MARK: continueToAccessCheck + + func testContinueToAccessCheck_showsLoadingSpinnerWhileReceivingKey() async throws { + XCTAssertFalse(delegateMock.hubAuthenticationViewModelWantsToShowLoadingIndicatorCalled) + XCTAssertFalse(delegateMock.hubAuthenticationViewModelWantsToHideLoadingIndicatorCalled) + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + let calledReceiveKey = XCTestExpectation() + hubKeyServiceMock.receiveKeyAuthStateVaultConfigClosure = { _, _ in + calledReceiveKey.fulfill() + return .success(Data(), [:]) + } + + let calledShowLoadingIndicator = XCTestExpectation() + delegateMock.hubAuthenticationViewModelWantsToShowLoadingIndicatorClosure = { + calledShowLoadingIndicator.fulfill() + } + + let calledHideLoadingIndicator = XCTestExpectation() + delegateMock.hubAuthenticationViewModelWantsToHideLoadingIndicatorClosure = { + calledHideLoadingIndicator.fulfill() + } + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the loading indicator should be displayed while receiving the key + await fulfillment(of: [calledShowLoadingIndicator, calledReceiveKey, calledHideLoadingIndicator], enforceOrder: true) + } + + func testContinueToAccessCheck_showsLoadingSpinnerWhileReceivingKeyHidesIfFailed() async throws { + XCTAssertFalse(delegateMock.hubAuthenticationViewModelWantsToShowLoadingIndicatorCalled) + XCTAssertFalse(delegateMock.hubAuthenticationViewModelWantsToHideLoadingIndicatorCalled) + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + let calledReceiveKey = XCTestExpectation() + hubKeyServiceMock.receiveKeyAuthStateVaultConfigClosure = { _, _ in + calledReceiveKey.fulfill() + throw TestError() + } + + let calledShowLoadingIndicator = XCTestExpectation() + delegateMock.hubAuthenticationViewModelWantsToShowLoadingIndicatorClosure = { + calledShowLoadingIndicator.fulfill() + } + + let calledHideLoadingIndicator = XCTestExpectation() + delegateMock.hubAuthenticationViewModelWantsToHideLoadingIndicatorClosure = { + calledHideLoadingIndicator.fulfill() + } + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the loading indicator should be displayed while receiving the key and gets hidden even if the operation fails + await fulfillment(of: [calledShowLoadingIndicator, calledReceiveKey, calledHideLoadingIndicator], enforceOrder: true) + } + + func testContinueToAccessCheck_success_hubSubscriptionStateIsActive() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + let hubKeyProviderMock = CryptomatorHubKeyProviderMock() + DependencyValues.mockDependency(\.cryptomatorHubKeyProvider, with: hubKeyProviderMock) + + // GIVEN + // the hub key service returns success with an active Cryptomator Hub subscription state + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = .success(validHubResponseData(), ["hub-subscription-state": "ACTIVE"]) + hubKeyProviderMock.getPrivateKeyReturnValue = P384.KeyAgreement.PrivateKey(compactRepresentable: false) + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the unlock handler gets informed about the successful remote unlock with an active Cryptomator Hub subscription state + let receivedResponse = unlockHandlerMock.didSuccessfullyRemoteUnlockReceivedResponse + XCTAssertEqual(unlockHandlerMock.didSuccessfullyRemoteUnlockCallsCount, 1) + XCTAssertEqual(receivedResponse?.subscriptionState, .active) + } + + func testContinueToAccessCheck_success_hubSubscriptionStateIsInactive() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + let hubKeyProviderMock = CryptomatorHubKeyProviderMock() + DependencyValues.mockDependency(\.cryptomatorHubKeyProvider, with: hubKeyProviderMock) + + // GIVEN + // the hub key service returns success with an inactive Cryptomator Hub subscription state + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = .success(validHubResponseData(), ["hub-subscription-state": "INACTIVE"]) + hubKeyProviderMock.getPrivateKeyReturnValue = P384.KeyAgreement.PrivateKey(compactRepresentable: false) + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the unlock handler gets informed about the successful remote unlock with an inactive Cryptomator Hub subscription state + let receivedResponse = unlockHandlerMock.didSuccessfullyRemoteUnlockReceivedResponse + XCTAssertEqual(unlockHandlerMock.didSuccessfullyRemoteUnlockCallsCount, 1) + XCTAssertEqual(receivedResponse?.subscriptionState, .inactive) + } + + func testContinueToAccessCheck_success_hubSubscriptionStateIsUnknown() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + let hubKeyProviderMock = CryptomatorHubKeyProviderMock() + DependencyValues.mockDependency(\.cryptomatorHubKeyProvider, with: hubKeyProviderMock) + + // GIVEN + // the hub key service returns success with an unknown Cryptomator Hub subscription state + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = .success(validHubResponseData(), ["hub-subscription-state": "FOO"]) + hubKeyProviderMock.getPrivateKeyReturnValue = P384.KeyAgreement.PrivateKey(compactRepresentable: false) + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the unlock handler gets not informed about a successful remote unlock + XCTAssertFalse(unlockHandlerMock.didSuccessfullyRemoteUnlockCalled) + // the user gets informed about the error + let currentAuthenticationFlowState = try XCTUnwrap(viewModel.authenticationFlowState) + XCTAssert(currentAuthenticationFlowState.isError) + } + + func testContinueToAccessCheck_accessNotGranted() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + + // GIVEN + // the hub key service returns access not granted + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = .accessNotGranted + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the authentication flow state is set to accessNotGranted + XCTAssertEqual(viewModel.authenticationFlowState, .accessNotGranted) + } + + func testContinueToAccessCheck_needsDeviceRegistration() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + + // GIVEN + // the hub key service returns needs device registration + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = .needsDeviceRegistration + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the authentication flow state is set to needsDeviceRegistration where the user needs to set the device name + XCTAssertEqual(viewModel.authenticationFlowState, .deviceRegistration(.deviceName)) + } + + func testContinueToAccessCheck_licenseExceeded() async throws { + DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + + // GIVEN + // the hub key service returns that the Cryptomator Hub License is exceeded + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = .licenseExceeded + + // WHEN + // continue the access check + await viewModel.continueToAccessCheck() + + // THEN + // the authentication flow state is set to licenseExceeded + XCTAssertEqual(viewModel.authenticationFlowState, .licenseExceeded) + } + + // MARK: Register + + func testRegister_registersDevice_withName() async { + let deviceRegisteringMock = HubDeviceRegisteringMock() + DependencyValues.mockDependency(\.hubDeviceRegisteringService, with: deviceRegisteringMock) + + // GIVEN + // a name has been set by the user + viewModel.deviceName = "My Device 123" + + // WHEN + // the user taps on register + await viewModel.register() + + // THEN + // the registerDevice got called on the device registering servie + let receivedArguments = deviceRegisteringMock.registerDeviceWithNameHubConfigAuthStateReceivedArguments + XCTAssertEqual(deviceRegisteringMock.registerDeviceWithNameHubConfigAuthStateCallsCount, 1) + // with the name set by the user + XCTAssertEqual(receivedArguments?.name, "My Device 123") + } + + private struct TestError: Error {} + + private func validHubVaultConfig() -> Data { + "eyJraWQiOiJodWIraHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMjkvYXBpL3ZhdWx0cy9mYjUzMDdmMC1jOWI4LTRjNWYtYjJiMi03ZDM4ODE4ZjZhNGIiLCJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiIsImh1YiI6eyJjbGllbnRJZCI6ImNyeXB0b21hdG9yIiwiYXV0aEVuZHBvaW50IjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcva2MvcmVhbG1zL2h1YjI5L3Byb3RvY29sL29wZW5pZC1jb25uZWN0L2F1dGgiLCJ0b2tlbkVuZHBvaW50IjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcva2MvcmVhbG1zL2h1YjI5L3Byb3RvY29sL29wZW5pZC1jb25uZWN0L3Rva2VuIiwiZGV2aWNlc1Jlc291cmNlVXJsIjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMjkvYXBpL2RldmljZXMvIiwiYXV0aFN1Y2Nlc3NVcmwiOiJodHRwczovL3Rlc3RpbmcuaHViLmNyeXB0b21hdG9yLm9yZy9odWIyOS9hcHAvdW5sb2NrLXN1Y2Nlc3M_dmF1bHQ9ZmI1MzA3ZjAtYzliOC00YzVmLWIyYjItN2QzODgxOGY2YTRiIiwiYXV0aEVycm9yVXJsIjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMjkvYXBwL3VubG9jay1lcnJvcj92YXVsdD1mYjUzMDdmMC1jOWI4LTRjNWYtYjJiMi03ZDM4ODE4ZjZhNGIifX0.eyJqdGkiOiJmYjUzMDdmMC1jOWI4LTRjNWYtYjJiMi03ZDM4ODE4ZjZhNGIiLCJmb3JtYXQiOjgsImNpcGhlckNvbWJvIjoiU0lWX0dDTSIsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMH0.2iFWE4Jj5lV6iaVTPOzGovnrNreuuAJCy_gPmK90MMU".data(using: .utf8)! + } + + private func validHubResponseData() -> Data { + "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJjcnYiOiJQLTM4NCIsImV4dCI6dHJ1ZSwia2V5X29wcyI6W10sImt0eSI6IkVDIiwieCI6Im9DLWlIcDhjZzVsUy1Qd3JjRjZxS0NzbWxfMFJzaEtCV0JJTUYzVjhuTGg2NGlCWTdsX0VsZ3Fjd0JZLXNsR3IiLCJ5IjoiVWozVzdYYVBQakJiMFRwWUFHeXlweVRIR3ByQU1hRXdWTk5Gb05tNEJuNjZuVkNKLU9pUUJYN3RhaVUtby1yWSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ.._r7LC8HLc00jk2SI.ooeI0-E29jryMJ_wbGWKVc_IfHOh3Mlfh5geRYEmLTA4GKHItRYmDdZvGsCj9pJRoNORyHdmlAMxXXIXq_v9ZocoCwZrN7EsaB8A3Kukka35i1sr7kpNbksk3G_COsGRmwQ.GJCKBE-OZ7Nm5RMf_9UwVg".data(using: .utf8)! + } +} + +private extension OIDAuthState { + static var stub: Self { + .init(authorizationResponse: .init(request: .init(configuration: .init(authorizationEndpoint: URL(string: "example.com")!, tokenEndpoint: URL(string: "example.com")!), clientId: "", scopes: nil, redirectURL: URL(string: "example.com")!, responseType: "code", additionalParameters: nil), parameters: [:])) + } +} + +private extension HubAuthenticationViewModel.State { + var isError: Bool { + switch self { + case .error: + return true + default: + return false + } + } +} + +// MARK: - HubAuthenticationViewModelDelegateMock - + +// swiftlint: disable all +final class HubAuthenticationViewModelDelegateMock: HubAuthenticationViewModelDelegate { + // MARK: - hubAuthenticationViewModelWantsToShowLoadingIndicator + + var hubAuthenticationViewModelWantsToShowLoadingIndicatorCallsCount = 0 + var hubAuthenticationViewModelWantsToShowLoadingIndicatorCalled: Bool { + hubAuthenticationViewModelWantsToShowLoadingIndicatorCallsCount > 0 + } + + var hubAuthenticationViewModelWantsToShowLoadingIndicatorClosure: (() -> Void)? + + func hubAuthenticationViewModelWantsToShowLoadingIndicator() { + hubAuthenticationViewModelWantsToShowLoadingIndicatorCallsCount += 1 + hubAuthenticationViewModelWantsToShowLoadingIndicatorClosure?() + } + + // MARK: - hubAuthenticationViewModelWantsToHideLoadingIndicator + + var hubAuthenticationViewModelWantsToHideLoadingIndicatorCallsCount = 0 + var hubAuthenticationViewModelWantsToHideLoadingIndicatorCalled: Bool { + hubAuthenticationViewModelWantsToHideLoadingIndicatorCallsCount > 0 + } + + var hubAuthenticationViewModelWantsToHideLoadingIndicatorClosure: (() -> Void)? + + func hubAuthenticationViewModelWantsToHideLoadingIndicator() { + hubAuthenticationViewModelWantsToHideLoadingIndicatorCallsCount += 1 + hubAuthenticationViewModelWantsToHideLoadingIndicatorClosure?() + } +} + +// swiftlint: enable all From a5696ac5d8bd2abbd45a38d23000c7735c7ab59b Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Mon, 20 Nov 2023 00:49:35 +0100 Subject: [PATCH 34/45] Use Xcode 14.3.1 --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54a523199..519947f7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: jobs: build: name: Build and test - runs-on: macos-12 + runs-on: macos-13 env: DERIVED_DATA_PATH: 'DerivedData' DEVICE: 'iPhone 12 Pro' @@ -26,6 +26,8 @@ jobs: run: | ./Scripts/process.sh exit $? + - name: Select Xcode 14.3.1 + run: sudo xcode-select -s /Applications/Xcode_14.3.1.app - name: Create CloudAccessSecrets run: | cd fastlane From 467018f9a65df8f61d9803fe2e5777240cb68e77 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 21 Nov 2023 11:43:48 +0100 Subject: [PATCH 35/45] Run swiftformat --- .../ChangePasswordViewModel.swift | 26 ++++++++----------- .../VaultDetail/VaultDetailViewModel.swift | 10 +++---- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift b/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift index 265e6350e..ad659fb43 100644 --- a/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift +++ b/Cryptomator/VaultDetail/ChangePassword/ChangePasswordViewModel.swift @@ -61,21 +61,17 @@ class ChangePasswordViewModel: TableViewModel, ChangePass return _sections } - lazy var cells: [ChangePasswordSection: [BindableTableViewCellViewModel]] = { - return [ - .oldPassword: [oldPasswordCellViewModel], - .newPassword: [newPasswordCellViewModel], - .newPasswordConfirmation: [newPasswordConfirmationCellViewModel] - ] - }() - - private lazy var _sections: [Section] = { - return [ - Section(id: .oldPassword, elements: [oldPasswordCellViewModel]), - Section(id: .newPassword, elements: [newPasswordCellViewModel]), - Section(id: .newPasswordConfirmation, elements: [newPasswordConfirmationCellViewModel]) - ] - }() + lazy var cells: [ChangePasswordSection: [BindableTableViewCellViewModel]] = [ + .oldPassword: [oldPasswordCellViewModel], + .newPassword: [newPasswordCellViewModel], + .newPasswordConfirmation: [newPasswordConfirmationCellViewModel] + ] + + private lazy var _sections: [Section] = [ + Section(id: .oldPassword, elements: [oldPasswordCellViewModel]), + Section(id: .newPassword, elements: [newPasswordCellViewModel]), + Section(id: .newPasswordConfirmation, elements: [newPasswordConfirmationCellViewModel]) + ] private static let minimumPasswordLength = 8 private let vaultAccount: VaultAccount diff --git a/Cryptomator/VaultDetail/VaultDetailViewModel.swift b/Cryptomator/VaultDetail/VaultDetailViewModel.swift index 196707819..ef21377cd 100644 --- a/Cryptomator/VaultDetail/VaultDetailViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailViewModel.swift @@ -137,12 +137,10 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { } } - private lazy var sectionFooter: [VaultDetailSection: HeaderFooterViewModel] = { - [.vaultInfoSection: VaultDetailInfoFooterViewModel(vault: vaultInfo), - .changeVaultPasswordSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.changePassword.footer")), - .lockingSection: unlockSectionFooterViewModel, - .removeVaultSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.removeVault.footer"))] - }() + private lazy var sectionFooter: [VaultDetailSection: HeaderFooterViewModel] = [.vaultInfoSection: VaultDetailInfoFooterViewModel(vault: vaultInfo), + .changeVaultPasswordSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.changePassword.footer")), + .lockingSection: unlockSectionFooterViewModel, + .removeVaultSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.removeVault.footer"))] private lazy var unlockSectionFooterViewModel = UnlockSectionFooterViewModel(vaultUnlocked: vaultInfo.vaultIsUnlocked.value, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: context.enrolledBiometricsAuthenticationName(), keepUnlockedDuration: currentKeepUnlockedDuration.value) From 3b28346da8ad8721f0c77753d4bec9ce637b7c67 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 21 Nov 2023 19:22:15 +0100 Subject: [PATCH 36/45] Use Xcode 15.0.1 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 519947f7f..13a95f5ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,8 +26,8 @@ jobs: run: | ./Scripts/process.sh exit $? - - name: Select Xcode 14.3.1 - run: sudo xcode-select -s /Applications/Xcode_14.3.1.app + - name: Select Xcode 15.0.1 + run: sudo xcode-select -s /Applications/Xcode_15.0.1.app - name: Create CloudAccessSecrets run: | cd fastlane From 27c9e27aebb148b97fe2edb1e6f0d2406171a65b Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 21 Nov 2023 19:31:03 +0100 Subject: [PATCH 37/45] Use iPhone 15 Pro --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13a95f5ea..8ddea7684 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ jobs: runs-on: macos-13 env: DERIVED_DATA_PATH: 'DerivedData' - DEVICE: 'iPhone 12 Pro' + DEVICE: 'iPhone 15 Pro' if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" strategy: matrix: From 6c7b24147965f3e10f61fd96cd3f52f4f139086b Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 21 Nov 2023 19:31:20 +0100 Subject: [PATCH 38/45] Removed unused localizations --- SharedResources/en.lproj/Localizable.strings | 2 -- 1 file changed, 2 deletions(-) diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 95b58e62b..f98e2720a 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -115,10 +115,8 @@ "getFolderIntent.error.noVaultSelected" = "No vault has been selected."; "hubAuthentication.title" = "Hub Vault"; -"hubAuthentication.loading" = "Cryptomator is receiving and processing the response from Hub. Please wait."; "hubAuthentication.accessNotGranted" = "Your device has not yet been authorized to access this vault. Ask the vault owner to authorize it."; "hubAuthentication.licenseExceeded" = "Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license."; -"hubAuthentication.deviceRegistration." = ""; "hubAuthentication.deviceRegistration.deviceName.cells.name" = "Device Name"; "hubAuthentication.deviceRegistration.deviceName.footer.title" = "This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device."; "hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Register Device Successful"; From 575960f2e09cb03785671eba032afd401c6f1cf3 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 21 Nov 2023 21:11:44 +0100 Subject: [PATCH 39/45] Fix missing mocked value --- .../Hub/HubAuthenticationViewModelTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift index 1714cdd82..7f0266703 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift @@ -38,6 +38,10 @@ final class HubAuthenticationViewModelTests: XCTestCase { XCTAssertFalse(delegateMock.hubAuthenticationViewModelWantsToShowLoadingIndicatorCalled) XCTAssertFalse(delegateMock.hubAuthenticationViewModelWantsToHideLoadingIndicatorCalled) DependencyValues.mockDependency(\.hubKeyService, with: hubKeyServiceMock) + let hubKeyProviderMock = CryptomatorHubKeyProviderMock() + DependencyValues.mockDependency(\.cryptomatorHubKeyProvider, with: hubKeyProviderMock) + hubKeyProviderMock.getPrivateKeyReturnValue = P384.KeyAgreement.PrivateKey(compactRepresentable: false) + let calledReceiveKey = XCTestExpectation() hubKeyServiceMock.receiveKeyAuthStateVaultConfigClosure = { _, _ in calledReceiveKey.fulfill() From 1f265299241f3ca0ade616954f74c595ab8ca739 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Wed, 22 Nov 2023 14:14:46 +0100 Subject: [PATCH 40/45] Fixed test --- CryptomatorTests/S3AuthenticationViewModelTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CryptomatorTests/S3AuthenticationViewModelTests.swift b/CryptomatorTests/S3AuthenticationViewModelTests.swift index 8043f3a25..8e529122a 100644 --- a/CryptomatorTests/S3AuthenticationViewModelTests.swift +++ b/CryptomatorTests/S3AuthenticationViewModelTests.swift @@ -113,7 +113,7 @@ class S3AuthenticationViewModelTests: XCTestCase { let recorder = viewModel.$loginState.recordNext(2) prepareViewModelWithDefaultValues() - viewModel.endpoint = "example invalid endpoint" + viewModel.endpoint = "https://example invalid endpoint" credentialVerifierMock.verifyCredentialReturnValue = Promise(()) viewModel.saveS3Credential() From f264019dfc93e7a37b95e067cc406a81ceb9eee1 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Wed, 22 Nov 2023 17:56:32 +0100 Subject: [PATCH 41/45] Removed Xcode selection, use default --- .github/workflows/build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ddea7684..26e6f72ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,8 +26,6 @@ jobs: run: | ./Scripts/process.sh exit $? - - name: Select Xcode 15.0.1 - run: sudo xcode-select -s /Applications/Xcode_15.0.1.app - name: Create CloudAccessSecrets run: | cd fastlane From 4bcc6a3324d04c4a849a89e7efbe275dfcb6ccce Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 22 Nov 2023 18:11:55 +0100 Subject: [PATCH 42/45] Change device --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 26e6f72ec..7225ba82d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ jobs: runs-on: macos-13 env: DERIVED_DATA_PATH: 'DerivedData' - DEVICE: 'iPhone 15 Pro' + DEVICE: 'iPhone 14 Pro' if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" strategy: matrix: From b70cec2ab12c92619ea19f0d2aaf7f70fd8f2e87 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 22 Nov 2023 22:58:14 +0100 Subject: [PATCH 43/45] Use structured concurrency for StoreObserverTests --- .../Purchase/StoreObserverTests.swift | 111 +++++++----------- 1 file changed, 44 insertions(+), 67 deletions(-) diff --git a/CryptomatorTests/Purchase/StoreObserverTests.swift b/CryptomatorTests/Purchase/StoreObserverTests.swift index 3e129d245..09e193b61 100644 --- a/CryptomatorTests/Purchase/StoreObserverTests.swift +++ b/CryptomatorTests/Purchase/StoreObserverTests.swift @@ -42,32 +42,25 @@ class StoreObserverTests: XCTestCase { // MARK: Buy Product - func testBuyFreeTrial() throws { - let expectation = XCTestExpectation() - storeManager.fetchProducts(with: [.thirtyDayTrial]).then { response -> Promise in - XCTAssertEqual(1, response.products.count) - return self.storeObserver.buy(response.products[0]) - }.then { purchaseTransaction in - try self.assertTrialStarted(purchaseTransaction: purchaseTransaction) - }.catch { error in - XCTFail("Promise failed with error: \(error)") - }.always { - expectation.fulfill() - } - wait(for: [expectation], timeout: 1.0) + func testBuyFreeTrial() async throws { + let response = try await storeManager.fetchProducts(with: [.thirtyDayTrial]).getValue() + XCTAssertEqual(1, response.products.count) + + let purchaseTransaction = try await storeObserver.buy(response.products[0]).getValue() + try assertTrialStarted(purchaseTransaction: purchaseTransaction) } - func testBuyFullVersion() throws { - assertFullVersionUnlockedWhenBuying(product: .fullVersion) - assertFullVersionUnlockedWhenBuying(product: .paidUpgrade) - assertFullVersionUnlockedWhenBuying(product: .freeUpgrade) + func testBuyFullVersion() async throws { + try await assertFullVersionUnlockedWhenBuying(product: .fullVersion) + try await assertFullVersionUnlockedWhenBuying(product: .paidUpgrade) + try await assertFullVersionUnlockedWhenBuying(product: .freeUpgrade) } // MARK: Deferred Transactions (Ask to buy) // Only test the approved case as there is no transaction state changes if the transaction gets declined // see https://developer.apple.com/forums/thread/685183 - func testAskToBuy() throws { + func testAskToBuy() async throws { session.askToBuyEnabled = true XCTAssert(session.allTransactions().isEmpty) @@ -84,42 +77,42 @@ class StoreObserverTests: XCTestCase { } storeObserver.fallbackDelegate = fallbackDelegateMock - assertBuyFailsWithDeferredTransactionError() + try await assertBuyFailsWithDeferredTransactionError() try approveAskToBuyTransaction() - wait(for: [fallbackCalledExpectation], timeout: 1.0) + await fulfillment(of: [fallbackCalledExpectation]) XCTAssertEqual(1, fallbackDelegateMock.purchaseDidSucceedTransactionCallsCount) } - func testRestoreRunningSubscription() { + func testRestoreRunningSubscription() async throws { let cryptomatorSettingsMock = CryptomatorSettingsMock() cryptomatorSettingsMock.hasRunningSubscription = true - assertRestored(with: .restoredFullVersion, cryptomatorSettings: cryptomatorSettingsMock) + try await assertRestored(with: .restoredFullVersion, cryptomatorSettings: cryptomatorSettingsMock) } - func testRestoreLifetimePremium() { + func testRestoreLifetimePremium() async throws { let cryptomatorSettingsMock = CryptomatorSettingsMock() cryptomatorSettingsMock.fullVersionUnlocked = true - assertRestored(with: .restoredFullVersion, cryptomatorSettings: cryptomatorSettingsMock) + try await assertRestored(with: .restoredFullVersion, cryptomatorSettings: cryptomatorSettingsMock) } - func testRestoreTrial() { + func testRestoreTrial() async throws { let cryptomatorSettingsMock = CryptomatorSettingsMock() let trialExpirationDate = Date.distantFuture cryptomatorSettingsMock.trialExpirationDate = trialExpirationDate - assertRestored(with: .restoredFreeTrial(expiresOn: trialExpirationDate), cryptomatorSettings: cryptomatorSettingsMock) + try await assertRestored(with: .restoredFreeTrial(expiresOn: trialExpirationDate), cryptomatorSettings: cryptomatorSettingsMock) } - func testRestoreExpiredTrial() { + func testRestoreExpiredTrial() async throws { let cryptomatorSettingsMock = CryptomatorSettingsMock() let trialExpirationDate = Date.distantPast cryptomatorSettingsMock.trialExpirationDate = trialExpirationDate - assertRestored(with: .noRestorablePurchases, cryptomatorSettings: cryptomatorSettingsMock) + try await assertRestored(with: .noRestorablePurchases, cryptomatorSettings: cryptomatorSettingsMock) } - func testRestoreNothing() { + func testRestoreNothing() async throws { let cryptomatorSettingsMock = CryptomatorSettingsMock() - assertRestored(with: .noRestorablePurchases, cryptomatorSettings: cryptomatorSettingsMock) + try await assertRestored(with: .noRestorablePurchases, cryptomatorSettings: cryptomatorSettingsMock) } // MARK: - Internal @@ -134,20 +127,14 @@ class StoreObserverTests: XCTestCase { try session.approveAskToBuyTransaction(identifier: deferredTransaction.identifier) } - private func assertFullVersionUnlockedWhenBuying(product: ProductIdentifier) { - let expectation = XCTestExpectation() - storeManager.fetchProducts(with: [product]).then { response -> Promise in - XCTAssertEqual(1, response.products.count) - return self.storeObserver.buy(response.products[0]) - }.then { purchaseTransaction in - XCTAssertEqual(.fullVersion, purchaseTransaction) - XCTAssert(self.cryptomatorSettingsMock.fullVersionUnlocked) - }.catch { error in - XCTFail("Promise failed with error: \(error)") - }.always { - expectation.fulfill() - } - wait(for: [expectation], timeout: 1.0) + private func assertFullVersionUnlockedWhenBuying(product: ProductIdentifier, file: StaticString = #filePath, line: UInt = #line) async throws { + let response = try await storeManager.fetchProducts(with: [product]).getValue() + XCTAssertEqual(1, response.products.count) + + let purchaseTransaction = try await storeObserver.buy(response.products[0]).getValue() + + XCTAssertEqual(.fullVersion, purchaseTransaction) + XCTAssert(cryptomatorSettingsMock.fullVersionUnlocked) } private func assertTrialStarted(purchaseTransaction: PurchaseTransaction) throws { @@ -162,39 +149,29 @@ class StoreObserverTests: XCTestCase { XCTAssertEqual(expectedDate.timeIntervalSinceReferenceDate, actualDate.timeIntervalSinceReferenceDate, accuracy: 2.0) } - private func assertBuyFailsWithDeferredTransactionError() { - let askToBuyExpectation = XCTestExpectation() - let fetchProductPromise = storeManager.fetchProducts(with: [.thirtyDayTrial]) - fetchProductPromise.then { response -> Promise in - XCTAssertEqual(1, response.products.count) - return self.storeObserver.buy(response.products[0]) - }.then { _ in - XCTFail("Promise fulfilled") - }.catch { error in + private func assertBuyFailsWithDeferredTransactionError(file: StaticString = #filePath, line: UInt = #line) async throws { + let response = try await storeManager.fetchProducts(with: [.thirtyDayTrial]).getValue() + + XCTAssertEqual(1, response.products.count) + + do { + _ = try await storeObserver.buy(response.products[0]).getValue() + XCTFail("Buy did not fail", file: file, line: line) + } catch { XCTAssertEqual(.deferredTransaction, error as? StoreObserverError) - }.always { - askToBuyExpectation.fulfill() } - wait(for: [askToBuyExpectation], timeout: 1.0) } - private func assertRestored(with expectedResult: RestoreTransactionsResult, cryptomatorSettings: CryptomatorSettings) { - let expectation = XCTestExpectation() + private func assertRestored(with expectedResult: RestoreTransactionsResult, cryptomatorSettings: CryptomatorSettings, file: StaticString = #filePath, line: UInt = #line) async throws { let premiumManagerMock = PremiumManagerTypeMock() let storeObserver = StoreObserver(cryptomatorSettings: cryptomatorSettings, premiumManager: premiumManagerMock) SKPaymentQueue.default().add(storeObserver) SKPaymentQueue.default().remove(self.storeObserver) - storeObserver.restore().then { result in - XCTAssertEqual(expectedResult, result) - XCTAssert(premiumManagerMock.refreshStatusCalled) - }.catch { error in - XCTFail("Promise failed with error: \(error)") - }.always { - expectation.fulfill() - } - wait(for: [expectation], timeout: 1.0) + let result = try await storeObserver.restore().getValue() + XCTAssertEqual(expectedResult, result) + XCTAssert(premiumManagerMock.refreshStatusCalled) } } From c228955e539f899aecd579b54b2fbb0ef6f6ad92 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Thu, 23 Nov 2023 00:01:13 +0100 Subject: [PATCH 44/45] Decrease accuracy for trial started check --- CryptomatorTests/Purchase/StoreObserverTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CryptomatorTests/Purchase/StoreObserverTests.swift b/CryptomatorTests/Purchase/StoreObserverTests.swift index 09e193b61..e88d5a339 100644 --- a/CryptomatorTests/Purchase/StoreObserverTests.swift +++ b/CryptomatorTests/Purchase/StoreObserverTests.swift @@ -143,10 +143,12 @@ class StoreObserverTests: XCTestCase { XCTFail("Wrong purchaseTransaction: \(purchaseTransaction)") return } - XCTAssertEqual(expectedDate.timeIntervalSinceReferenceDate, expiresOn.timeIntervalSinceReferenceDate, accuracy: 2.0) + + // decrease the accuracy to 2 minutes to increase stability of the unit tests in the CI. + XCTAssertEqual(expectedDate.timeIntervalSinceReferenceDate, expiresOn.timeIntervalSinceReferenceDate, accuracy: 120.0) let actualDate = try XCTUnwrap(cryptomatorSettingsMock.trialExpirationDate, "trialExpirationDate was not set") - XCTAssertEqual(expectedDate.timeIntervalSinceReferenceDate, actualDate.timeIntervalSinceReferenceDate, accuracy: 2.0) + XCTAssertEqual(expectedDate.timeIntervalSinceReferenceDate, actualDate.timeIntervalSinceReferenceDate, accuracy: 120.0) } private func assertBuyFailsWithDeferredTransactionError(file: StaticString = #filePath, line: UInt = #line) async throws { From 1d749f7afe893f9b0338ec96ce32d672cddaa77d Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Thu, 23 Nov 2023 00:04:22 +0100 Subject: [PATCH 45/45] Fix linter warning --- CryptomatorTests/Purchase/StoreObserverTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CryptomatorTests/Purchase/StoreObserverTests.swift b/CryptomatorTests/Purchase/StoreObserverTests.swift index e88d5a339..a8a6adfc0 100644 --- a/CryptomatorTests/Purchase/StoreObserverTests.swift +++ b/CryptomatorTests/Purchase/StoreObserverTests.swift @@ -143,7 +143,7 @@ class StoreObserverTests: XCTestCase { XCTFail("Wrong purchaseTransaction: \(purchaseTransaction)") return } - + // decrease the accuracy to 2 minutes to increase stability of the unit tests in the CI. XCTAssertEqual(expectedDate.timeIntervalSinceReferenceDate, expiresOn.timeIntervalSinceReferenceDate, accuracy: 120.0)