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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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/92] 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 2fbeeb0aed3e6f2d8354e3ce393238588155e120 Mon Sep 17 00:00:00 2001 From: Julian Raufelder Date: Thu, 20 Apr 2023 15:46:49 +0200 Subject: [PATCH 17/92] Welcome status and no-response bot :wave: --- .github/workflows/no-response.yml | 22 ++++++++++++++++++++++ .github/workflows/stale.yml | 24 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/workflows/no-response.yml create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 000000000..1e5a848dd --- /dev/null +++ b/.github/workflows/no-response.yml @@ -0,0 +1,22 @@ +# Configuration for close-stale-issues - https://github.com/marketplace/actions/close-stale-issues + +name: 'Close awaiting response issues' +on: + schedule: + - cron: '00 09 * * *' + +jobs: + no-response: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v8 + with: + days-before-stale: 14 + days-before-close: 0 + days-before-pr-close: -1 + stale-issue-label: 'state:stale' + close-issue-message: "This issue has been automatically closed because there has been no response to our request for more information from the original author. With only the information that is currently in the issue, we don't have enough information to take action. Please reach out if you have or find the answers we need so that we can investigate further." + only-labels: 'state:awaiting-response' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..f3a57687d --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,24 @@ +# Configuration for close-stale-issues - https://github.com/marketplace/actions/close-stale-issues + +name: 'Close stale issues' +on: + schedule: + - cron: '00 09 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v8 + with: + days-before-stale: 365 + days-before-close: 90 + exempt-issue-labels: 'type:security-issue,type:feature-request,type:enhancement,type:upstream-bug,state:awaiting-response,state:blocked,state:confirmed' + exempt-all-milestones: true + stale-issue-label: 'state:stale' + stale-pr-label: 'state:stale' + stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' + stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' From 94fcc4b5874a6b50334cc17cfb5f37751700eff8 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Thu, 20 Apr 2023 16:22:40 +0200 Subject: [PATCH 18/92] Fixed SwiftLint error --- .../Mocks/FileProviderConnectorMock.swift | 3 +++ 1 file changed, 3 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 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 19/92] 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 739ec1889b299eba2cdf5796165f4222dc20ce20 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Fri, 16 Jun 2023 11:21:02 +0200 Subject: [PATCH 20/92] Update SECURITY.md [ci skip] --- .github/SECURITY.md | 136 +++++++------------------------------------- 1 file changed, 19 insertions(+), 117 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 9ea042d3d..4f129cd42 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -2,123 +2,25 @@ ## Reporting a Vulnerability -For reporting security-related vulnerabilities or exploits that [haven't been reported yet](https://github.com/cryptomator/ios/labels/type%3Asecurity-issue), contact us at: security@cryptomator.org +We take security seriously at Cryptomator. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. -
-PGP Key +To report a security vulnerability, please use the GitHub Security Advisory feature. This feature allows you to privately discuss, fix, and publish information about security vulnerabilities. -``` ------BEGIN PGP PUBLIC KEY BLOCK----- -Comment: GPGTools - https://gpgtools.org +If you prefer to report the vulnerability via email, please send an email to security@cryptomator.org. -mQINBFbgeicBEADM9AcU6DTgM5KZnBaJc6x9DBLr+TCMHntTt7YM9GLTlO2Z43Jt -oYoyqdRWAY28veqpLEFgRvvVD3fdBj/KUOxF1cr2JsErwXqbjwaLq0o/0KIXz7UK -a6pQSemZKfpOtJrfacofOTwvG6AuG9uakBYNMyxuojyOkoh3xsYS1KZ7TwPgCdET -t8/zva41Pa5kh5+GeSZJdCuygG6ynPBJEpmK5V7Qizvics5fziXecF+QaFZijafv -YahfxokvF9pXCQTmV4m57NQma9uK0w83U9nJCPjEd+x3wK0Hxrc1ojy8ZFTA1YND -AQg/MTABgHbQQkXDQhjS/TloOObqtbMBqNSbcSXpaR4teaCWKBl1MSq00nJLj8db -vPJGqfg7UbXhlALggp029/kskYlR5SmbxWquLbl0Xre3fDHuHEiWcJL6MS3454Wt -Mno13/4UhOlRFh5g0pLmPz7seOTJjDqc9abn/RXOLq0+3qX0gC0bDm5aCE5dQ2MV -FMbrrlw/dZESNLZvtB3gOsramSry1R3HVZ0QJ2vMaF2cxewebqcYbuecUNj6bxpv -5LEhEmqz6dG1meLLWDsvQLPEUWEIJnfpBiDSm342yxJq4pXnVF+aqAQsCL3FpmvZ -2j0FgFOs7iXOcFUJIiR0xUmWPk1NWYcUowqmRW8pMM9nFUzFF99iggPznwARAQAB -tC1DcnlwdG9tYXRvciBTdXBwb3J0IDxzdXBwb3J0QGNyeXB0b21hdG9yLm9yZz6J -AkAEEwEKACoCGwMFCQcrKAAFCwkIBwMFFQoJCAsFFgIDAQACHgECF4AFAleu2cQC -GQEACgkQI7Xb75TU2B3+7A/7BKRWdo5/moCCEbBzYQ7vRMLFdwmjFFlSZ7aGC0fP -YHdeUwxPbO0cATwmNpGMma7rBn1FDg3Vto6/wottGxm+XIRwlyY84CD1VZAihZ/e -WvjOO28/7VgRy6PGKzlhpDSoT8GwFOgO69e7bEff1Zj562RZe7nXc4tDivILMB++ -KgmmSgtddygmNQCS3RD3KssGo+l+cSjsg09F5WAJ6nQe8Jq2hICq+o/P6UXPI5lX -bhvWYDn4/8sRHsIlGpQYYDDe0fz7IQKuSLAHpF5upNDxj6dYb05F8PPVrk6MW6nL -/kf1fZ27DlLN5/NFvhhBRuwxxoAFqPS7Iel3z7L0JkRUYmGLVB5m9Cqiw6FK8JRv -OtvakdDoKb5lVAoN5NeBfNBSqEcXVF/EdfTfIyyo7hZRA6xFMEVbmYbzt0sj0djV -ZOey2TOFrTCpkHfUUDgKvk5sn+F3u8mmPIbqquEzlFJSFjcyiYYDv22rg1In+zKV -Xmw4BFZRDS6IVSQRGlskRGJBixCaGyDYxHXXT2cg4Rk9uiCX11+0E9qlAsg6xPe6 -rnaYDT8dU0AFyVpDpshflXH3kVQSpiqZS3jkAk1/54ODO8pE80Zrnd5m5AMuNcmX -+9MkZKE+h0882UskDs1dyt26GU2hoy4lAeRUaut7zIK/WO6nnuLaTvGWT95RDz+q -kD2JAiIEEwEKAAwFAleu2iYFgweGH4AACgkQZnuGbqgkCgnmCA//U22uhyEC/Tp3 -Cbt5lctQmqbgMbjRBaHQyW52tPFMaq8vXMbo/5TTtVC6xsp2PJT84cxAd8KX8hWq -cPtF4wWCJGng/AzyxQ5dWfGvA/ll32ygjtJN3P/AvA9KlhG+6XYmS8cPkBkJBi6B -2yCdZT1cXc/TPAFzjgAwz7K9g3awG0OeOc/CXymH0DD/snkiwKQoucStolYywZGc -GszjMQgeT4zOc1wtEz24uL3dMNDlDcQMAh56YvK2oB0iMYmAFyX/IS+f2bM9paXi -HX+mg/z53iwgf5ZXbslNDbMTJ5GNksjEGjCFfDHAdNdgT+lcW4l2U7q4PYUaN4LA -DE9j2OlOlQ9qjucOgoCStirnTP7XHd4p31lgdz8+THOQowB5Ji95OkiNQAFCfxBt -mcA/bWnJZQDm7L8RVzHovBpAaK6vUjxEvR+DXdESSzyZwkpsZwGZcyqGRT26R1/L -JE5WvjKufNc5v3Cat320MjyrLZwVGRgvEpDMoCw3nTWl9AtOj5vgaakEWr7AnqET -xk7UFbYmdTlQqkWuLKubz9Rx/FbrBmvd6vwTHy1Dfl6QyMWNCClatgN00Hxped/6 -CErg+R/RXd8apGxnOuWDqoujPn5LOHzgJolp1Ox16nTiZe2G+LbDr3hqRFi1wW6w -ioMB4KpkdA03uyxJSWmDEMiR1l3Oxom0KUNyeXB0b21hdG9yIFByZXNzIDxwcmVz -c0BjcnlwdG9tYXRvci5vcmc+iQI9BBMBCgAnBQJXrtnDAhsDBQkHKygABQsJCAcD -BRUKCQgLBRYCAwEAAh4BAheAAAoJECO12++U1NgdQYMQAKCIzNJF8rURQcFLSv3J -sPBjRy2HCzCWm21MuhU+bsaZx7U9M9dgEjzLfxN9s19VsBH3WKLgok2FgiYSGka3 -6Oy/P8VFLFmHs7dS9i2fro2eF7i4zj/ZD/9t0jM4ZIgLpbzr5sTBld292nsfXGob -xOJeOx3oWYyR2FO9VQxXjC3JvJyZkFgoy0tauS4Mvii4cF56wJGcxDTbe1s7UaRC -a/fh4zgISZSBE3rYhCawkN4mqMDM5RDjrdtjKUPWk345HcjjQ4Wos8xw4YbGbNr9 -Pc7m2URYJJ0jFM4tnoRF6cmA3bT9tm8pcOFg+K/ycVrltVEy+A8Wj8UGjyP1uI1t -EqWHN3LZpIGfW0w9AGrw7OUI9czXcukfngj/DsOU3WMBDIM8pW9+zBpr75yIS6lz -C0IqksLXSqX0b/Rby4O+wb6UZ1ZFkaim2GGtAZV+nGXtdnEXSNFiP7ykzjZ02m/1 -7CKyj3VmdAgT56zEIypFSfxm9gOWsJPmfhSyuE8bFyoitgNxpheZk6xZy4upVMPR -WK3hutScU0yDv2HVCiA3o3Ggy42nmz9HpGF6W2DmBx4bhMaVs6I2VFyKdQzmJD/3 -FCWjwz8PiEgVGHGPnD+WdPFLhrc/44gF4h/VuLjkubtULGuTVvgjeTIJ5LR1Gmwc -YOk6eD7MAJPzJVj5/PYFtIbKiQIiBBMBCgAMBQJXrtonBYMHhh+AAAoJEGZ7hm6o -JAoJBh4P/1w88YMTKUHpFTfJEwH2hK36BZN96Bf/k+vP7n1Xxp3NheInJblHFOt/ -ccsup6am+APrk8gGtlIVmtVc3nO8WMsWxfJxGDecyRsNbessnODv/llyg3tzVU/H -tLk7gLiK0TcIsOLfeNXGTxRRSKWjVFsNfuixNCzzHa7tFq6ddVn9VRZ8fqJB2p21 -OogWSDqUo9q9Wfb4RkYHguDx+8Jzoo/MxR1TSt8gUO2xDvEbqgeQiMCLF8R0lO3Y -zz0FrpyOsFU1CxVp+wo55bWv1UdwgQKQt4o0m5/zDJ2RAtscXpd4YcTE+XxKeK+4 -qhihhkhLGpKsxzK5m9/qwMbodHwoBCBzfalkUR9xOq9yQIeEoC8XYL62NqB3BCSU -KfWFIHxUkE9WH5zHWaV+bhrlNgk7nz3xBfPf1P2mNIc1VUHoNqOZOmWwz2VaKLSW -f3GIqx9wGythFbLdXmUoC3W//DDYgQnvImvkncMqQ5nRHPf8uHcLQK5WZyIxpgWT -eKon5G/cj0BTptcBhapMwSIyfaC5FV7so0/CkOA6R9Fyq2VpGoHy7XPhFS+6ieLi -KUWhCvbuf2deWbSaJ0peMdzy1p72UXwrsEM0M3Fz+Jd8zvCaFzf5Fx27+pAAdlfg -4bT3/2gSf7S+cU3+DnYOH0NeRt2Z2mjEKg9OwttTO/oDboQHdZlrtDRDcnlwdG9t -YXRvciBTZWN1cml0eS1UZWFtIDxzZWN1cml0eUBjcnlwdG9tYXRvci5vcmc+iQI9 -BBMBCgAnBQJXrtnWAhsDBQkHKygABQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJ -ECO12++U1NgddzoQAI78+Nvm6VvNuptXJjEmrpHRyKCnHF9wH5kxvF8WZCgpOkJ4 -vONmyS+9ZlepnT83MpGm/MzdIMCnDJmDmqmA5ISBRcD7k9Gjzz5rPKwE2zDyo0M0 -wF1L2UEUqAlcvE0e4twZcP2DGoNqdSf6IaWsXhQMb1a/rTMsoGZLuTB8kCbv6Ntl -ULahcRToTB2shsbZjzE896P6X5hDCfGWl0Jhcbf53pnXX1dOsEw3et9AGru1IUMs -UGM+wpgTwagRj+XB/WY1x9IznKtiHTq83Fvt+3bkg0+NIcV3GDqXDIUtqIwy8gDd -4KgBU+LkyxXFDa4OxLc53n6b+Iy1nDosM+SiqSzdCCgEs/dY1tQBn/7P1GT18dEe -tFgeH/c6wLvEpDIc9urAsYXf8H+1uy7glWpWTq8DE0yhCr4adjCqlIsVHQQO4UUW -NfqMGEFpJ+3HjSSwnvDGY78lLQh5d4vqWV435aNaMqZg0gJIA0FtiP1fRtmT73BG -N/tBNiBxretFR4B+x/TWqPd5iJV7/MAn/pa1WSOcaxzJrVUsjXdgLQCqcHWd4/w1 -f4DU9cJjl3sxZlMdAlg8Q1bF+pmjQQ4WKZkqMtwpoUilfVXmL42ay1LBCgW68/uJ -OTyGfp8ntUsbbm5raGsny3TLqnacyG9hxcPGNTzD1+MrbUvfsc7+4U0dCZTuiQIi -BBMBCgAMBQJXrtonBYMHhh+AAAoJEGZ7hm6oJAoJ1DQP/R+1drZiZQU45ChMbfTb -XQjJRsUOGZp3PTWtx4KrVFvE8ea0PF+DZX5gLJYIU+iZmPXRpzFu6dKPbcZ7RfRt -5RRH102zDZzijt2CQd7YLO8wxUFoWX9X7DGgxXEcNjl9kFVmnyHgiTwTzuZ0Zy4y -PvoiwrhcZmXEYbOeV40gLFie6wuzz5IIcs01e30xIs+1/1gwmgI5UnG3jveUgmcj -f/lvg3POKiwrY5Uzw1FSruJx21X06wTpDcfOACID4L7aY9eg2B/qL2Xj8nuhejqG -+1AVTMk2o6pxkvevHmxYQfEpuWGCw0RCBn9ObWwz6Zn5J9pjGbMrM+b1/M2Ouv3N -cpoGgCSahKNsRMKO7RMrBG0jtLcasPSgZFYPJSZAAb+YhxKUbpPHzDIwTEjgM7CL -gKSyRTKyp5IoFK53bpXL/ZIjkAhMvyDult6+BL6vI0+h3BBA9I0FF2Qhe139xLv/ -DS7aDiYAE9vGMGoeCBfxJPwUsDU3hrGe/wgL7fR6nmN7R2QffisBHKHsklORy9t3 -w3YFRd5sBAxv+EOcdkgXEmqKOfVQ8KU9adQcxPDGMAK/esjVwxUxsaf2PF5noxxW -3zL2ureUO/mMoH5Cwr0BuM3HFb82t1JJd4IXlLEyNvDMFMwD2d7h37bGK7Y5hEsl -zL7Dm+wQRY8sxg4QOZHbJjQXuQINBFbgeicBEADnkxGSEL1zwACaiVqADKC6/pgO -MMWjxoENBT6r8Vnp1D5hfNDkEi9iXUpCEO6nzywBf3/4c4Yk1wBOBZ7YWyWXMf4v -2g1evxELO5z1UlAwna6HSl7G0omIBqzz1Er5IS7C9WEZM8ZggwcuswCrbxfz4+fN -t7cCL5QyOvuxez+vrn+VIgLQzKm+LV4Wc+OFbHIys+0saQUhItKO0/CsXGc8R314 -jdN5UsZk/MUdPPAs+6OCr8d3PpJvR6IST76TtN8aDjSS9T6em7dwdGFEwCGww3Jc -xrAkvvUmSlscz+rnvHA5DYQGK6NXLenB40sVQVfch1r1VqwvlzA0u7OovjwM8+7u -+DaBQ0YejbdnC7yfeE91LmZkG6jRKfvTJkv18tjNsgZsVmM13xzP67fCFIB9M+lN -t9zEldGKHVwm+06FHIWJsBDRgrquNb9xd1vgHHeIbJvKf+LqZhVrbKVEneG34Km+ -ndtb+mvcGc0fOoMU9lYrFaxAWl8oU9BchC9IyjcPZB445R+AhfTuoHSUViSCo6IO -TG0hQsJuNoKmDAU8l5sTsiFXuXBOo1wK8gTkRnhZHduZrZIjJXvT7efz1knLQ6eG -prZHf4CtbgHyAe2XZabetWtCsFcPbOjC7ezNK57UvVH98h2GkckxOM00BESMCTee -kYy7uG0v0rrajzHY1wARAQABiQIlBBgBCgAPBQJW4HonAhsMBQkHKygAAAoJECO1 -2++U1NgdyAsQAKZUVA6pY225BASkeNiW31L7K4VeRYpAdFkiRex2zQFtj9Vovfi1 -JeTs0fRm35dUsQraf1bkhsjEdPVZ3gD324/baauFO04KX+soyQvK/tUq8KO+5ALt -Ul5aAljuSwxfJWFpApv+Mbf7gOjm+77jirs7pgG/gCow/mkRlmKTwAmn2DXjkckC -2EH0mqmh5pdoNWKO7WeTFFbUmESsPcnB2FwTpEjHFvgHll+rmKpXZTgFYN4dDhhm -HsL/SCf/Nw+YIsuvErQ9TJVdJDLG8ZYatruk7dZZMPtFxvxM1Q36gDIpPEOKPkvm -dMXg6jHaIdYIaoMpzXFaXsQMdRuMtzbcA+CdwXVY55qGLtfmM/QuEiIJdDeeh7iB -+VAMyEFOOpi8IFhixaeMoZAmrKDqOkzPcMJVklLYq8N+b9p5JszYNwZEbpyWCACM -6K+iJzlWzW/OPZttGLJBgYuSYIJIuG80Cx5m5m1e5RAgQ1iT8nbfrS+gYttwP48J -V7SXQg7QugxG9l1vlK4VjnXiOFulJ7V0e/VyUBpJp3qHcCxFq3RnxVwlIqKZh+jm -Q1bk0H0Xodd27nQITfDP5ullByGW2Jrjs6SsXeR3jl9+t0XQfInU1L9d/wSOkMjL -9IMUt06lV4vB/WP2xioqLZiZ4eAi0E+lWkFxjZsgNs2xbOAYRThMB8a5 -=W1Ri ------END PGP PUBLIC KEY BLOCK----- -``` -
+PGP key fingerprint: `3647 9903 B23A E0A5 9359  9A3E 23B5 DBEF 94D4 D81D` ([public key](https://gist.github.com/cryptobot/864300b6b44ae2d2a15abedfe14bd040)) + +## Expectations + +When reporting a vulnerability, please provide us with a detailed report that includes: + +- A description of the vulnerability +- Steps to reproduce the vulnerability +- Possible impact of the vulnerability +- Any additional information that may be helpful + +We ask that you do not publicly disclose the vulnerability until we have had a chance to address it. + +## Thank You + +We appreciate your help in keeping Cryptomator for iOS secure. Thank you for your contributions to the security of our project. From 07f9d401a63ba89bb1e97af406e5fe238b00413b Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Fri, 16 Jun 2023 11:41:51 +0200 Subject: [PATCH 21/92] Update SECURITY.md [ci skip] --- .github/SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 4f129cd42..25ce5081e 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -4,7 +4,7 @@ We take security seriously at Cryptomator. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. -To report a security vulnerability, please use the GitHub Security Advisory feature. This feature allows you to privately discuss, fix, and publish information about security vulnerabilities. +To report a security vulnerability, please use the [GitHub Security Advisory feature](https://github.com/cryptomator/ios/security/advisories). This feature allows you to privately discuss, fix, and publish information about security vulnerabilities. If you prefer to report the vulnerability via email, please send an email to security@cryptomator.org. From cbd5d3408c3356f9e6e0ac0e345431f9c93520dc Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 10 Jul 2023 15:45:53 +0200 Subject: [PATCH 22/92] 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 23/92] 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 24/92] 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 25/92] 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 26/92] 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 27/92] 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 28/92] 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 29/92] 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 30/92] 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 31/92] 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 2cb3e947d022d0fd63fb8d421753975d4f4aaa8a Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sun, 1 Oct 2023 00:26:32 +0200 Subject: [PATCH 32/92] Improve injection of main database --- Cryptomator.xcodeproj/project.pbxproj | 7 ++ .../xcshareddata/swiftpm/Package.resolved | 18 ++++ Cryptomator/AppDelegate.swift | 16 +--- Cryptomator/Common/DatabaseManager.swift | 21 ++--- .../VaultDetailUnlockVaultViewModel.swift | 2 +- CryptomatorCommon/Package.swift | 6 +- .../CryptomatorDatabase.swift | 86 +++++++++++++++---- .../CloudProviderAccountDBManager.swift | 17 ++-- .../Manager/VaultAccountDBManager.swift | 19 ++-- .../Manager/VaultDBCache.swift | 15 ++-- .../Manager/VaultDBManager.swift | 2 +- .../S3/S3CredentialManager.swift | 13 +-- .../CloudProviderAccountManagerTests.swift | 18 +--- .../Manager/CloudProviderManagerTests.swift | 17 +--- .../Manager/S3CredentialManagerTests.swift | 7 +- .../Manager/VaultDBCacheTests.swift | 27 +++--- .../Manager/VaultManagerTests.swift | 25 +----- CryptomatorIntents/IntentHandler.swift | 8 -- .../AccountListViewModelTests.swift | 38 +++----- CryptomatorTests/DatabaseManagerTests.swift | 67 ++++++++------- .../VaultListViewModelTests.swift | 29 ++----- .../FileProviderExtension.swift | 30 +++---- .../RootViewController.swift | 15 ---- .../UnlockVaultViewModel.swift | 2 +- 24 files changed, 225 insertions(+), 280 deletions(-) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index b45695d55..78ca95e2d 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -353,6 +353,7 @@ 4AF45359271F38FC00CF1919 /* RenameVaultViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF45358271F38FC00CF1919 /* RenameVaultViewModelTests.swift */; }; 4AF4535D27205F6200CF1919 /* VaultDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF4535C27205F6200CF1919 /* VaultDetailCoordinator.swift */; }; 4AF4535F272066A600CF1919 /* RenameVaultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF4535E272066A600CF1919 /* RenameVaultViewController.swift */; }; + 4AF91A0F2AC2F025002357BA /* Dependencies in Frameworks */ = {isa = PBXBuildFile; productRef = 4AF91A0E2AC2F025002357BA /* Dependencies */; }; 4AF91CBE25A63FD600ACF01E /* VaultListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91CBD25A63FD600ACF01E /* VaultListViewModel.swift */; }; 4AF91CC725A6437000ACF01E /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4AF91CC625A6437000ACF01E /* Colors.xcassets */; }; 4AF91CD025A71C5800ACF01E /* UIImage+CloudProviderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF91CCF25A71C5800ACF01E /* UIImage+CloudProviderType.swift */; }; @@ -1095,6 +1096,7 @@ buildActionMask = 2147483647; files = ( 4A91728B2619F1D0003C4043 /* CryptomatorCommonCore in Frameworks */, + 4AF91A0F2AC2F025002357BA /* Dependencies in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2220,6 +2222,7 @@ name = CryptomatorFileProvider; packageProductDependencies = ( 4A91728A2619F1D0003C4043 /* CryptomatorCommonCore */, + 4AF91A0E2AC2F025002357BA /* Dependencies */, ); productName = CryptomatorFileProvider; productReference = 740375D72587AE7A0023FF53 /* libCryptomatorFileProvider.a */; @@ -3705,6 +3708,10 @@ package = 4AED9A6D286B38D900352951 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; productName = Introspect; }; + 4AF91A0E2AC2F025002357BA /* Dependencies */ = { + isa = XCSwiftPackageProductDependency; + productName = Dependencies; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4A5E5B212453119100BD6298 /* Project object */; diff --git a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 6620f8acf..aa74da510 100644 --- a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -163,6 +163,15 @@ "version": "2.0.0" } }, + { + "package": "Dependencies", + "repositoryURL": "https://github.com/PhilLibs/simple-swift-dependencies", + "state": { + "branch": null, + "revision": "36e2e7732b5fe2bfec76e4af78d2ef532fe09456", + "version": "0.1.0" + } + }, { "package": "swift-log", "repositoryURL": "https://github.com/apple/swift-log.git", @@ -189,6 +198,15 @@ "revision": "5b830d6ce6c34bb4bb976917576ab560e7945037", "version": "3.3.4" } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "23cbf2294e350076ea4dbd7d5d047c1e76b03631", + "version": "1.0.2" + } } ] }, diff --git a/Cryptomator/AppDelegate.swift b/Cryptomator/AppDelegate.swift index 8635e1f1a..7c99f3dd0 100644 --- a/Cryptomator/AppDelegate.swift +++ b/Cryptomator/AppDelegate.swift @@ -29,22 +29,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { setupIAP() // Set up database - guard let dbURL = CryptomatorDatabase.sharedDBURL else { - // MARK: Handle error + DatabaseManager.shared = DatabaseManager() - DDLogError("dbURL is nil") - return false - } - do { - let dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - CryptomatorDatabase.shared = try CryptomatorDatabase(dbPool) - DatabaseManager.shared = try DatabaseManager(dbPool: dbPool) - } catch { - // MARK: Handle error - - DDLogError("Initializing CryptomatorDatabase failed with error: \(error)") - return false - } VaultDBManager.shared.recoverMissingFileProviderDomains().catch { error in DDLogError("Recover missing FileProvider domains failed with error: \(error)") } diff --git a/Cryptomator/Common/DatabaseManager.swift b/Cryptomator/Common/DatabaseManager.swift index ae54ed371..1b0d9419b 100644 --- a/Cryptomator/Common/DatabaseManager.swift +++ b/Cryptomator/Common/DatabaseManager.swift @@ -8,20 +8,17 @@ import Combine import CryptomatorCommonCore +import Dependencies import Foundation import GRDB class DatabaseManager { public static var shared: DatabaseManager! - let dbPool: DatabasePool - - init(dbPool: DatabasePool) throws { - self.dbPool = dbPool - } + @Dependency(\.database) private var database func getAllVaults() throws -> [VaultInfo] { - try dbPool.read { db in + try database.read { db in let request = VaultAccount.including(required: VaultAccount.delegateAccount).including(required: VaultAccount.vaultListPosition) return try VaultInfo.fetchAll(db, request) } @@ -35,7 +32,7 @@ class DatabaseManager { for i in tempPositions.indices { tempPositions[i].position = nil } - try dbPool.write { db in + try database.write { db in try db.execute(sql: "PRAGMA ignore_check_constraints=YES") for position in tempPositions { try position.update(db) @@ -51,18 +48,18 @@ class DatabaseManager { let observation = ValueObservation .tracking { try VaultAccount.fetchAll($0) } .removeDuplicates() - return observation.start(in: dbPool, scheduling: .immediate, onError: onError, onChange: onChange) + return observation.start(in: database, scheduling: .immediate, onError: onError, onChange: onChange) } func observeVaultAccount(withVaultUID vaultUID: String, onError: @escaping (Error) -> Void, onChange: @escaping (VaultAccount?) -> Void) -> DatabaseCancellable { let observation = ValueObservation.tracking { db in try VaultAccount.fetchOne(db, key: vaultUID) } - return observation.start(in: dbPool, scheduling: .immediate, onError: onError, onChange: onChange) + return observation.start(in: database, scheduling: .immediate, onError: onError, onChange: onChange) } func getAllAccounts(for cloudProviderType: CloudProviderType) throws -> [AccountInfo] { - try dbPool.read { db in + try database.read { db in let accountWithCloudProviderType = AccountListPosition.account.filter(Column("cloudProviderType") == cloudProviderType) let request = AccountListPosition.including(required: accountWithCloudProviderType).order(Column("position")) return try AccountInfo.fetchAll(db, request) @@ -77,7 +74,7 @@ class DatabaseManager { for i in tempPositions.indices { tempPositions[i].position = nil } - try dbPool.write { db in + try database.write { db in try db.execute(sql: "PRAGMA ignore_check_constraints=YES") for position in tempPositions { try position.update(db) @@ -102,7 +99,7 @@ class DatabaseManager { .removeDuplicates() .map { rows in rows.map(AccountWithDisplayName.init(row:)) } .map { annotatedAccounts in annotatedAccounts.map(\.account) } - return observation.start(in: dbPool, scheduling: .immediate, onError: onError, onChange: onChange) + return observation.start(in: database, scheduling: .immediate, onError: onError, onChange: onChange) } } diff --git a/Cryptomator/VaultDetail/VaultDetailUnlockVaultViewModel.swift b/Cryptomator/VaultDetail/VaultDetailUnlockVaultViewModel.swift index faf7451da..83b7d24ae 100644 --- a/Cryptomator/VaultDetail/VaultDetailUnlockVaultViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailUnlockVaultViewModel.swift @@ -47,7 +47,7 @@ class VaultDetailUnlockVaultViewModel: SingleSectionTableViewModel, ReturnButton } func unlockVault() throws { - let cachedVault = try VaultDBCache(dbWriter: CryptomatorDatabase.shared.dbPool).getCachedVault(withVaultUID: vault.vaultUID) + let cachedVault = try VaultDBCache().getCachedVault(withVaultUID: vault.vaultUID) let masterkeyFile = try MasterkeyFile.withContentFromData(data: cachedVault.masterkeyFileData) _ = try masterkeyFile.unlock(passphrase: password) try passwordManager.setPassword(password, forVaultUID: vault.vaultUID) diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index 5617494f4..6c5184aee 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", .upToNextMinor(from: "1.7.0")), - .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/PhilLibs/simple-swift-dependencies", .upToNextMajor(from: "0.1.0")) ], targets: [ .target( @@ -41,7 +42,8 @@ let package = Package( name: "CryptomatorCommonCore", dependencies: [ "CocoaLumberjackSwift", - "CryptomatorCloudAccessCore" + "CryptomatorCloudAccessCore", + .product(name: "Dependencies", package: "simple-swift-dependencies") ] ), .testTarget( diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift index ae543ed4d..b6c690fd5 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorDatabase.swift @@ -6,31 +6,74 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import CocoaLumberjackSwift +import Dependencies import Foundation import GRDB +private enum CryptomatorDatabaseKey: DependencyKey { + static let liveValue: DatabaseWriter = CryptomatorDatabase.live + + static var testValue: DatabaseWriter { + let inMemoryDB = DatabaseQueue(configuration: .defaultCryptomatorConfiguration) + do { + try CryptomatorDatabase.migrator.migrate(inMemoryDB) + } catch { + DDLogError("Failed to migrate in-memory database: \(error)") + } + return inMemoryDB + } +} + +public extension DependencyValues { + var database: DatabaseWriter { + get { self[CryptomatorDatabaseKey.self] } + set { self[CryptomatorDatabaseKey.self] = newValue } + } +} + +private enum CryptomatorDatabaseLocationKey: DependencyKey { + static var liveValue: URL? { CryptomatorDatabase.sharedDBURL } + static var testValue: URL? { FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: false) } +} + +public extension DependencyValues { + var databaseLocation: URL? { + get { self[CryptomatorDatabaseLocationKey.self] } + set { self[CryptomatorDatabaseLocationKey.self] = newValue } + } +} + public enum CryptomatorDatabaseError: Error { case dbDoesNotExist case incompleteMigration } public class CryptomatorDatabase { - public static var shared: CryptomatorDatabase! + static var live: DatabaseWriter { + @Dependency(\.databaseLocation) var databaseURL - public static var sharedDBURL: URL? { - let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: CryptomatorConstants.appGroupName) - return sharedContainer?.appendingPathComponent("db.sqlite") + guard let dbURL = databaseURL else { + fatalError("Could not get URL for shared database") + } + let database: DatabaseWriter + do { + database = try CryptomatorDatabase.openSharedDatabase(at: dbURL) + } catch { + DDLogError("Failed to open shared database: \(error)") + fatalError("Could not open shared database") + } + do { + try CryptomatorDatabase.migrator.migrate(database) + } catch { + DDLogError("Failed to migrate database: \(error)") + } + return database } - public let dbPool: DatabasePool - private static var oldSharedDBURL: URL? { + static var sharedDBURL: URL? { let sharedContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: CryptomatorConstants.appGroupName) - return sharedContainer?.appendingPathComponent("main.sqlite") - } - - public init(_ dbPool: DatabasePool) throws { - self.dbPool = dbPool - try CryptomatorDatabase.migrator.migrate(dbPool) + return sharedContainer?.appendingPathComponent("db.sqlite") } static var migrator: DatabaseMigrator { @@ -48,7 +91,7 @@ public class CryptomatorDatabase { } // swiftlint:disable:next function_body_length - public class func v1Migration(_ db: Database) throws { + class func v1Migration(_ db: Database) throws { // Common try db.create(table: "cloudProviderAccounts") { table in table.column("accountUID", .text).primaryKey() @@ -157,12 +200,10 @@ public class CryptomatorDatabase { var coordinatorError: NSError? var dbPool: DatabasePool? var dbError: Error? - var configuration = Configuration() - // Workaround for a SQLite regression (see https://github.com/groue/GRDB.swift/issues/1171 for more details) - configuration.acceptsDoubleQuotedStringLiterals = true + coordinator.coordinate(writingItemAt: databaseURL, options: .forMerging, error: &coordinatorError, byAccessor: { _ in do { - dbPool = try DatabasePool(path: databaseURL.path, configuration: configuration) + dbPool = try DatabasePool(path: databaseURL.path, configuration: .defaultCryptomatorConfiguration) } catch { dbError = error } @@ -193,7 +234,7 @@ public class CryptomatorDatabase { private static func openReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool { do { - var configuration = Configuration() + var configuration = Configuration.defaultCryptomatorConfiguration configuration.readonly = true let dbPool = try DatabasePool(path: databaseURL.path, configuration: configuration) @@ -211,3 +252,12 @@ public class CryptomatorDatabase { } } } + +extension Configuration { + static var defaultCryptomatorConfiguration: Configuration { + var configuration = Configuration() + // Workaround for a SQLite regression (see https://github.com/groue/GRDB.swift/issues/1171 for more details) + configuration.acceptsDoubleQuotedStringLiterals = true + return configuration + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderAccountDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderAccountDBManager.swift index 2e460699a..5fe7ded1f 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderAccountDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderAccountDBManager.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import Dependencies import Foundation import GRDB @@ -41,15 +42,11 @@ public protocol CloudProviderAccountManager { } public class CloudProviderAccountDBManager: CloudProviderAccountManager { - public static let shared = CloudProviderAccountDBManager(dbPool: CryptomatorDatabase.shared.dbPool) - private let dbPool: DatabasePool - - init(dbPool: DatabasePool) { - self.dbPool = dbPool - } + @Dependency(\.database) var database + public static let shared = CloudProviderAccountDBManager() public func getCloudProviderType(for accountUID: String) throws -> CloudProviderType { - let cloudAccount = try dbPool.read { db in + let cloudAccount = try database.read { db in return try CloudProviderAccount.fetchOne(db, key: accountUID) } guard let providerType = cloudAccount?.cloudProviderType else { @@ -59,7 +56,7 @@ public class CloudProviderAccountDBManager: CloudProviderAccountManager { } public func getAllAccountUIDs(for type: CloudProviderType) throws -> [String] { - let accounts: [CloudProviderAccount] = try dbPool.read { db in + let accounts: [CloudProviderAccount] = try database.read { db in return try CloudProviderAccount .filter(Column("cloudProviderType") == type) .fetchAll(db) @@ -68,13 +65,13 @@ public class CloudProviderAccountDBManager: CloudProviderAccountManager { } public func saveNewAccount(_ account: CloudProviderAccount) throws { - try dbPool.write { db in + try database.write { db in try account.save(db) } } public func removeAccount(with accountUID: String) throws { - try dbPool.write { db in + try database.write { db in guard try CloudProviderAccount.deleteOne(db, key: accountUID) else { throw CloudProviderAccountError.accountNotFoundError } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift index 018d49060..3069ca55b 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultAccountDBManager.swift @@ -7,6 +7,7 @@ // import CryptomatorCloudAccessCore +import Dependencies import Foundation import GRDB @@ -55,16 +56,12 @@ public enum VaultAccountManagerError: Error { } public class VaultAccountDBManager: VaultAccountManager { - public static let shared = VaultAccountDBManager(dbPool: CryptomatorDatabase.shared.dbPool) - private let dbPool: DatabasePool - - public init(dbPool: DatabasePool) { - self.dbPool = dbPool - } + public static let shared = VaultAccountDBManager() + @Dependency(\.database) private var database public func saveNewAccount(_ account: VaultAccount) throws { do { - try dbPool.write { db in + try database.write { db in try account.save(db) } } catch let error as DatabaseError where error.resultCode == .SQLITE_CONSTRAINT { @@ -73,7 +70,7 @@ public class VaultAccountDBManager: VaultAccountManager { } public func removeAccount(with vaultUID: String) throws { - try dbPool.write { db in + try database.write { db in guard try VaultAccount.deleteOne(db, key: vaultUID) else { throw CloudProviderAccountError.accountNotFoundError } @@ -81,7 +78,7 @@ public class VaultAccountDBManager: VaultAccountManager { } public func getAccount(with vaultUID: String) throws -> VaultAccount { - let fetchedAccount = try dbPool.read { db in + let fetchedAccount = try database.read { db in return try VaultAccount.fetchOne(db, key: vaultUID) } guard let account = fetchedAccount else { @@ -91,13 +88,13 @@ public class VaultAccountDBManager: VaultAccountManager { } public func getAllAccounts() throws -> [VaultAccount] { - try dbPool.read { db in + try database.read { db in try VaultAccount.fetchAll(db) } } public func updateAccount(_ account: VaultAccount) throws { - try dbPool.write { db in + try database.write { db in try account.update(db) } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift index 52db65392..b97878335 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBCache.swift @@ -8,6 +8,7 @@ import CryptomatorCloudAccessCore import CryptomatorCryptoLib +import Dependencies import Foundation import GRDB import Promises @@ -49,20 +50,18 @@ public enum VaultCacheError: Error { } public class VaultDBCache: VaultCache { - private let dbWriter: DatabaseWriter + @Dependency(\.database) var database - public init(dbWriter: DatabaseWriter) { - self.dbWriter = dbWriter - } + public init() {} public func cache(_ entry: CachedVault) throws { - try dbWriter.write({ db in + try database.write({ db in try entry.save(db) }) } public func getCachedVault(withVaultUID vaultUID: String) throws -> CachedVault { - try dbWriter.read({ db in + try database.read({ db in guard let cachedVault = try CachedVault.fetchOne(db, key: vaultUID) else { throw VaultCacheError.vaultNotFound } @@ -83,7 +82,7 @@ public class VaultDBCache: VaultCache { } public func setMasterkeyFileData(_ data: Data, forVaultUID vaultUID: String, lastModifiedDate: Date?) throws { - _ = try dbWriter.write { db in + _ = try database.write { db in try CachedVault.filter(CachedVault.Columns.vaultUID == vaultUID).updateAll(db, CachedVault.Columns.masterkeyFileData.set(to: data), CachedVault.Columns.masterkeyFileLastModifiedDate.set(to: lastModifiedDate)) @@ -153,7 +152,7 @@ public class VaultDBCache: VaultCache { } private func setVaultConfigData(_ data: Data?, forVaultUID vaultUID: String, lastModifiedDate: Date?) throws { - _ = try dbWriter.write { db in + _ = try database.write { db in try CachedVault.filter(CachedVault.Columns.vaultUID == vaultUID).updateAll(db, CachedVault.Columns.vaultConfigToken.set(to: data), CachedVault.Columns.vaultConfigLastModifiedDate.set(to: lastModifiedDate)) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift index 3c00887dd..81505f07c 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift @@ -36,7 +36,7 @@ public protocol VaultManager { public class VaultDBManager: VaultManager { public static let shared = VaultDBManager(providerManager: CloudProviderDBManager.shared, vaultAccountManager: VaultAccountDBManager.shared, - vaultCache: VaultDBCache(dbWriter: CryptomatorDatabase.shared.dbPool), + vaultCache: VaultDBCache(), passwordManager: VaultPasswordKeychainManager(), masterkeyCacheManager: MasterkeyCacheKeychainManager.shared, masterkeyCacheHelper: VaultKeepUnlockedManager.shared) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/S3CredentialManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/S3CredentialManager.swift index e645ad8dd..5e431b141 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/S3CredentialManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/S3/S3CredentialManager.swift @@ -6,6 +6,7 @@ // import CryptomatorCloudAccessCore +import Dependencies import Foundation import GRDB @@ -41,13 +42,13 @@ public extension CloudProviderAccount { } public struct S3CredentialManager: S3CredentialManagerType { - public static let shared = S3CredentialManager(dbWriter: CryptomatorDatabase.shared.dbPool, keychain: CryptomatorKeychain.s3) - let dbWriter: DatabaseWriter + @Dependency(\.database) var database + public static let shared = S3CredentialManager(keychain: CryptomatorKeychain.s3) let keychain: CryptomatorKeychainType public func save(credential: S3Credential, displayName: String) throws { do { - try dbWriter.write { db in + try database.write { db in let entry = S3DisplayName(id: credential.identifier, displayName: displayName) try entry.save(db) try keychain.saveS3Credential(credential) @@ -56,14 +57,14 @@ public struct S3CredentialManager: S3CredentialManagerType { } public func removeCredential(with identifier: String) throws { - try dbWriter.write { db in + try database.write { db in try S3DisplayName.deleteOne(db, key: ["id": identifier]) try keychain.delete(identifier) } } public func getDisplayName(for identifier: String) throws -> String? { - try dbWriter.read { db in + try database.read { db in let entry = try S3DisplayName.fetchOne(db, key: ["id": identifier]) return entry?.displayName } @@ -84,5 +85,5 @@ extension S3CredentialManager { return inMemoryDB } - public static let demo = S3CredentialManager(dbWriter: inMemoryDB, keychain: CryptomatorKeychain(service: "s3CredentialDemo")) + public static let demo = S3CredentialManager(keychain: CryptomatorKeychain(service: "s3CredentialDemo")) } diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift index c25acb343..ce7b48842 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderAccountManagerTests.swift @@ -10,27 +10,13 @@ import Foundation import GRDB import XCTest @testable import CryptomatorCommonCore +@testable import Dependencies class CloudProviderAccountManagerTests: XCTestCase { var accountManager: CloudProviderAccountDBManager! - var tmpDir: URL! override func setUpWithError() throws { - tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) - let dbPool = try DatabasePool(path: tmpDir.appendingPathComponent("db.sqlite").path) - try dbPool.write { db in - try db.create(table: CloudProviderAccount.databaseTableName) { table in - table.column(CloudProviderAccount.accountUIDKey, .text).primaryKey() - table.column(CloudProviderAccount.cloudProviderTypeKey, .text).notNull() - } - } - accountManager = CloudProviderAccountDBManager(dbPool: dbPool) - } - - override func tearDownWithError() throws { - accountManager = nil - try FileManager.default.removeItem(at: tmpDir) + accountManager = CloudProviderAccountDBManager() } func testSaveAccount() throws { diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderManagerTests.swift index 782e06ba9..be08fd73c 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/CloudProviderManagerTests.swift @@ -15,25 +15,12 @@ import XCTest class CloudProviderManagerTests: XCTestCase { var manager: CloudProviderDBManager! var accountManager: CloudProviderAccountDBManager! - var tmpDir: URL! + override func setUpWithError() throws { - tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) - let dbPool = try DatabasePool(path: tmpDir.appendingPathComponent("db.sqlite").path) - try dbPool.write { db in - try db.create(table: CloudProviderAccount.databaseTableName) { table in - table.column(CloudProviderAccount.accountUIDKey, .text).primaryKey() - table.column(CloudProviderAccount.cloudProviderTypeKey, .text).notNull() - } - } - accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + accountManager = CloudProviderAccountDBManager() manager = CloudProviderDBManager(accountManager: accountManager) } - override func tearDownWithError() throws { - try FileManager.default.removeItem(at: tmpDir) - } - func testCreateProviderCachesTheProvider() throws { DropboxSetup.constants = DropboxSetup(appKey: "", sharedContainerIdentifier: nil, keychainService: nil, forceForegroundSession: false) let account = CloudProviderAccount(accountUID: UUID().uuidString, cloudProviderType: .dropbox) diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/S3CredentialManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/S3CredentialManagerTests.swift index 1cc629adf..8012a5806 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/S3CredentialManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/S3CredentialManagerTests.swift @@ -16,13 +16,8 @@ class S3CredentialManagerTests: XCTestCase { let displayName = "Cryptomator S3" override func setUpWithError() throws { - var configuration = Configuration() - // Workaround for a SQLite regression (see https://github.com/groue/GRDB.swift/issues/1171 for more details) - configuration.acceptsDoubleQuotedStringLiterals = true - let inMemoryDB = DatabaseQueue(configuration: configuration) - try CryptomatorDatabase.migrator.migrate(inMemoryDB) cryptomatorKeychainMock = CryptomatorKeychainMock() - manager = S3CredentialManager(dbWriter: inMemoryDB, keychain: cryptomatorKeychainMock) + manager = S3CredentialManager(keychain: cryptomatorKeychainMock) } func testSaveCredential() throws { diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultDBCacheTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultDBCacheTests.swift index 78f2de195..5a47c5185 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultDBCacheTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultDBCacheTests.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import Dependencies import Foundation import GRDB import Promises @@ -21,7 +22,6 @@ class VaultDBCacheTests: XCTestCase { private let vaultPath = CloudPath("/Vault") private lazy var vaultAccount: VaultAccount = .init(vaultUID: vaultUID, delegateAccountUID: account.accountUID, vaultPath: vaultPath, vaultName: "Vault") private let cloudProviderMock = CloudProviderMock() - private var inMemoryDB: DatabaseQueue! private var masterkeyFileData: Data! private var updatedMasterkeyFileData: Data! private let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) @@ -40,16 +40,9 @@ class VaultDBCacheTests: XCTestCase { vaultConfigData = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: masterkey.rawKey) updatedVaultConfigData = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: updatedMasterkey.rawKey) defaultCachedVault = CachedVault(vaultUID: vaultUID, masterkeyFileData: masterkeyFileData, vaultConfigToken: vaultConfigData, lastUpToDateCheck: Date(timeIntervalSince1970: 0), masterkeyFileLastModifiedDate: Date(timeIntervalSince1970: 0), vaultConfigLastModifiedDate: Date(timeIntervalSince1970: 0)) - var configuration = Configuration() - // Workaround for a SQLite regression (see https://github.com/groue/GRDB.swift/issues/1171 for more details) - configuration.acceptsDoubleQuotedStringLiterals = true - inMemoryDB = DatabaseQueue(configuration: configuration) - vaultCache = VaultDBCache(dbWriter: inMemoryDB) - try CryptomatorDatabase.migrator.migrate(inMemoryDB) - try inMemoryDB.write { db in - try account.save(db) - try vaultAccount.save(db) - } + + vaultCache = VaultDBCache() + try prepareDatabase() } func testCacheVault() throws { @@ -74,9 +67,11 @@ class VaultDBCacheTests: XCTestCase { } func testCascadeOnVaultAccountDeletion() throws { + @Dependency(\.database) var database + try vaultCache.cache(defaultCachedVault) - _ = try inMemoryDB.write { db in + _ = try database.write { db in try vaultAccount.delete(db) } XCTAssertThrowsError(try vaultCache.getCachedVault(withVaultUID: vaultUID)) { error in @@ -304,4 +299,12 @@ class VaultDBCacheTests: XCTestCase { private func assertDownloadedOnlyMasterkey() { XCTAssertEqual([CloudPath("/Vault/masterkey.cryptomator")], cloudProviderMock.downloadFileFromToReceivedInvocations.map { $0.cloudPath }) } + + private func prepareDatabase() throws { + @Dependency(\.database) var database + try database.write { db in + try account.save(db) + try vaultAccount.save(db) + } + } } diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift index bd12cf3cc..2a75305a2 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift @@ -61,8 +61,6 @@ class VaultManagerTests: XCTestCase { var masterkeyCacheManagerMock: MasterkeyCacheManagerMock! var masterkeyCacheHelperMock: MasterkeyCacheHelperMock! var cloudProviderMock: CloudProviderMock! - var tmpDir: URL! - var dbPool: DatabasePool! let vaultUID = "VaultUID-12345" let passphrase = "PW" let delegateAccountUID = UUID().uuidString @@ -74,17 +72,10 @@ class VaultManagerTests: XCTestCase { override func setUpWithError() throws { cloudProviderMock = CloudProviderMock() - tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) - var configuration = Configuration() - // Workaround for a SQLite regression (see https://github.com/groue/GRDB.swift/issues/1171 for more details) - configuration.acceptsDoubleQuotedStringLiterals = true - dbPool = try DatabasePool(path: tmpDir.appendingPathComponent("db.sqlite").path, configuration: configuration) - try CryptomatorDatabase.migrator.migrate(dbPool) - - providerAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) + + providerAccountManager = CloudProviderAccountDBManager() providerManager = CloudProviderManagerMock(provider: cloudProviderMock, accountManager: providerAccountManager) - accountManager = VaultAccountDBManager(dbPool: dbPool) + accountManager = VaultAccountDBManager() vaultCacheMock = VaultCacheMock() vaultCacheMock.refreshVaultCacheForWithReturnValue = Promise(()) passwordManagerMock = VaultPasswordManagerMock() @@ -94,16 +85,6 @@ class VaultManagerTests: XCTestCase { manager = VaultManagerMock(providerManager: providerManager, vaultAccountManager: accountManager, vaultCache: vaultCacheMock, passwordManager: passwordManagerMock, masterkeyCacheManager: masterkeyCacheManagerMock, masterkeyCacheHelper: masterkeyCacheHelperMock) } - override func tearDownWithError() throws { - // Set all objects related to the sqlite database to nil to avoid warnings about database integrity when deleting the test database. - manager = nil - providerAccountManager = nil - providerManager = nil - accountManager = nil - dbPool = nil - try FileManager.default.removeItem(at: tmpDir) - } - func testCreateNewVault() throws { let expectation = XCTestExpectation() let delegateAccountUID = UUID().uuidString diff --git a/CryptomatorIntents/IntentHandler.swift b/CryptomatorIntents/IntentHandler.swift index 8f3ed8101..9f4202b02 100644 --- a/CryptomatorIntents/IntentHandler.swift +++ b/CryptomatorIntents/IntentHandler.swift @@ -34,13 +34,5 @@ class IntentHandler: INExtension { private static var oneTimeSetup: () -> Void = { // Set up logger LoggerSetup.oneTimeSetup() - if let dbURL = CryptomatorDatabase.sharedDBURL { - do { - let dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - CryptomatorDatabase.shared = try CryptomatorDatabase(dbPool) - } catch { - DDLogError("Open shared database at \(dbURL) failed with error: \(error)") - } - } } } diff --git a/CryptomatorTests/AccountListViewModelTests.swift b/CryptomatorTests/AccountListViewModelTests.swift index ec447d042..c3e3ddf7c 100644 --- a/CryptomatorTests/AccountListViewModelTests.swift +++ b/CryptomatorTests/AccountListViewModelTests.swift @@ -13,27 +13,9 @@ import XCTest @testable import CryptomatorCommonCore class AccountListViewModelTests: XCTestCase { - var tmpDir: URL! - var dbPool: DatabasePool! - var cryptomatorDB: CryptomatorDatabase! - override func setUpWithError() throws { - tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) - let dbURL = tmpDir.appendingPathComponent("db.sqlite") - dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - cryptomatorDB = try CryptomatorDatabase(dbPool) - _ = try DatabaseManager(dbPool: dbPool) - } - - override func tearDownWithError() throws { - dbPool = nil - cryptomatorDB = nil - try FileManager.default.removeItem(at: tmpDir) - } - func testMoveRow() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let dbManagerMock = try DatabaseManagerMock() + let accountManager = CloudProviderAccountDBManager() let cloudAuthenticatorMock = CloudAuthenticatorMock(accountManager: accountManager) let accountListViewModel = AccountListViewModelMock(with: .dropbox, dbManager: dbManagerMock, cloudAuthenticator: cloudAuthenticatorMock) try accountListViewModel.refreshItems() @@ -57,8 +39,8 @@ class AccountListViewModelTests: XCTestCase { } func testRemoveRow() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() + let accountManager = CloudProviderAccountDBManager() let cloudAuthenticatorMock = CloudAuthenticatorMock(accountManager: accountManager) let accountListViewModel = AccountListViewModelMock(with: .dropbox, dbManager: dbManagerMock, cloudAuthenticator: cloudAuthenticatorMock) try accountListViewModel.refreshItems() @@ -78,8 +60,8 @@ class AccountListViewModelTests: XCTestCase { } func testWebDAVAccountCellContent() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() + let accountManager = CloudProviderAccountDBManager() let cloudAuthenticatorMock = CloudAuthenticatorMock(accountManager: accountManager) let accountListViewModel = AccountListViewModel(with: .dropbox, dbManager: dbManagerMock, cloudAuthenticator: cloudAuthenticatorMock) let baseURL = URL(string: "https://www.example.com")! @@ -90,8 +72,8 @@ class AccountListViewModelTests: XCTestCase { } func testWebDAVAccountCellContentWithPathInDetailLabel() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() + let accountManager = CloudProviderAccountDBManager() let cloudAuthenticatorMock = CloudAuthenticatorMock(accountManager: accountManager) let accountListViewModel = AccountListViewModel(with: .dropbox, dbManager: dbManagerMock, cloudAuthenticator: cloudAuthenticatorMock) let baseURL = URL(string: "https://www.example.com/path")! @@ -102,8 +84,8 @@ class AccountListViewModelTests: XCTestCase { } func testWebDAVAccountCellContentWithUnknownHost() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) - let accountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() + let accountManager = CloudProviderAccountDBManager() let cloudAuthenticatorMock = CloudAuthenticatorMock(accountManager: accountManager) let accountListViewModel = AccountListViewModel(with: .dropbox, dbManager: dbManagerMock, cloudAuthenticator: cloudAuthenticatorMock) let baseURL = URL(string: "www")! diff --git a/CryptomatorTests/DatabaseManagerTests.swift b/CryptomatorTests/DatabaseManagerTests.swift index 15671a1b4..5faa9892d 100644 --- a/CryptomatorTests/DatabaseManagerTests.swift +++ b/CryptomatorTests/DatabaseManagerTests.swift @@ -11,39 +11,38 @@ import GRDB import XCTest @testable import Cryptomator @testable import CryptomatorCommonCore +@testable import Dependencies class DatabaseManagerTests: XCTestCase { var tmpDir: URL! - var dbPool: DatabasePool! var dbManager: DatabaseManager! - var cryptomatorDB: CryptomatorDatabase! + override func setUpWithError() throws { tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) let dbURL = tmpDir.appendingPathComponent("db.sqlite") - dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - cryptomatorDB = try CryptomatorDatabase(dbPool) - dbManager = try DatabaseManager(dbPool: dbPool) + + DependencyValues.mockDependency(\.databaseLocation, with: dbURL) + DependencyValues.mockDependency(\.database, with: CryptomatorDatabase.live) + dbManager = DatabaseManager() } override func tearDownWithError() throws { - dbPool = nil - cryptomatorDB = nil - dbManager = nil try FileManager.default.removeItem(at: tmpDir) } // MARK: VaultListPosition func testCreatePositionTrigger() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) - let vaultAccountManager = VaultAccountDBManager(dbPool: dbPool) + @Dependency(\.database) var database + let cloudAccountManager = CloudProviderAccountDBManager() + let vaultAccountManager = VaultAccountDBManager() let cloudProviderAccount = CloudProviderAccount(accountUID: "1", cloudProviderType: .dropbox) try cloudAccountManager.saveNewAccount(cloudProviderAccount) let vaultAccount = VaultAccount(vaultUID: "Vault1", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault1"), vaultName: "Vault1") try vaultAccountManager.saveNewAccount(vaultAccount) - let firstVaultListPosition = try dbPool.read { db in + let firstVaultListPosition = try database.read { db in try VaultListPosition.filter(Column("vaultUID") == "Vault1").fetchOne(db) } XCTAssertNotNil(firstVaultListPosition) @@ -53,7 +52,7 @@ class DatabaseManagerTests: XCTestCase { let secondVaultAccount = VaultAccount(vaultUID: "Vault2", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault2"), vaultName: "Vault2") try vaultAccountManager.saveNewAccount(secondVaultAccount) - let secondVaultListPosition = try dbPool.read { db in + let secondVaultListPosition = try database.read { db in try VaultListPosition.filter(Column("vaultUID") == "Vault2").fetchOne(db) } XCTAssertNotNil(secondVaultListPosition) @@ -62,8 +61,10 @@ class DatabaseManagerTests: XCTestCase { } func testDeleteVaultAccountUpdatesPositions() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) - let vaultAccountManager = VaultAccountDBManager(dbPool: dbPool) + @Dependency(\.database) var database + + let cloudAccountManager = CloudProviderAccountDBManager() + let vaultAccountManager = VaultAccountDBManager() let cloudProviderAccount = CloudProviderAccount(accountUID: "1", cloudProviderType: .dropbox) try cloudAccountManager.saveNewAccount(cloudProviderAccount) @@ -74,22 +75,22 @@ class DatabaseManagerTests: XCTestCase { let thirdVaultAccount = VaultAccount(vaultUID: "Vault3", delegateAccountUID: cloudProviderAccount.accountUID, vaultPath: CloudPath("/Vault3"), vaultName: "Vault3") try vaultAccountManager.saveNewAccount(thirdVaultAccount) - _ = try dbPool.write { db in + _ = try database.write { db in try vaultAccount.delete(db) } - let vaultListPositionEntryForVault1 = try dbPool.read { db in + let vaultListPositionEntryForVault1 = try database.read { db in try VaultListPosition.filter(Column("vaultUID") == "Vault1").fetchOne(db) } XCTAssertNil(vaultListPositionEntryForVault1) - let firstVaultListPosition = try dbPool.read { db in + let firstVaultListPosition = try database.read { db in try VaultListPosition.filter(Column("vaultUID") == "Vault2").fetchOne(db) } XCTAssertNotNil(firstVaultListPosition) XCTAssertEqual(0, firstVaultListPosition?.position) - let secondVaultListPosition = try dbPool.read { db in + let secondVaultListPosition = try database.read { db in try VaultListPosition.filter(Column("vaultUID") == "Vault3").fetchOne(db) } XCTAssertNotNil(secondVaultListPosition) @@ -97,8 +98,8 @@ class DatabaseManagerTests: XCTestCase { } func testUpdateVaultListPositions() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) - let vaultAccountManager = VaultAccountDBManager(dbPool: dbPool) + let cloudAccountManager = CloudProviderAccountDBManager() + let vaultAccountManager = VaultAccountDBManager() let cloudProviderAccount = CloudProviderAccount(accountUID: "1", cloudProviderType: .dropbox) try cloudAccountManager.saveNewAccount(cloudProviderAccount) @@ -130,7 +131,8 @@ class DatabaseManagerTests: XCTestCase { // MARK: AccountListPosition func testCreateAccountListPositionTrigger() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) + @Dependency(\.database) var database + let cloudAccountManager = CloudProviderAccountDBManager() let firstWebdavCloudProviderAccount = CloudProviderAccount(accountUID: "firstWebdavCloudProviderAccount", cloudProviderType: .webDAV(type: .custom)) try cloudAccountManager.saveNewAccount(firstWebdavCloudProviderAccount) @@ -141,21 +143,21 @@ class DatabaseManagerTests: XCTestCase { let firstDropboxCloudProviderAccount = CloudProviderAccount(accountUID: "firstDropboxCloudProviderAccount", cloudProviderType: .dropbox) try cloudAccountManager.saveNewAccount(firstDropboxCloudProviderAccount) - let firstWebDAVAccountListPosition = try dbPool.read { db in + let firstWebDAVAccountListPosition = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "firstWebdavCloudProviderAccount" && Column("cloudProviderType") == CloudProviderType.webDAV(type: .custom)).fetchOne(db) } XCTAssertNotNil(firstWebDAVAccountListPosition) XCTAssertEqual(0, firstWebDAVAccountListPosition?.position) XCTAssertEqual(1, firstWebDAVAccountListPosition?.id) - let secondWebDAVAccountListPosition = try dbPool.read { db in + let secondWebDAVAccountListPosition = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "secondWebdavCloudProviderAccount" && Column("cloudProviderType") == CloudProviderType.webDAV(type: .custom)).fetchOne(db) } XCTAssertNotNil(secondWebDAVAccountListPosition) XCTAssertEqual(1, secondWebDAVAccountListPosition?.position) XCTAssertEqual(2, secondWebDAVAccountListPosition?.id) - let firstDropboxAccountListPosition = try dbPool.read { db in + let firstDropboxAccountListPosition = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "firstDropboxCloudProviderAccount" && Column("cloudProviderType") == CloudProviderType.dropbox).fetchOne(db) } XCTAssertNotNil(firstDropboxAccountListPosition) @@ -164,7 +166,8 @@ class DatabaseManagerTests: XCTestCase { } func testDeleteCloudProviderAccountUpdatesPositions() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) + @Dependency(\.database) var database + let cloudAccountManager = CloudProviderAccountDBManager() let firstWebdavCloudProviderAccount = CloudProviderAccount(accountUID: "firstWebdavCloudProviderAccount", cloudProviderType: .webDAV(type: .custom)) try cloudAccountManager.saveNewAccount(firstWebdavCloudProviderAccount) @@ -178,28 +181,28 @@ class DatabaseManagerTests: XCTestCase { let firstDropboxCloudProviderAccount = CloudProviderAccount(accountUID: "firstDropboxCloudProviderAccount", cloudProviderType: .dropbox) try cloudAccountManager.saveNewAccount(firstDropboxCloudProviderAccount) - _ = try dbPool.write { db in + _ = try database.write { db in try firstWebdavCloudProviderAccount.delete(db) } - let accountListPositionEntryForFirstWebDAVAccount = try dbPool.read { db in + let accountListPositionEntryForFirstWebDAVAccount = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "firstWebdavCloudProviderAccount").fetchOne(db) } XCTAssertNil(accountListPositionEntryForFirstWebDAVAccount) - let firstAccountListPositionForWebDAV = try dbPool.read { db in + let firstAccountListPositionForWebDAV = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "secondWebdavCloudProviderAccount").fetchOne(db) } XCTAssertNotNil(firstAccountListPositionForWebDAV) XCTAssertEqual(0, firstAccountListPositionForWebDAV?.position) - let secondAccountListPositionForWebDAV = try dbPool.read { db in + let secondAccountListPositionForWebDAV = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "thirdWebdavCloudProviderAccount").fetchOne(db) } XCTAssertNotNil(secondAccountListPositionForWebDAV) XCTAssertEqual(1, secondAccountListPositionForWebDAV?.position) - let firstAccountListPositionForDropbox = try dbPool.read { db in + let firstAccountListPositionForDropbox = try database.read { db in try AccountListPosition.filter(Column("accountUID") == "firstDropboxCloudProviderAccount").fetchOne(db) } XCTAssertNotNil(firstAccountListPositionForDropbox) @@ -207,7 +210,7 @@ class DatabaseManagerTests: XCTestCase { } func testUpdateAccountListPositions() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let cloudAccountManager = CloudProviderAccountDBManager() let firstWebdavCloudProviderAccount = CloudProviderAccount(accountUID: "firstWebdavCloudProviderAccount", cloudProviderType: .webDAV(type: .custom)) try cloudAccountManager.saveNewAccount(firstWebdavCloudProviderAccount) @@ -239,7 +242,7 @@ class DatabaseManagerTests: XCTestCase { } func testGetAllAccountsIsFiltered() throws { - let cloudAccountManager = CloudProviderAccountDBManager(dbPool: dbPool) + let cloudAccountManager = CloudProviderAccountDBManager() let firstWebdavCloudProviderAccount = CloudProviderAccount(accountUID: "firstWebdavCloudProviderAccount", cloudProviderType: .webDAV(type: .custom)) try cloudAccountManager.saveNewAccount(firstWebdavCloudProviderAccount) diff --git a/CryptomatorTests/VaultListViewModelTests.swift b/CryptomatorTests/VaultListViewModelTests.swift index eb317c003..afcac5d0f 100644 --- a/CryptomatorTests/VaultListViewModelTests.swift +++ b/CryptomatorTests/VaultListViewModelTests.swift @@ -15,38 +15,23 @@ import XCTest @testable import CryptomatorCommonCore class VaultListViewModelTests: XCTestCase { - var tmpDir: URL! - var dbPool: DatabasePool! - var cryptomatorDB: CryptomatorDatabase! private var vaultManagerMock: VaultDBManagerMock! private var vaultAccountManagerMock: VaultAccountManagerMock! private var passwordManagerMock: VaultPasswordManagerMock! private var vaultCacheMock: VaultCacheMock! private var fileProviderConnectorMock: FileProviderConnectorMock! - override func setUpWithError() throws { - tmpDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: true, attributes: nil) - let dbURL = tmpDir.appendingPathComponent("db.sqlite") - dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - cryptomatorDB = try CryptomatorDatabase(dbPool) - let cloudProviderManager = CloudProviderDBManager(accountManager: CloudProviderAccountDBManager(dbPool: dbPool)) + override func setUpWithError() throws { + let cloudProviderManager = CloudProviderDBManager(accountManager: CloudProviderAccountDBManager()) vaultAccountManagerMock = VaultAccountManagerMock() passwordManagerMock = VaultPasswordManagerMock() vaultCacheMock = VaultCacheMock() vaultManagerMock = VaultDBManagerMock(providerManager: cloudProviderManager, vaultAccountManager: vaultAccountManagerMock, vaultCache: vaultCacheMock, passwordManager: passwordManagerMock, masterkeyCacheManager: MasterkeyCacheManagerMock(), masterkeyCacheHelper: MasterkeyCacheHelperMock()) fileProviderConnectorMock = FileProviderConnectorMock() - _ = try DatabaseManager(dbPool: dbPool) - } - - override func tearDownWithError() throws { - dbPool = nil - cryptomatorDB = nil - try FileManager.default.removeItem(at: tmpDir) } func testRefreshVaultsIsSorted() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) XCTAssert(vaultListViewModel.getVaults().isEmpty) try vaultListViewModel.refreshItems() @@ -59,7 +44,7 @@ class VaultListViewModelTests: XCTestCase { } func testMoveRow() throws { - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) try vaultListViewModel.refreshItems() @@ -82,7 +67,7 @@ class VaultListViewModelTests: XCTestCase { let cachedVault = CachedVault(vaultUID: "vault2", masterkeyFileData: "".data(using: .utf8)!, vaultConfigToken: nil, lastUpToDateCheck: Date(), masterkeyFileLastModifiedDate: nil, vaultConfigLastModifiedDate: nil) try vaultCacheMock.cache(cachedVault) - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) try vaultListViewModel.refreshItems() @@ -105,7 +90,7 @@ class VaultListViewModelTests: XCTestCase { func testLockVault() throws { let expectation = XCTestExpectation() - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) let vaultInfo = VaultInfo(vaultAccount: VaultAccount(vaultUID: "vault1", delegateAccountUID: "1", vaultPath: CloudPath("/vault1"), vaultName: "vault1"), cloudProviderAccount: CloudProviderAccount(accountUID: "1", cloudProviderType: .dropbox), @@ -131,7 +116,7 @@ class VaultListViewModelTests: XCTestCase { func testRefreshVaultLockedStates() throws { let expectation = XCTestExpectation() - let dbManagerMock = try DatabaseManagerMock(dbPool: dbPool) + let dbManagerMock = DatabaseManagerMock() let vaultListViewModel = VaultListViewModel(dbManager: dbManagerMock, vaultManager: vaultManagerMock, fileProviderConnector: fileProviderConnectorMock) try vaultListViewModel.refreshItems() diff --git a/FileProviderExtension/FileProviderExtension.swift b/FileProviderExtension/FileProviderExtension.swift index 51f0e2ce4..a4004ad4d 100644 --- a/FileProviderExtension/FileProviderExtension.swift +++ b/FileProviderExtension/FileProviderExtension.swift @@ -25,27 +25,19 @@ class FileProviderExtension: NSFileProviderExtension { LoggerSetup.oneTimeSetup() FileProviderExtension.setupIAP() if !FileProviderExtension.sharedDatabaseInitialized { - if let dbURL = CryptomatorDatabase.sharedDBURL { - do { - let dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - CryptomatorDatabase.shared = try CryptomatorDatabase(dbPool) - FileProviderExtension.sharedDatabaseInitialized = true - DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: CryptomatorConstants.appGroupName, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: false) - GoogleDriveSetup.constants = GoogleDriveSetup(clientId: CloudAccessSecrets.googleDriveClientId, redirectURL: CloudAccessSecrets.googleDriveRedirectURL!, sharedContainerIdentifier: CryptomatorConstants.appGroupName) - OneDriveSetup.sharedContainerIdentifier = CryptomatorConstants.appGroupName - let oneDriveConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.oneDriveClientId, redirectUri: CloudAccessSecrets.oneDriveRedirectURI, authority: nil) - oneDriveConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId - OneDriveSetup.clientApplication = try MSALPublicClientApplication(configuration: oneDriveConfiguration) - } catch { - // MARK: Handle error - - FileProviderExtension.databaseError = error - DDLogError("Failed to initialize FPExt sharedDB: \(error)") - } - } else { + do { + FileProviderExtension.sharedDatabaseInitialized = true + DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: CryptomatorConstants.appGroupName, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: false) + GoogleDriveSetup.constants = GoogleDriveSetup(clientId: CloudAccessSecrets.googleDriveClientId, redirectURL: CloudAccessSecrets.googleDriveRedirectURL!, sharedContainerIdentifier: CryptomatorConstants.appGroupName) + OneDriveSetup.sharedContainerIdentifier = CryptomatorConstants.appGroupName + let oneDriveConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.oneDriveClientId, redirectUri: CloudAccessSecrets.oneDriveRedirectURI, authority: nil) + oneDriveConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId + OneDriveSetup.clientApplication = try MSALPublicClientApplication(configuration: oneDriveConfiguration) + } catch { // MARK: Handle error - DDLogError("FPExt - dbURL is nil") + FileProviderExtension.databaseError = error + DDLogError("Failed to initialize FPExt sharedDB: \(error)") } } diff --git a/FileProviderExtensionUI/RootViewController.swift b/FileProviderExtensionUI/RootViewController.swift index 2da0ac122..1da46a054 100644 --- a/FileProviderExtensionUI/RootViewController.swift +++ b/FileProviderExtensionUI/RootViewController.swift @@ -56,22 +56,7 @@ class RootViewController: FPUIActionExtensionViewController { static var oneTimeSetup: () -> Void = { // Set up logger LoggerSetup.oneTimeSetup() - // Set up database - guard let dbURL = CryptomatorDatabase.sharedDBURL else { - // MARK: Handle error - DDLogError("dbURL is nil") - return {} - } - do { - let dbPool = try CryptomatorDatabase.openSharedDatabase(at: dbURL) - CryptomatorDatabase.shared = try CryptomatorDatabase(dbPool) - } catch { - // MARK: Handle error - - DDLogError("Initializing CryptomatorDatabase failed with error: \(error)") - return {} - } // Set up cloud storage services CloudProviderDBManager.shared.useBackgroundSession = false DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: nil, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: true) diff --git a/FileProviderExtensionUI/UnlockVaultViewModel.swift b/FileProviderExtensionUI/UnlockVaultViewModel.swift index 10b46d3aa..1654283a4 100644 --- a/FileProviderExtensionUI/UnlockVaultViewModel.swift +++ b/FileProviderExtensionUI/UnlockVaultViewModel.swift @@ -119,7 +119,7 @@ class UnlockVaultViewModel { passwordManager: VaultPasswordKeychainManager(), vaultAccountManager: VaultAccountDBManager.shared, providerManager: CloudProviderDBManager.shared, - vaultCache: VaultDBCache(dbWriter: CryptomatorDatabase.shared.dbPool)) + vaultCache: VaultDBCache()) } init(domain: NSFileProviderDomain, wrongBiometricalPassword: Bool, fileProviderConnector: FileProviderConnector, passwordManager: VaultPasswordManager, vaultAccountManager: VaultAccountManager, providerManager: CloudProviderManager, vaultCache: VaultCache) { From c8899705a1513aea6c667c07e32be8cf8a4420df Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sun, 1 Oct 2023 00:36:33 +0200 Subject: [PATCH 33/92] Fix swiftlint unneeded_synthesized_initializer warning --- 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 26698c656436a1e1f190cbdc8055828e35dd2e95 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sun, 1 Oct 2023 00:40:00 +0200 Subject: [PATCH 34/92] Run 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 9098c0293c459e7f4d64e7c6096677e84ccc3e47 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sun, 15 Oct 2023 13:32:35 +0200 Subject: [PATCH 35/92] Inject FullVersionChecker with new DI framework --- .../AddVault/AddVaultCoordinator.swift | 4 ++- Cryptomator/AppDelegate.swift | 18 +++++++++++-- .../Onboarding/OnboardingCoordinator.swift | 4 ++- .../VaultList/VaultListViewController.swift | 4 ++- .../CryptomatorUserDefaults.swift | 15 +++++++++++ .../FullVersionChecker.swift | 26 +++++++++++-------- .../Mocks/FullVersionCheckerMock.swift | 2 -- .../FullVersionCheckerTests.swift | 4 ++- .../FileProviderAdapter.swift | 5 ++-- .../FileProviderItem.swift | 6 ++--- .../RootFileProviderItem.swift | 10 ++----- .../FileImportingServiceSource.swift | 7 +++-- .../FileImportingServiceSourceTests.swift | 5 ++-- .../FileProviderAdapterTestCase.swift | 3 ++- .../FileProviderAdapterManagerTests.swift | 4 --- .../FileProviderEnumeratorTests.swift | 4 +-- .../FileProviderItemTests.swift | 16 ++++++++---- .../CloudTaskExecutorTestCase.swift | 4 --- .../CacheManagingServiceSourceTests.swift | 4 --- .../FileProviderExtension.swift | 18 +++++++++++-- 20 files changed, 102 insertions(+), 61 deletions(-) diff --git a/Cryptomator/AddVault/AddVaultCoordinator.swift b/Cryptomator/AddVault/AddVaultCoordinator.swift index 91ebf5db4..5dcd933cb 100644 --- a/Cryptomator/AddVault/AddVaultCoordinator.swift +++ b/Cryptomator/AddVault/AddVaultCoordinator.swift @@ -7,12 +7,14 @@ // import CryptomatorCommonCore +import Dependencies import Foundation import UIKit class AddVaultCoordinator: Coordinator { var childCoordinators = [Coordinator]() var navigationController: UINavigationController + @Dependency(\.fullVersionChecker) private var fullVersionChecker weak var parentCoordinator: MainCoordinator? init(navigationController: UINavigationController) { @@ -76,7 +78,7 @@ class AddVaultCoordinator: Coordinator { } private func isAllowedToCreateNewVault() -> Bool { - return GlobalFullVersionChecker.default.isFullVersion + fullVersionChecker.isFullVersion } } diff --git a/Cryptomator/AppDelegate.swift b/Cryptomator/AppDelegate.swift index 7c99f3dd0..2b8b94633 100644 --- a/Cryptomator/AppDelegate.swift +++ b/Cryptomator/AppDelegate.swift @@ -11,6 +11,7 @@ import CryptomatorCloudAccess import CryptomatorCloudAccessCore import CryptomatorCommon import CryptomatorCommonCore +import Dependencies import MSAL import ObjectiveDropboxOfficial import StoreKit @@ -130,11 +131,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate { private func setupIAP() { #if ALWAYS_PREMIUM DDLogDebug("Always activated premium") - GlobalFullVersionChecker.default = AlwaysActivatedPremium.default CryptomatorUserDefaults.shared.fullVersionUnlocked = true #else DDLogDebug("Freemium version") - GlobalFullVersionChecker.default = UserDefaultsFullVersionChecker.default + #endif + } +} + +/** + Define the liveValue in the main target since compilation flags do not work on Swift Package Manager level. + Be aware that it is needed to set the default value once per app launch (+ also when launching the FileProviderExtension). + */ +extension FullVersionCheckerKey: DependencyKey { + public static var liveValue: FullVersionChecker { + #if ALWAYS_PREMIUM + GlobalFullVersionChecker.default = AlwaysActivatedPremium.default + CryptomatorUserDefaults.shared.fullVersionUnlocked = true + #else + return UserDefaultsFullVersionChecker.default #endif } } diff --git a/Cryptomator/Onboarding/OnboardingCoordinator.swift b/Cryptomator/Onboarding/OnboardingCoordinator.swift index 68d9228d6..6a4869569 100644 --- a/Cryptomator/Onboarding/OnboardingCoordinator.swift +++ b/Cryptomator/Onboarding/OnboardingCoordinator.swift @@ -7,12 +7,14 @@ // import CryptomatorCommonCore +import Dependencies import Foundation import UIKit class OnboardingCoordinator: Coordinator { var childCoordinators = [Coordinator]() var navigationController: UINavigationController + @Dependency(\.fullVersionChecker) private var fullVersionChecker init(navigationController: UINavigationController) { self.navigationController = navigationController @@ -25,7 +27,7 @@ class OnboardingCoordinator: Coordinator { } func showIAP() { - guard !GlobalFullVersionChecker.default.isFullVersion else { + guard !fullVersionChecker.isFullVersion else { navigationController.dismiss(animated: true) return } diff --git a/Cryptomator/VaultList/VaultListViewController.swift b/Cryptomator/VaultList/VaultListViewController.swift index 8ad7d804d..010251d14 100644 --- a/Cryptomator/VaultList/VaultListViewController.swift +++ b/Cryptomator/VaultList/VaultListViewController.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import Combine import CryptomatorCommonCore +import Dependencies import Foundation import UIKit @@ -17,6 +18,7 @@ class VaultListViewController: ListViewController { private let viewModel: VaultListViewModelProtocol private var observer: NSObjectProtocol? + @Dependency(\.fullVersionChecker) private var fullVersionChecker init(with viewModel: VaultListViewModelProtocol) { self.viewModel = viewModel @@ -60,7 +62,7 @@ class VaultListViewController: ListViewController { super.viewDidAppear(animated) if CryptomatorUserDefaults.shared.showOnboardingAtStartup { coordinator?.showOnboarding() - } else if GlobalFullVersionChecker.default.hasExpiredTrial, !CryptomatorUserDefaults.shared.showedTrialExpiredAtStartup { + } else if fullVersionChecker.hasExpiredTrial, !CryptomatorUserDefaults.shared.showedTrialExpiredAtStartup { coordinator?.showTrialExpired() } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorUserDefaults.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorUserDefaults.swift index c4bf4fab6..cb8b81081 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorUserDefaults.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorUserDefaults.swift @@ -7,6 +7,7 @@ // import CocoaLumberjackSwift +import Dependencies import Foundation public protocol CryptomatorSettings { @@ -16,6 +17,20 @@ public protocol CryptomatorSettings { var hasRunningSubscription: Bool { get set } } +private enum CryptomatorSettingsKey: DependencyKey { + #if DEBUG + static let testValue: CryptomatorSettings = CryptomatorSettingsMock() + #endif + static let liveValue: CryptomatorSettings = CryptomatorUserDefaults.shared +} + +public extension DependencyValues { + var cryptomatorSettings: CryptomatorSettings { + get { self[CryptomatorSettingsKey.self] } + set { self[CryptomatorSettingsKey.self] = newValue } + } +} + public class CryptomatorUserDefaults { public static let shared = CryptomatorUserDefaults() diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/FullVersionChecker.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/FullVersionChecker.swift index 0db539a38..9e2e5cac0 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/FullVersionChecker.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/FullVersionChecker.swift @@ -6,27 +6,31 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import Dependencies import Foundation + public protocol FullVersionChecker { var isFullVersion: Bool { get } var hasExpiredTrial: Bool { get } } -/** - Use a singleton to inject the full version checker conveniently at several initializers since compilation flags do not work on Swift Package Manager level. - Be aware that it is needed to set the default value once per app launch (+ also when launching the FileProviderExtension). - */ -public enum GlobalFullVersionChecker { - public static var `default`: FullVersionChecker! +public enum FullVersionCheckerKey {} + +extension FullVersionCheckerKey: TestDependencyKey { + public static let testValue: FullVersionChecker = FullVersionCheckerMock() +} + +public extension DependencyValues { + var fullVersionChecker: FullVersionChecker { + get { self[FullVersionCheckerKey.self] } + set { self[FullVersionCheckerKey.self] = newValue } + } } public class UserDefaultsFullVersionChecker: FullVersionChecker { - public static let `default` = UserDefaultsFullVersionChecker(cryptomatorSettings: CryptomatorUserDefaults.shared) - private let cryptomatorSettings: CryptomatorSettings + @Dependency(\.cryptomatorSettings) private var cryptomatorSettings - init(cryptomatorSettings: CryptomatorSettings) { - self.cryptomatorSettings = cryptomatorSettings - } + public static let `default` = UserDefaultsFullVersionChecker() public var isFullVersion: Bool { if cryptomatorSettings.fullVersionUnlocked { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FullVersionCheckerMock.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FullVersionCheckerMock.swift index d11559613..c6c9dc62d 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FullVersionCheckerMock.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/FullVersionCheckerMock.swift @@ -6,11 +6,9 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // -#if DEBUG import Foundation final class FullVersionCheckerMock: FullVersionChecker { var isFullVersion: Bool = false var hasExpiredTrial: Bool = false } -#endif diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/FullVersionCheckerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/FullVersionCheckerTests.swift index 574f3eabd..eec8fc2ca 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/FullVersionCheckerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/FullVersionCheckerTests.swift @@ -7,6 +7,7 @@ import XCTest @testable import CryptomatorCommonCore +@testable import Dependencies class FullVersionCheckerTests: XCTestCase { var settingsMock: CryptomatorSettingsMock! @@ -14,10 +15,11 @@ class FullVersionCheckerTests: XCTestCase { override func setUpWithError() throws { settingsMock = CryptomatorSettingsMock() + DependencyValues.mockDependency(\.cryptomatorSettings, with: settingsMock) settingsMock.fullVersionUnlocked = false settingsMock.hasRunningSubscription = false settingsMock.trialExpirationDate = nil - fullVersionChecker = UserDefaultsFullVersionChecker(cryptomatorSettings: settingsMock) + fullVersionChecker = UserDefaultsFullVersionChecker() } // MARK: Is Full Version diff --git a/CryptomatorFileProvider/FileProviderAdapter.swift b/CryptomatorFileProvider/FileProviderAdapter.swift index 05fab94dd..ce2564f5d 100644 --- a/CryptomatorFileProvider/FileProviderAdapter.swift +++ b/CryptomatorFileProvider/FileProviderAdapter.swift @@ -9,6 +9,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import Promises @@ -68,7 +69,7 @@ public class FileProviderAdapter: FileProviderAdapterType { private let provider: CloudProvider private let localURLProvider: LocalURLProviderType private let notificator: FileProviderItemUpdateDelegate? - private let fullVersionChecker: FullVersionChecker + @Dependency(\.fullVersionChecker) private var fullVersionChecker private let workflowFactory: WorkflowFactoryLocking private let domainIdentifier: NSFileProviderDomainIdentifier private let fileCoordinator: NSFileCoordinator @@ -87,7 +88,6 @@ public class FileProviderAdapter: FileProviderAdapterType { coordinator: NSFileCoordinator, notificator: FileProviderItemUpdateDelegate? = nil, localURLProvider: LocalURLProviderType, - fullVersionChecker: FullVersionChecker = GlobalFullVersionChecker.default, taskRegistrator: SessionTaskRegistrator) { self.lastUnlockedDate = Date() self.domainIdentifier = domainIdentifier @@ -112,7 +112,6 @@ public class FileProviderAdapter: FileProviderAdapterType { self.provider = provider self.notificator = notificator self.localURLProvider = localURLProvider - self.fullVersionChecker = fullVersionChecker self.fileCoordinator = coordinator self.taskRegistrator = taskRegistrator } diff --git a/CryptomatorFileProvider/FileProviderItem.swift b/CryptomatorFileProvider/FileProviderItem.swift index 6d67274a3..64c822b6f 100644 --- a/CryptomatorFileProvider/FileProviderItem.swift +++ b/CryptomatorFileProvider/FileProviderItem.swift @@ -8,6 +8,7 @@ import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import MobileCoreServices @@ -22,15 +23,14 @@ public class FileProviderItem: NSObject, NSFileProviderItem { let newestVersionLocallyCached: Bool let localURL: URL? let domainIdentifier: NSFileProviderDomainIdentifier - private let fullVersionChecker: FullVersionChecker + @Dependency(\.fullVersionChecker) private var fullVersionChecker - init(metadata: ItemMetadata, domainIdentifier: NSFileProviderDomainIdentifier, newestVersionLocallyCached: Bool = false, localURL: URL? = nil, error: Error? = nil, fullVersionChecker: FullVersionChecker = GlobalFullVersionChecker.default) { + init(metadata: ItemMetadata, domainIdentifier: NSFileProviderDomainIdentifier, newestVersionLocallyCached: Bool = false, localURL: URL? = nil, error: Error? = nil) { self.metadata = metadata self.domainIdentifier = domainIdentifier self.error = error self.newestVersionLocallyCached = newestVersionLocallyCached self.localURL = localURL - self.fullVersionChecker = fullVersionChecker } public var itemIdentifier: NSFileProviderItemIdentifier { diff --git a/CryptomatorFileProvider/RootFileProviderItem.swift b/CryptomatorFileProvider/RootFileProviderItem.swift index 2f5c6fe56..c46984e8b 100644 --- a/CryptomatorFileProvider/RootFileProviderItem.swift +++ b/CryptomatorFileProvider/RootFileProviderItem.swift @@ -7,6 +7,7 @@ // import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation import MobileCoreServices @@ -25,12 +26,5 @@ public class RootFileProviderItem: NSObject, NSFileProviderItem { } } - private let fullVersionChecker: FullVersionChecker - override public convenience init() { - self.init(fullVersionChecker: GlobalFullVersionChecker.default) - } - - init(fullVersionChecker: FullVersionChecker) { - self.fullVersionChecker = fullVersionChecker - } + @Dependency(\.fullVersionChecker) private var fullVersionChecker } diff --git a/CryptomatorFileProvider/ServiceSource/FileImportingServiceSource.swift b/CryptomatorFileProvider/ServiceSource/FileImportingServiceSource.swift index a43596799..680a069ba 100644 --- a/CryptomatorFileProvider/ServiceSource/FileImportingServiceSource.swift +++ b/CryptomatorFileProvider/ServiceSource/FileImportingServiceSource.swift @@ -8,6 +8,7 @@ import CryptomatorCloudAccessCore import CryptomatorCommonCore +import Dependencies import FileProvider import Foundation @@ -17,7 +18,7 @@ public class FileImportingServiceSource: ServiceSource, FileImporting { private let dbPath: URL private let localURLProvider: LocalURLProviderType private let adapterManager: FileProviderAdapterProviding - private let fullVersionChecker: FullVersionChecker + @Dependency(\.fullVersionChecker) private var fullVersionChecker private let taskRegistrator: SessionTaskRegistrator public convenience init(domain: NSFileProviderDomain, notificator: FileProviderNotificatorType, dbPath: URL, delegate: LocalURLProviderType, taskRegistrator: SessionTaskRegistrator) { @@ -26,17 +27,15 @@ public class FileImportingServiceSource: ServiceSource, FileImporting { dbPath: dbPath, delegate: delegate, adapterManager: FileProviderAdapterManager.shared, - fullVersionChecker: GlobalFullVersionChecker.default, taskRegistrator: taskRegistrator) } - init(domain: NSFileProviderDomain, notificator: FileProviderNotificatorType, dbPath: URL, delegate: LocalURLProviderType, adapterManager: FileProviderAdapterProviding, fullVersionChecker: FullVersionChecker, taskRegistrator: SessionTaskRegistrator) { + init(domain: NSFileProviderDomain, notificator: FileProviderNotificatorType, dbPath: URL, delegate: LocalURLProviderType, adapterManager: FileProviderAdapterProviding, taskRegistrator: SessionTaskRegistrator) { self.domain = domain self.notificator = notificator self.dbPath = dbPath self.localURLProvider = delegate self.adapterManager = adapterManager - self.fullVersionChecker = fullVersionChecker self.taskRegistrator = taskRegistrator super.init(serviceName: .fileImporting, exportedInterface: NSXPCInterface(with: FileImporting.self)) } diff --git a/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift b/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift index f39f9fb59..37b28bcc5 100644 --- a/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift +++ b/CryptomatorFileProviderTests/FileImportingServiceSourceTests.swift @@ -10,6 +10,7 @@ import CryptomatorCloudAccessCore import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies @testable import Promises class FileImportingServiceSourceTests: XCTestCase { @@ -21,7 +22,7 @@ class FileImportingServiceSourceTests: XCTestCase { var taskRegistratorMock: SessionTaskRegistratorMock! let dbPath = FileManager.default.temporaryDirectory let domain = NSFileProviderDomain(identifier: .test, displayName: "Foo", pathRelativeToDocumentStorage: "/") - let itemStub = FileProviderItem(metadata: .init(name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: CloudPath("/foo"), isPlaceholderItem: false), domainIdentifier: .test, fullVersionChecker: FullVersionCheckerMock()) + let itemStub = FileProviderItem(metadata: .init(name: "Foo", type: .file, size: nil, parentID: NSFileProviderItemIdentifier.rootContainerDatabaseValue, lastModifiedDate: nil, statusCode: .isUploading, cloudPath: CloudPath("/foo"), isPlaceholderItem: false), domainIdentifier: .test) override func setUpWithError() throws { notificatorMock = FileProviderNotificatorTypeMock() @@ -29,13 +30,13 @@ class FileImportingServiceSourceTests: XCTestCase { adapterProvidingMock = FileProviderAdapterProvidingMock() fullVersionCheckerMock = FullVersionCheckerMock() fullVersionCheckerMock.isFullVersion = true + DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) taskRegistratorMock = SessionTaskRegistratorMock() serviceSource = FileImportingServiceSource(domain: domain, notificator: notificatorMock, dbPath: dbPath, delegate: urlProviderMock, adapterManager: adapterProvidingMock, - fullVersionChecker: fullVersionCheckerMock, taskRegistrator: taskRegistratorMock) } diff --git a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift index 97568ed13..1931101c1 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapter/FileProviderAdapterTestCase.swift @@ -11,6 +11,7 @@ import Promises import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderAdapterTestCase: CloudTaskExecutorTestCase { let fileCoordinator = NSFileCoordinator() @@ -27,6 +28,7 @@ class FileProviderAdapterTestCase: CloudTaskExecutorTestCase { fileProviderItemUpdateDelegateMock = FileProviderItemUpdateDelegateMock() fullVersionCheckerMock = FullVersionCheckerMock() fullVersionCheckerMock.isFullVersion = true + DependencyValues.mockDependency(\.fullVersionChecker, with: fullVersionCheckerMock) taskRegistratorMock = SessionTaskRegistratorMock() adapter = FileProviderAdapter(domainIdentifier: .test, uploadTaskManager: uploadTaskManagerMock, @@ -41,7 +43,6 @@ class FileProviderAdapterTestCase: CloudTaskExecutorTestCase { coordinator: fileCoordinator, notificator: fileProviderItemUpdateDelegateMock, localURLProvider: localURLProviderMock, - fullVersionChecker: fullVersionCheckerMock, taskRegistrator: taskRegistratorMock) uploadTaskManagerMock.createNewTaskRecordForClosure = { return UploadTaskRecord(correspondingItem: $0.id!, lastFailedUploadDate: nil, uploadErrorCode: nil, uploadErrorDomain: nil) diff --git a/CryptomatorFileProviderTests/FileProviderAdapterManagerTests.swift b/CryptomatorFileProviderTests/FileProviderAdapterManagerTests.swift index be46a6f86..683b8e843 100644 --- a/CryptomatorFileProviderTests/FileProviderAdapterManagerTests.swift +++ b/CryptomatorFileProviderTests/FileProviderAdapterManagerTests.swift @@ -34,10 +34,6 @@ class FileProviderAdapterManagerTests: XCTestCase { case test } - override class func setUp() { - GlobalFullVersionChecker.default = FullVersionCheckerMock() - } - #warning("TODO: Replace unlockMonitor with mock") override func setUpWithError() throws { masterkeyCacheManagerMock = MasterkeyCacheManagerMock() diff --git a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift index ecc2721b1..131daf148 100644 --- a/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift +++ b/CryptomatorFileProviderTests/FileProviderEnumeratorTests.swift @@ -25,8 +25,8 @@ class FileProviderEnumeratorTestCase: XCTestCase { let dbPath = FileManager.default.temporaryDirectory let domain = NSFileProviderDomain(vaultUID: "VaultUID-12345", displayName: "Test Vault") let items: [FileProviderItem] = [ - .init(metadata: ItemMetadata(id: 2, name: "Test.txt", type: .file, size: 100, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test.txt"), isPlaceholderItem: false), domainIdentifier: .test, fullVersionChecker: FullVersionCheckerMock()), - .init(metadata: ItemMetadata(id: 3, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestFolder"), isPlaceholderItem: false), domainIdentifier: .test, fullVersionChecker: FullVersionCheckerMock()) + .init(metadata: ItemMetadata(id: 2, name: "Test.txt", type: .file, size: 100, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/Test.txt"), isPlaceholderItem: false), domainIdentifier: .test), + .init(metadata: ItemMetadata(id: 3, name: "TestFolder", type: .folder, size: nil, parentID: 1, lastModifiedDate: nil, statusCode: .isUploaded, cloudPath: CloudPath("/TestFolder"), isPlaceholderItem: false), domainIdentifier: .test) ] let deleteItemIdentifiers = [1, 2, 3].map { NSFileProviderItemIdentifier("\($0)") } diff --git a/CryptomatorFileProviderTests/FileProviderItemTests.swift b/CryptomatorFileProviderTests/FileProviderItemTests.swift index 25ebeee9b..1c4af1510 100644 --- a/CryptomatorFileProviderTests/FileProviderItemTests.swift +++ b/CryptomatorFileProviderTests/FileProviderItemTests.swift @@ -11,6 +11,7 @@ import MobileCoreServices import XCTest @testable import CryptomatorCommonCore @testable import CryptomatorFileProvider +@testable import Dependencies class FileProviderItemTests: XCTestCase { func testRootItem() { @@ -110,50 +111,55 @@ class FileProviderItemTests: XCTestCase { func testUploadingItemRestrictsCapabilityToRead() { let fullVersionCheckerMock = FullVersionCheckerMock() fullVersionCheckerMock.isFullVersion = true + 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: .isUploading, cloudPath: cloudPath, isPlaceholderItem: false) - let item = FileProviderItem(metadata: metadata, domainIdentifier: .test, fullVersionChecker: fullVersionCheckerMock) + 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, fullVersionChecker: fullVersionCheckerMock) + 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, fullVersionChecker: fullVersionCheckerMock) + 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, fullVersionChecker: fullVersionCheckerMock) + 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, fullVersionChecker: fullVersionCheckerMock) + let item = FileProviderItem(metadata: metadata, domainIdentifier: .test) XCTAssertEqual(NSFileProviderItemCapabilities.allowsDeleting, item.capabilities) } diff --git a/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift b/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift index cf9126186..157a855ee 100644 --- a/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift +++ b/CryptomatorFileProviderTests/Middleware/TaskExecutor/CloudTaskExecutorTestCase.swift @@ -24,10 +24,6 @@ class CloudTaskExecutorTestCase: XCTestCase { var deleteItemHelper: DeleteItemHelper! var tmpDirectory: URL! - override class func setUp() { - GlobalFullVersionChecker.default = FullVersionCheckerMock() - } - override func setUpWithError() throws { cloudProviderMock = CustomCloudProviderMock() metadataManagerMock = MetadataManagerMock() diff --git a/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift index 77c284290..2772c4de2 100644 --- a/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift +++ b/CryptomatorFileProviderTests/ServiceSource/CacheManagingServiceSourceTests.swift @@ -20,10 +20,6 @@ class CacheManagingServiceSourceTests: XCTestCase { let domains = [NSFileProviderDomain(identifier: NSFileProviderDomainIdentifier("1")), NSFileProviderDomain(identifier: NSFileProviderDomainIdentifier("2"))] - override class func setUp() { - GlobalFullVersionChecker.default = FullVersionCheckerMock() - } - override func setUpWithError() throws { cacheManagerFactoryMock = CachedFileManagerFactoryMock() domainProviderMock = NSFileProviderDomainProviderMock() diff --git a/FileProviderExtension/FileProviderExtension.swift b/FileProviderExtension/FileProviderExtension.swift index a4004ad4d..b5de8dd08 100644 --- a/FileProviderExtension/FileProviderExtension.swift +++ b/FileProviderExtension/FileProviderExtension.swift @@ -10,6 +10,7 @@ import CocoaLumberjackSwift import CryptomatorCloudAccessCore import CryptomatorCommonCore import CryptomatorFileProvider +import Dependencies import FileProvider import MSAL @@ -308,15 +309,28 @@ class FileProviderExtension: NSFileProviderExtension { static var setupIAP: () -> Void = { #if ALWAYS_PREMIUM DDLogDebug("Always activated premium") - GlobalFullVersionChecker.default = AlwaysActivatedPremium.default #else DDLogDebug("Freemium version") - GlobalFullVersionChecker.default = UserDefaultsFullVersionChecker.default #endif return {} }() } +/** + Define the liveValue in the main target since compilation flags do not work on Swift Package Manager level. + Be aware that it is needed to set the default value once per app launch (+ also when launching the FileProviderExtension). + */ +extension FullVersionCheckerKey: DependencyKey { + public static var liveValue: FullVersionChecker { + #if ALWAYS_PREMIUM + GlobalFullVersionChecker.default = AlwaysActivatedPremium.default + CryptomatorUserDefaults.shared.fullVersionUnlocked = true + #else + return UserDefaultsFullVersionChecker.default + #endif + } +} + enum FileProviderDecoratorSetupError: Error { case fileProviderManagerIsNil case domainIsNil From 5ab938aa38faf5e51eb18d296292ef220ad95f2e Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:49:00 +0200 Subject: [PATCH 36/92] Fix always premium --- Cryptomator/AppDelegate.swift | 3 +-- FileProviderExtension/FileProviderExtension.swift | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cryptomator/AppDelegate.swift b/Cryptomator/AppDelegate.swift index 2b8b94633..729e12a5c 100644 --- a/Cryptomator/AppDelegate.swift +++ b/Cryptomator/AppDelegate.swift @@ -145,8 +145,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { extension FullVersionCheckerKey: DependencyKey { public static var liveValue: FullVersionChecker { #if ALWAYS_PREMIUM - GlobalFullVersionChecker.default = AlwaysActivatedPremium.default - CryptomatorUserDefaults.shared.fullVersionUnlocked = true + return AlwaysActivatedPremium.default #else return UserDefaultsFullVersionChecker.default #endif diff --git a/FileProviderExtension/FileProviderExtension.swift b/FileProviderExtension/FileProviderExtension.swift index b5de8dd08..ee8f385b9 100644 --- a/FileProviderExtension/FileProviderExtension.swift +++ b/FileProviderExtension/FileProviderExtension.swift @@ -309,6 +309,7 @@ class FileProviderExtension: NSFileProviderExtension { static var setupIAP: () -> Void = { #if ALWAYS_PREMIUM DDLogDebug("Always activated premium") + CryptomatorUserDefaults.shared.fullVersionUnlocked = true #else DDLogDebug("Freemium version") #endif @@ -323,8 +324,7 @@ class FileProviderExtension: NSFileProviderExtension { extension FullVersionCheckerKey: DependencyKey { public static var liveValue: FullVersionChecker { #if ALWAYS_PREMIUM - GlobalFullVersionChecker.default = AlwaysActivatedPremium.default - CryptomatorUserDefaults.shared.fullVersionUnlocked = true + return AlwaysActivatedPremium.default #else return UserDefaultsFullVersionChecker.default #endif From 153fe0af8c81b8e4edf66198f157b16301bb1b94 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:27:48 +0200 Subject: [PATCH 37/92] Run build & test also for Premium configuration --- .github/workflows/build.yml | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2f7bb67ca..856aa233f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,15 +2,18 @@ name: Build on: [push] +env: + DERIVED_DATA_PATH: 'DerivedData' + DEVICE: 'iPhone 14 Pro' jobs: build: name: Build and test - runs-on: macos-12 - env: - DERIVED_DATA_PATH: 'DerivedData' - DEVICE: 'iPhone 12 Pro' + runs-on: macos-13 if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" + strategy: + matrix: + version: ['Freemium', 'Premium'] steps: - uses: actions/checkout@v3 - uses: actions/cache@v3 @@ -27,11 +30,20 @@ jobs: run: | cd fastlane ./scripts/create-cloud-access-secrets.sh + - name: Configuration for Freemium + if: ${{ matrix.version == 'Freemium' }} + run: | + echo "BUILD_CMD=-enableCodeCoverage YES" >> $GITHUB_ENV + - name: Configuration for Premium + if: ${{ matrix.version == 'Premium' }} + run: | + echo "BUILD_CMD=SWIFT_ACTIVE_COMPILATION_CONDITIONS='\$(inherited) ALWAYS_PREMIUM'" >> $GITHUB_ENV - name: Build - run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild clean build-for-testing -scheme 'AllTests' -destination "name=$DEVICE" -derivedDataPath $DERIVED_DATA_PATH -enableCodeCoverage YES | xcpretty + run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild clean build-for-testing -scheme 'AllTests' -destination "name=$DEVICE" -derivedDataPath $DERIVED_DATA_PATH -enableCodeCoverage YES ${{ env.BUILD_CMD }} | xcpretty - name: Test run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild test-without-building -xctestrun $(find . -type f -name "*.xctestrun") -destination "name=$DEVICE" -derivedDataPath $DERIVED_DATA_PATH | xcpretty - name: Upload code coverage report + if: ${{ matrix.version == 'Freemium' }} run: | gem install slather slather coverage -x --build-directory $DERIVED_DATA_PATH --ignore "$DERIVED_DATA_PATH/SourcePackages/*" --scheme AllTests Cryptomator.xcodeproj @@ -39,3 +51,4 @@ jobs: env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} continue-on-error: true + From a7e9b127f5fdeb90a5d8d7afe31f37ce6df43c43 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:30:04 +0200 Subject: [PATCH 38/92] Update build.yml --- .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 856aa233f..0baecb1ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,7 +39,7 @@ jobs: run: | echo "BUILD_CMD=SWIFT_ACTIVE_COMPILATION_CONDITIONS='\$(inherited) ALWAYS_PREMIUM'" >> $GITHUB_ENV - name: Build - run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild clean build-for-testing -scheme 'AllTests' -destination "name=$DEVICE" -derivedDataPath $DERIVED_DATA_PATH -enableCodeCoverage YES ${{ env.BUILD_CMD }} | xcpretty + run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild clean build-for-testing -scheme 'AllTests' -destination "name=$DEVICE" -derivedDataPath $DERIVED_DATA_PATH ${{ env.BUILD_CMD }} | xcpretty - name: Test run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild test-without-building -xctestrun $(find . -type f -name "*.xctestrun") -destination "name=$DEVICE" -derivedDataPath $DERIVED_DATA_PATH | xcpretty - name: Upload code coverage report From 7852ddedc556931674502d64510afeedc5af76d9 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 17 Oct 2023 00:02:47 +0200 Subject: [PATCH 39/92] Revert use of macOS 13 somehow this runner is way slower than the macOS 12 one --- .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 0baecb1ea..5bcbada7a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,12 +4,12 @@ on: [push] env: DERIVED_DATA_PATH: 'DerivedData' - DEVICE: 'iPhone 14 Pro' + DEVICE: 'iPhone 12 Pro' jobs: build: name: Build and test - runs-on: macos-13 + runs-on: macos-12 if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" strategy: matrix: From 90d6fd4c324a8abea76633305fab93b8bb4957a3 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 17 Oct 2023 12:13:12 +0200 Subject: [PATCH 40/92] Reverted env diff --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5bcbada7a..72f3250f7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,14 +2,14 @@ name: Build on: [push] -env: - DERIVED_DATA_PATH: 'DerivedData' - DEVICE: 'iPhone 12 Pro' jobs: build: name: Build and test runs-on: macos-12 + env: + DERIVED_DATA_PATH: 'DerivedData' + DEVICE: 'iPhone 12 Pro' if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" strategy: matrix: From 4662163bb17a27350ee01486e169613eb06e040b Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 17 Oct 2023 12:17:22 +0200 Subject: [PATCH 41/92] Renamed matrix strategy to match fastlane config --- .github/workflows/build.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 72f3250f7..54a523199 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" strategy: matrix: - version: ['Freemium', 'Premium'] + config: ['freemium', 'premium'] steps: - uses: actions/checkout@v3 - uses: actions/cache@v3 @@ -30,12 +30,12 @@ jobs: run: | cd fastlane ./scripts/create-cloud-access-secrets.sh - - name: Configuration for Freemium - if: ${{ matrix.version == 'Freemium' }} + - name: Configuration for freemium + if: ${{ matrix.config == 'freemium' }} run: | echo "BUILD_CMD=-enableCodeCoverage YES" >> $GITHUB_ENV - - name: Configuration for Premium - if: ${{ matrix.version == 'Premium' }} + - name: Configuration for premium + if: ${{ matrix.config == 'premium' }} run: | echo "BUILD_CMD=SWIFT_ACTIVE_COMPILATION_CONDITIONS='\$(inherited) ALWAYS_PREMIUM'" >> $GITHUB_ENV - name: Build @@ -43,7 +43,7 @@ jobs: - name: Test run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild test-without-building -xctestrun $(find . -type f -name "*.xctestrun") -destination "name=$DEVICE" -derivedDataPath $DERIVED_DATA_PATH | xcpretty - name: Upload code coverage report - if: ${{ matrix.version == 'Freemium' }} + if: ${{ matrix.config == 'freemium' }} run: | gem install slather slather coverage -x --build-directory $DERIVED_DATA_PATH --ignore "$DERIVED_DATA_PATH/SourcePackages/*" --scheme AllTests Cryptomator.xcodeproj @@ -51,4 +51,3 @@ jobs: env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} continue-on-error: true - 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 42/92] 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 43/92] 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 44/92] 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 45/92] 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 46/92] 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 47/92] 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 48/92] 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 49/92] 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 50/92] 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 51/92] 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 52/92] 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 53/92] 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 54/92] 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 55/92] 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 56/92] 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 57/92] 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 58/92] 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 59/92] 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) From 2c04784410b730b947a91e0391668fae0aa99be3 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 13 Dec 2023 21:37:57 +0100 Subject: [PATCH 60/92] Handle 410 status code --- .../CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index f32906807..34132b59b 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -54,7 +54,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return .success(data, httpResponse?.allHeaderFields ?? [:]) case 402: return .licenseExceeded - case 403: + case 403, 410: return .accessNotGranted case 404: return .needsDeviceRegistration From 59da4c3901519d798861b4e3f11db3c73dce9794 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 13 Dec 2023 22:37:41 +0100 Subject: [PATCH 61/92] Handle 449 status code --- .../Hub/CryptomatorHubAuthenticator.swift | 4 ++++ .../Hub/HubAuthenticationCoordinator.swift | 14 ++++++++++++++ .../Hub/HubAuthenticationViewModel.swift | 5 +++++ SharedResources/en.lproj/Localizable.strings | 3 +++ 4 files changed, 26 insertions(+) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 34132b59b..fe996aabc 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -17,6 +17,7 @@ public enum HubAuthenticationFlow { case accessNotGranted case needsDeviceRegistration case licenseExceeded + case requiresAccountInitialization(at: URL) } public enum CryptomatorHubAuthenticatorError: Error { @@ -58,6 +59,9 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return .accessNotGranted case 404: return .needsDeviceRegistration + case 449: + let profileURL = baseURL.appendingPathComponent("/app/profile") + return .requiresAccountInitialization(at: profileURL) default: throw CryptomatorHubAuthenticatorError.unexpectedResponse } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift index 9e700e326..008c73b76 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationCoordinator.swift @@ -105,4 +105,18 @@ extension HubAuthenticationCoordinator: HubAuthenticationViewModelDelegate { public func hubAuthenticationViewModelWantsToHideLoadingIndicator() async { await hideProgressHUD() } + + public func hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert(profileURL: URL) { + let alertController = UIAlertController(title: LocalizedString.getValue("hubAuthentication.requireAccountInit.alert.title"), + message: LocalizedString.getValue("hubAuthentication.requireAccountInit.alert.message"), + preferredStyle: .alert) + + alertController.addAction(UIAlertAction(title: LocalizedString.getValue("common.button.cancel"), style: .cancel)) + let goToProfileAction = UIAlertAction(title: LocalizedString.getValue("hubAuthentication.requireAccountInit.alert.actionButton"), + style: .default, + handler: { _ in UIApplication.shared.open(profileURL) }) + alertController.addAction(goToProfileAction) + + navigationController.present(alertController, animated: true) + } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index 197f351f3..bd8cf072e 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -20,6 +20,9 @@ public protocol HubAuthenticationViewModelDelegate: AnyObject { @MainActor func hubAuthenticationViewModelWantsToHideLoadingIndicator() async + + @MainActor + func hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert(profileURL: URL) } public final class HubAuthenticationViewModel: ObservableObject { @@ -101,6 +104,8 @@ public final class HubAuthenticationViewModel: ObservableObject { await setState(to: .deviceRegistration(.deviceName)) case .licenseExceeded: await setState(to: .licenseExceeded) + case let .requiresAccountInitialization(profileURL): + await delegate?.hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert(profileURL: profileURL) } } diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index f98e2720a..65ea219a7 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -121,6 +121,9 @@ "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."; +"hubAuthentication.requireAccountInit.alert.title" = "Action required"; +"hubAuthentication.requireAccountInit.alert.message" = "To proceed, please complete the steps required in your Hub user profile."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Go to profile"; "intents.saveFile.missingFile" = "The provided file is not valid."; "intents.saveFile.invalidFolder" = "The provided folder is not valid."; From 69155872532281cef95379a2fd6dd9f933ee7ed7 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sat, 23 Dec 2023 22:48:26 +0100 Subject: [PATCH 62/92] DeviceSetup: Decrypt / Encrypt user key --- .../CryptomatorCommonCore/JWEHelper.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift index 2d88abc2b..e7a6775ba 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift @@ -19,6 +19,7 @@ public enum JWEHelper { 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 { + // TODO: Change Error throw VaultManagerError.invalidDecrypter } let payload = try jwe.decrypt(using: decrypter) @@ -29,4 +30,40 @@ public enum JWEHelper { } return Masterkey.createFromRaw(rawKey: [UInt8](masterkeyData)) } + + public static func decryptUserKey(jwe: JWE, setupCode: String) throws -> String { + guard let decrypter = Decrypter(keyManagementAlgorithm: .PBES2_HS512_A256KW, contentEncryptionAlgorithm: .A256GCM, decryptionKey: setupCode) else { + // TODO: Change Error + throw VaultManagerError.invalidDecrypter + } + let payload = try jwe.decrypt(using: decrypter) + let payloadData = payload.data() + guard let jsonObject = try JSONSerialization.jsonObject(with: payloadData, options: []) as? [String: Any], + let key = jsonObject["key"] as? String else { + // TODO: Change Error + throw VaultManagerError.invalidPayloadMasterkey + } + return key + } + + public static func encryptUserKey(userKey: String, deviceKey: P384.KeyAgreement.PublicKey) throws -> JWE { + let header = JWEHeader(keyManagementAlgorithm: .ECDH_ES, contentEncryptionAlgorithm: .A256GCM) + let x = deviceKey.x963Representation[1 ..< 49] + let y = deviceKey.x963Representation[49 ..< 97] + let encryptionKey = ECPublicKey(crv: .P384, + x: x.base64EncodedString(), + y: y.base64EncodedString()) + guard let encrypter = Encrypter(keyManagementAlgorithm: .ECDH_ES, + contentEncryptionAlgorithm: .A256GCM, + encryptionKey: encryptionKey) else { + // TODO: Change Error + throw VaultManagerError.invalidDecrypter + } + guard let userKey = userKey.data(using: .utf8) else { + // TODO: Change Error + throw VaultManagerError.invalidDecrypter + } + let payload = Payload(userKey) + return try JWE(header: header, payload: payload, encrypter: encrypter) + } } From df39a7850d4aad77c1224f6cf5928e0e50ae6a0a Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sat, 23 Dec 2023 22:50:49 +0100 Subject: [PATCH 63/92] Device Setup: Register Device with new requests --- .../Hub/CryptomatorHubAuthenticator.swift | 101 ++++++++++++++++-- .../Hub/HubDeviceRegisteringService.swift | 30 +++--- 2 files changed, 107 insertions(+), 24 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index fe996aabc..0ca45eadf 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -67,25 +67,84 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving } } - public func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws { - let deviceID = try getDeviceID() + public func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String) async throws { + guard let apiBaseURL = hubConfig.getAPIBaseURL() else { + // TODO: More specific error + throw CryptomatorHubAuthenticatorError.invalidBaseURL + } + + let userDto = try await getUser(apiBaseURL: apiBaseURL, authState: authState) + let publicKey = try cryptomatorHubKeyProvider.getPublicKey() + + let encryptedUserKeyJWE = try getEncryptedUserKeyJWE(userDto: userDto, setupCode: setupCode, publicKey: publicKey) + + let deviceID = try getDeviceID() let derPubKey = publicKey.derRepresentation - let dto = CreateDeviceDto(id: deviceID, name: name, type: "MOBILE", publicKey: derPubKey.base64URLEncodedString()) - guard let devicesResourceURL = URL(string: hubConfig.devicesResourceUrl) else { - throw CryptomatorHubAuthenticatorError.invalidDeviceResourceURL + + let now = getCurrentDateForDeviceCreation() + + let dto = CreateDeviceDto(id: deviceID, + name: name, + type: "MOBILE", + publicKey: derPubKey.base64EncodedString(), + userPrivateKey: encryptedUserKeyJWE.compactSerializedString, + creationTime: now) + + try await createDevice(dto, apiBaseURL: apiBaseURL, authState: authState) + } + + private func getUser(apiBaseURL: URL, authState: OIDAuthState) async throws -> UserDTO { + let url = apiBaseURL.appendingPathComponent("users/me") + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + throw CryptomatorHubAuthenticatorError.missingAccessToken + } + var request = URLRequest(url: url) + request.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (data, response) = try await URLSession.shared.data(with: request) + let httpResponse = response as? HTTPURLResponse + guard httpResponse?.statusCode == 200 else { + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + return try JSONDecoder().decode(UserDTO.self, from: data) + } + + private func getEncryptedUserKeyJWE(userDto: UserDTO, setupCode: String, publicKey: P384.KeyAgreement.PublicKey) throws -> JWE { + guard let privateKey = userDto.privateKey.data(using: .utf8) else { + // TODO: Throw proper error + fatalError() } - let keyURL = devicesResourceURL.appendingPathComponent("\(deviceID)") - var request = URLRequest(url: keyURL) + let jwe = try JWE(compactSerialization: privateKey) + + let userKey = try JWEHelper.decryptUserKey(jwe: jwe, setupCode: setupCode) + + return try JWEHelper.encryptUserKey(userKey: userKey, deviceKey: publicKey) + } + + private func getCurrentDateForDeviceCreation() -> String { + let formatter = ISO8601DateFormatter() + formatter.timeZone = TimeZone(secondsFromGMT: 0) // Set to UTC + return formatter.string(from: Date()) + } + + private func createDevice(_ dto: CreateDeviceDto, apiBaseURL: URL, authState: OIDAuthState) async throws { + let deviceResourceURL = apiBaseURL.appendingPathComponent("devices") + let deviceURL = deviceResourceURL.appendingPathComponent(dto.id) + + var request = URLRequest(url: deviceURL) 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 { + guard let secondAccessToken = accessToken else { throw CryptomatorHubAuthenticatorError.missingAccessToken } - request.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + request.allHTTPHeaderFields = ["Authorization": "Bearer \(secondAccessToken)"] + let (_, response) = try await URLSession.shared.data(with: request) + switch (response as? HTTPURLResponse)?.statusCode { case 201: break @@ -115,6 +174,12 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving let name: String let type: String let publicKey: String + let userPrivateKey: String + let creationTime: String + } + + private struct DeviceDto: Codable { + let userPrivateKey: String } } @@ -161,3 +226,21 @@ extension String { return String(dropFirst(prefix.count)) } } + +extension HubConfig { + func getAPIBaseURL() -> URL? { + return URL(string: apiBaseUrl) + } + + func getWebAppURL() -> URL? { + getAPIBaseURL()?.deletingLastPathComponent().appendingPathComponent("app") + } +} + +private struct UserDTO: Codable { + let id: String + let name: String + let publicKey: String + let privateKey: String + let setupCode: String +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteringService.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteringService.swift index b1bd034f5..bf1de0b72 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteringService.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteringService.swift @@ -13,7 +13,7 @@ import Foundation import XCTestDynamicOverlay public protocol HubDeviceRegistering { - func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws + func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String) async throws } private enum HubDeviceRegisteringKey: DependencyKey { @@ -32,7 +32,7 @@ extension DependencyValues { #if DEBUG final class UnimplementedHubDeviceRegisteringService: HubDeviceRegistering { - func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) async throws { + func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String) async throws { XCTFail("\(Self.self).registerDevice is unimplemented.") } } @@ -43,24 +43,24 @@ final class UnimplementedHubDeviceRegisteringService: HubDeviceRegistering { final class HubDeviceRegisteringMock: HubDeviceRegistering { // MARK: - registerDevice - var registerDeviceWithNameHubConfigAuthStateThrowableError: Error? - var registerDeviceWithNameHubConfigAuthStateCallsCount = 0 - var registerDeviceWithNameHubConfigAuthStateCalled: Bool { - registerDeviceWithNameHubConfigAuthStateCallsCount > 0 + var registerDeviceWithNameHubConfigAuthStateSetupCodeThrowableError: Error? + var registerDeviceWithNameHubConfigAuthStateSetupCodeCallsCount = 0 + var registerDeviceWithNameHubConfigAuthStateSetupCodeCalled: Bool { + registerDeviceWithNameHubConfigAuthStateSetupCodeCallsCount > 0 } - var registerDeviceWithNameHubConfigAuthStateReceivedArguments: (name: String, hubConfig: HubConfig, authState: OIDAuthState)? - var registerDeviceWithNameHubConfigAuthStateReceivedInvocations: [(name: String, hubConfig: HubConfig, authState: OIDAuthState)] = [] - var registerDeviceWithNameHubConfigAuthStateClosure: ((String, HubConfig, OIDAuthState) throws -> Void)? + var registerDeviceWithNameHubConfigAuthStateSetupCodeReceivedArguments: (name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String)? + var registerDeviceWithNameHubConfigAuthStateSetupCodeReceivedInvocations: [(name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String)] = [] + var registerDeviceWithNameHubConfigAuthStateSetupCodeClosure: ((String, HubConfig, OIDAuthState, String) throws -> Void)? - func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState) throws { - if let error = registerDeviceWithNameHubConfigAuthStateThrowableError { + func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String) throws { + if let error = registerDeviceWithNameHubConfigAuthStateSetupCodeThrowableError { throw error } - registerDeviceWithNameHubConfigAuthStateCallsCount += 1 - registerDeviceWithNameHubConfigAuthStateReceivedArguments = (name: name, hubConfig: hubConfig, authState: authState) - registerDeviceWithNameHubConfigAuthStateReceivedInvocations.append((name: name, hubConfig: hubConfig, authState: authState)) - try registerDeviceWithNameHubConfigAuthStateClosure?(name, hubConfig, authState) + registerDeviceWithNameHubConfigAuthStateSetupCodeCallsCount += 1 + registerDeviceWithNameHubConfigAuthStateSetupCodeReceivedArguments = (name: name, hubConfig: hubConfig, authState: authState, setupCode: setupCode) + registerDeviceWithNameHubConfigAuthStateSetupCodeReceivedInvocations.append((name: name, hubConfig: hubConfig, authState: authState, setupCode: setupCode)) + try registerDeviceWithNameHubConfigAuthStateSetupCodeClosure?(name, hubConfig, authState, setupCode) } } // swiftlint: enable all From 320b1c0a696ec289437bc2b17260f819a72d83fb Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sat, 23 Dec 2023 22:56:53 +0100 Subject: [PATCH 64/92] Update unlock flow (temp) --- .../Hub/CryptomatorHubAuthenticator.swift | 162 +++++++++++++++--- 1 file changed, 137 insertions(+), 25 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 0ca45eadf..7edd9138a 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -11,9 +11,10 @@ import CryptoKit import CryptomatorCloudAccessCore import Dependencies import Foundation +import JOSESwift public enum HubAuthenticationFlow { - case success(Data, [AnyHashable: Any]) + case success(encryptedVaultKey: String, encryptedUserKey: String) case accessNotGranted case needsDeviceRegistration case licenseExceeded @@ -28,43 +29,61 @@ public enum CryptomatorHubAuthenticatorError: Error { case invalidBaseURL case invalidDeviceResourceURL case missingAccessToken + case incompatibleHubVersion } public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving { private static let scheme = "hub+" + private static let minimumHubVersion = 1 @Dependency(\.cryptomatorHubKeyProvider) private var cryptomatorHubKeyProvider public init() {} public func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { - guard let baseURL = createBaseURL(vaultConfig: vaultConfig) else { + guard let hubConfig = vaultConfig.allegedHubConfig, let vaultBaseURL = getVaultBaseURL(from: vaultConfig) else { + // handle error + fatalError() + } + + guard let apiBaseURL = hubConfig.getAPIBaseURL(), let webAppURL = hubConfig.getWebAppURL() else { + // TODO: More specific error throw CryptomatorHubAuthenticatorError.invalidBaseURL } - let deviceID = try getDeviceID() - let url = baseURL.appendingPathComponent("/keys").appendingPathComponent("/\(deviceID)") - let (accessToken, _) = try await authState.performAction() - guard let accessToken = accessToken else { - throw CryptomatorHubAuthenticatorError.missingAccessToken + + guard try await hubInstanceHasMinimumAPILevel(of: Self.minimumHubVersion, apiBaseURL: apiBaseURL, authState: authState) else { + throw CryptomatorHubAuthenticatorError.incompatibleHubVersion } - var urlRequest = URLRequest(url: url) - urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] - let (data, response) = try await URLSession.shared.data(with: urlRequest) - let httpResponse = response as? HTTPURLResponse - switch httpResponse?.statusCode { - case 200: - return .success(data, httpResponse?.allHeaderFields ?? [:]) - case 402: - return .licenseExceeded - case 403, 410: +// let deviceID = try getDeviceID() + + let retrieveMasterkeyResponse = try await getVaultMasterKey(vaultBaseURL: vaultBaseURL, + authState: authState, + webAppURL: webAppURL) + + let encryptedVaultKey: String + switch retrieveMasterkeyResponse { + case let .success(key): + encryptedVaultKey = key + case .accessNotGranted: return .accessNotGranted - case 404: - return .needsDeviceRegistration - case 449: - let profileURL = baseURL.appendingPathComponent("/app/profile") + case .licenseExceeded: + return .licenseExceeded + case let .requiresAccountInitialization(profileURL): return .requiresAccountInitialization(at: profileURL) - default: - throw CryptomatorHubAuthenticatorError.unexpectedResponse + case .legacyHubVersion: + throw CryptomatorHubAuthenticatorError.incompatibleHubVersion } + + let retrieveUserPrivateKeyResponse = try await getUserKey(apiBaseURL: apiBaseURL, authState: authState) + + let encryptedUserKey: String + switch retrieveUserPrivateKeyResponse { + case let .unlockedSucceeded(deviceDto): + encryptedUserKey = deviceDto.userPrivateKey + case .deviceSetup: + return .needsDeviceRegistration + } + + return .success(encryptedVaultKey: encryptedVaultKey, encryptedUserKey: encryptedUserKey) } public func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String) async throws { @@ -155,7 +174,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving } } - func createBaseURL(vaultConfig: UnverifiedVaultConfig) -> URL? { + private func getVaultBaseURL(from vaultConfig: UnverifiedVaultConfig) -> URL? { guard let keyId = vaultConfig.keyId, keyId.hasPrefix(CryptomatorHubAuthenticator.scheme) else { return nil } @@ -163,12 +182,81 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return URL(string: baseURLPath) } - func getDeviceID() throws -> String { + private func getDeviceID() throws -> String { let publicKey = try cryptomatorHubKeyProvider.getPublicKey() let digest = SHA256.hash(data: publicKey.derRepresentation) return digest.data.base16EncodedString } + private func hubInstanceHasMinimumAPILevel(of minimumLevel: Int, apiBaseURL: URL, authState: OIDAuthState) async throws -> Bool { + let url = apiBaseURL.appendingPathComponent("config") + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + throw CryptomatorHubAuthenticatorError.missingAccessToken + } + var request = URLRequest(url: url) + request.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (data, response) = try await URLSession.shared.data(with: request) + + guard (response as? HTTPURLResponse)?.statusCode == 200 else { + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + let config = try JSONDecoder().decode(APIConfigDto.self, from: data) + return config.apiLevel >= minimumLevel + } + + private func getVaultMasterKey(vaultBaseURL: URL, authState: OIDAuthState, webAppURL: URL) async throws -> RetrieveVaultMasterkeyEncryptedForUserResponse { + let url = vaultBaseURL.appendingPathComponent("access-token") + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + throw CryptomatorHubAuthenticatorError.missingAccessToken + } + var urlRequest = URLRequest(url: url) + urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (data, response) = try await URLSession.shared.data(with: urlRequest) + let httpResponse = response as? HTTPURLResponse + switch httpResponse?.statusCode { + case 200: + guard let body = String(data: data, encoding: .utf8) else { + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + return .success(encryptedVaultKey: body) + case 402: + return .licenseExceeded + case 403, 410: + return .accessNotGranted + case 404: + return .legacyHubVersion + case 449: + let profileURL = webAppURL.appendingPathComponent("profile") + return .requiresAccountInitialization(at: profileURL) + default: + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + } + + private func getUserKey(apiBaseURL: URL, authState: OIDAuthState) async throws -> RetrieveUserEncryptedPKResponse { + let deviceID = try getDeviceID() + let url = apiBaseURL.appendingPathComponent("devices").appendingPathComponent(deviceID) + let (accessToken, _) = try await authState.performAction() + guard let accessToken = accessToken else { + throw CryptomatorHubAuthenticatorError.missingAccessToken + } + var urlRequest = URLRequest(url: url) + urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"] + let (data, response) = try await URLSession.shared.data(with: urlRequest) + let httpResponse = response as? HTTPURLResponse + + switch httpResponse?.statusCode { + case 200: + return try .unlockedSucceeded(JSONDecoder().decode(DeviceDto.self, from: data)) + case 404: + return .deviceSetup + default: + throw CryptomatorHubAuthenticatorError.unexpectedResponse + } + } + struct CreateDeviceDto: Codable { let id: String let name: String @@ -178,6 +266,30 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving let creationTime: String } + private struct APIConfigDto: Codable { + let apiLevel: Int + } + + private enum RetrieveUserEncryptedPKResponse { + // 200 + case unlockedSucceeded(DeviceDto) + // 404 + case deviceSetup + } + + private enum RetrieveVaultMasterkeyEncryptedForUserResponse { + // 200 + case success(encryptedVaultKey: String) + // 403, 410 + case accessNotGranted + // 402 + case licenseExceeded + // 449 + case requiresAccountInitialization(at: URL) + // 404 + case legacyHubVersion + } + private struct DeviceDto: Codable { let userPrivateKey: String } From 9ac1232e1fd2259fcbcb05a2d1eb631bd2f4bd99 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sat, 23 Dec 2023 22:58:08 +0100 Subject: [PATCH 65/92] Remove unneeded method --- .../Hub/CryptomatorHubAuthenticator.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 7edd9138a..699484e6f 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -101,7 +101,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving let deviceID = try getDeviceID() let derPubKey = publicKey.derRepresentation - let now = getCurrentDateForDeviceCreation() + let now = ISO8601DateFormatter().string(from: Date()) let dto = CreateDeviceDto(id: deviceID, name: name, @@ -141,12 +141,6 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return try JWEHelper.encryptUserKey(userKey: userKey, deviceKey: publicKey) } - private func getCurrentDateForDeviceCreation() -> String { - let formatter = ISO8601DateFormatter() - formatter.timeZone = TimeZone(secondsFromGMT: 0) // Set to UTC - return formatter.string(from: Date()) - } - private func createDevice(_ dto: CreateDeviceDto, apiBaseURL: URL, authState: OIDAuthState) async throws { let deviceResourceURL = apiBaseURL.appendingPathComponent("devices") let deviceURL = deviceResourceURL.appendingPathComponent(dto.id) From b35eecb8c35258ab431345039ced022d15db1e91 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sun, 24 Dec 2023 12:21:35 +0100 Subject: [PATCH 66/92] Add Account Key input --- CryptomatorCommon/Package.swift | 3 +- .../Hub/HubAuthenticationView.swift | 1 + .../Hub/HubAuthenticationViewModel.swift | 3 +- .../Hub/HubDeviceRegistrationView.swift | 20 +++- .../SwiftUI/SwiftUI+CustomKeyboard.swift | 107 ++++++++++++++++++ 5 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+CustomKeyboard.swift diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index 23e7e2842..bea40fdf4 100644 --- a/CryptomatorCommon/Package.swift +++ b/CryptomatorCommon/Package.swift @@ -45,7 +45,8 @@ let package = Package( .product(name: "CocoaLumberjackSwift", package: "CocoaLumberjack"), .product(name: "CryptomatorCloudAccessCore", package: "cloud-access-swift"), .product(name: "Dependencies", package: "simple-swift-dependencies"), - .product(name: "Introspect", package: "SwiftUI-Introspect") + .product(name: "Introspect", package: "SwiftUI-Introspect"), + .product(name: "SwiftUIIntrospect", package: "SwiftUI-Introspect") ] ), .testTarget( diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift index adc7c43ff..77e3c2815 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift @@ -16,6 +16,7 @@ public struct HubAuthenticationView: View { case .deviceRegistration: HubDeviceRegistrationView( deviceName: $viewModel.deviceName, + accountKey: $viewModel.setupCode, onRegisterTap: { Task { await viewModel.register() }} ) case .accessNotGranted: diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index bd8cf072e..5d0a6bdab 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -44,6 +44,7 @@ public final class HubAuthenticationViewModel: ObservableObject { @Published var authenticationFlowState: State? @Published public var deviceName: String = UIDevice.current.name + @Published public var setupCode: String = "" private(set) var isLoggedIn = false private let vaultConfig: UnverifiedVaultConfig @@ -71,7 +72,7 @@ public final class HubAuthenticationViewModel: ObservableObject { } do { - try await deviceRegisteringService.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState) + try await deviceRegisteringService.registerDevice(withName: deviceName, hubConfig: hubConfig, authState: authState, setupCode: setupCode) } catch { await setStateToErrorState(with: error) return diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift index 67260b4ed..5da7c5d0b 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift @@ -2,12 +2,14 @@ import SwiftUI struct HubDeviceRegistrationView: View { @Binding var deviceName: String + @Binding var accountKey: String var onRegisterTap: () -> Void @FocusStateLegacy private var field: Field? = .deviceName private enum Field: CaseIterable { case deviceName + case accountKey } var body: some View { @@ -16,12 +18,26 @@ struct HubDeviceRegistrationView: View { TextField( LocalizedString.getValue("hubAuthentication.deviceRegistration.deviceName.cells.name"), text: $deviceName, - onCommit: onRegisterTap + onCommit: { field = .accountKey } ) .focusedLegacy($field, equals: .deviceName) + .backportedSubmitlabel(.next) } footer: { Text(LocalizedString.getValue("hubAuthentication.deviceRegistration.deviceName.footer.title")) } + + Section { + TextField( + "Account Key", + text: $accountKey, + onCommit: onRegisterTap + ) + .focusedLegacy($field, equals: .accountKey) + .backportedSubmitlabel(.done) + } footer: { + // TODO: Add localization + Text("Your Account Key is required to login from other apps or browsers. It can be found in your profile.") + } } .setListBackgroundColor(.cryptomatorBackground) .toolbar { @@ -36,6 +52,6 @@ struct HubDeviceRegistrationView: View { struct HubDeviceRegistrationView_Previews: PreviewProvider { static var previews: some View { - HubDeviceRegistrationView(deviceName: .constant(""), onRegisterTap: {}) + HubDeviceRegistrationView(deviceName: .constant(""), accountKey: .constant(""), onRegisterTap: {}) } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+CustomKeyboard.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+CustomKeyboard.swift new file mode 100644 index 000000000..3d4425e91 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/SwiftUI/SwiftUI+CustomKeyboard.swift @@ -0,0 +1,107 @@ +// +// SwiftUI+CustomKeyboard.swift +// +// +// Created by Philipp Schmid on 23.12.23. +// + +import SwiftUI +import SwiftUIIntrospect + +public extension View { + func backportedSubmitlabel(_ submitLabel: BackportedSubmitLabel) -> some View { + modifier(BackportedSubmitLabelModifier(label: submitLabel)) + } +} + +public enum BackportedSubmitLabel { + /// Defines a submit label with text of "Done". + case done + + /// Defines a submit label with text of "Go". + case go + + /// Defines a submit label with text of "Send". + case send + + /// Defines a submit label with text of "Join". + case join + + /// Defines a submit label with text of "Route". + case route + + /// Defines a submit label with text of "Search". + case search + + /// Defines a submit label with text of "Return". + case `return` + + /// Defines a submit label with text of "Next". + case next + + /// Defines a submit label with text of "Continue". + case `continue` + + @available(iOS 15, *) + var submitLabel: SubmitLabel { + switch self { + case .done: + return .done + case .go: + return .go + case .send: + return .send + case .join: + return .join + case .route: + return .route + case .search: + return .search + case .return: + return .return + case .next: + return .next + case .continue: + return .continue + } + } + + var returnKeyType: UIReturnKeyType { + switch self { + case .done: + return .done + case .go: + return .go + case .send: + return .send + case .join: + return .join + case .route: + return .route + case .search: + return .search + case .return: + return .default + case .next: + return .next + case .continue: + return .continue + } + } +} + +struct BackportedSubmitLabelModifier: ViewModifier { + let label: BackportedSubmitLabel + + public func body(content: Content) -> some View { + if #available(iOS 15, *) { + content + .submitLabel(label.submitLabel) + } else { + content + .introspect(.textField, on: .iOS(.v13, .v14), scope: .ancestor) { textField in + textField.returnKeyType = label.returnKeyType + } + } + } +} From d3b12a4477dd318a689ac12195d8df39c6d1cd2f Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Mon, 25 Dec 2023 16:48:10 +0100 Subject: [PATCH 67/92] New unlock flow completed - support PKCS#8 key - encrypt vault key with user key --- CryptomatorCommon/Package.swift | 6 +- .../Hub/CryptomatorHubAuthenticator.swift | 15 ++++- .../Hub/HubAuthenticationViewModel.swift | 21 ++++--- .../HubXPCVaultUnlockHandler.swift | 2 +- .../CryptomatorCommonCore/JWEHelper.swift | 62 ++++++++++++++----- .../Manager/VaultDBManager.swift | 2 +- 6 files changed, 77 insertions(+), 31 deletions(-) diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index bea40fdf4..e4a11411a 100644 --- a/CryptomatorCommon/Package.swift +++ b/CryptomatorCommon/Package.swift @@ -29,7 +29,8 @@ let package = Package( .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/PhilLibs/simple-swift-dependencies", .upToNextMajor(from: "0.1.0")), - .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", .upToNextMajor(from: "0.3.0")) + .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", .upToNextMajor(from: "0.3.0")), + .package(url: "https://github.com/leif-ibsen/SwiftECC", from: "5.0.0") ], targets: [ .target( @@ -46,7 +47,8 @@ let package = Package( .product(name: "CryptomatorCloudAccessCore", package: "cloud-access-swift"), .product(name: "Dependencies", package: "simple-swift-dependencies"), .product(name: "Introspect", package: "SwiftUI-Introspect"), - .product(name: "SwiftUIIntrospect", package: "SwiftUI-Introspect") + .product(name: "SwiftUIIntrospect", package: "SwiftUI-Introspect"), + .product(name: "SwiftECC", package: "SwiftECC") ] ), .testTarget( diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 699484e6f..7d5b0bd7c 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -9,18 +9,25 @@ import AppAuthCore import CryptoKit import CryptomatorCloudAccessCore +import CryptomatorCryptoLib import Dependencies import Foundation import JOSESwift public enum HubAuthenticationFlow { - case success(encryptedVaultKey: String, encryptedUserKey: String) + case success(HubAuthenticationFlowSuccess) case accessNotGranted case needsDeviceRegistration case licenseExceeded case requiresAccountInitialization(at: URL) } +public struct HubAuthenticationFlowSuccess { + public let encryptedUserKey: JWE + public let encryptedVaultKey: JWE + public let header: [AnyHashable: Any] +} + public enum CryptomatorHubAuthenticatorError: Error { case unexpectedError case unexpectedResponse @@ -53,7 +60,6 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving guard try await hubInstanceHasMinimumAPILevel(of: Self.minimumHubVersion, apiBaseURL: apiBaseURL, authState: authState) else { throw CryptomatorHubAuthenticatorError.incompatibleHubVersion } -// let deviceID = try getDeviceID() let retrieveMasterkeyResponse = try await getVaultMasterKey(vaultBaseURL: vaultBaseURL, authState: authState, @@ -83,7 +89,10 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return .needsDeviceRegistration } - return .success(encryptedVaultKey: encryptedVaultKey, encryptedUserKey: encryptedUserKey) + let encryptedUserKeyJWE = try JWE(compactSerialization: encryptedUserKey) + let encryptedVaultKeyJWE = try JWE(compactSerialization: encryptedVaultKey) + + return .success(.init(encryptedUserKey: encryptedUserKeyJWE, encryptedVaultKey: encryptedVaultKeyJWE, header: [:])) } public func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String) async throws { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index 5d0a6bdab..88dac11a9 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 CryptomatorCryptoLib import Dependencies import Foundation import JOSESwift @@ -97,8 +98,8 @@ public final class HubAuthenticationViewModel: ObservableObject { await delegate?.hubAuthenticationViewModelWantsToHideLoadingIndicator() switch authFlow { - case let .success(data, header): - await receivedExistingKey(data: data, header: header) + case let .success(response): + await receivedExistingKey(response) case .accessNotGranted: await setState(to: .accessNotGranted) case .needsDeviceRegistration: @@ -110,20 +111,20 @@ public final class HubAuthenticationViewModel: ObservableObject { } } - private func receivedExistingKey(data: Data, header: [AnyHashable: Any]) async { - let privateKey: P384.KeyAgreement.PrivateKey - let jwe: JWE + private func receivedExistingKey(_ flowResponse: HubAuthenticationFlowSuccess) async { let subscriptionState: HubSubscriptionState + let userKey: P384.KeyAgreement.PrivateKey do { - privateKey = try cryptomatorHubKeyProvider.getPrivateKey() - jwe = try JWE(compactSerialization: data) - subscriptionState = try getSubscriptionState(from: header) + let deviceKey = try cryptomatorHubKeyProvider.getPrivateKey() + userKey = try JWEHelper.decryptUserKey(jwe: flowResponse.encryptedUserKey, privateKey: deviceKey) + subscriptionState = .active // try getSubscriptionState(from: flowResponse.header) // TODO: Revert this after Cryptomator Hub adds the subscription state back to the header } catch { await setStateToErrorState(with: error) return } - let response = HubUnlockResponse(jwe: jwe, - privateKey: privateKey, + + let response = HubUnlockResponse(jwe: flowResponse.encryptedVaultKey, + privateKey: userKey, subscriptionState: subscriptionState) await MainActor.run { isLoggedIn = true } await unlockHandler.didSuccessfullyRemoteUnlock(response) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubXPCVaultUnlockHandler.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubXPCVaultUnlockHandler.swift index 4e78362c7..d52575fdc 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubXPCVaultUnlockHandler.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/UnlockHandler/HubXPCVaultUnlockHandler.swift @@ -19,7 +19,7 @@ struct HubXPCVaultUnlockHandler: HubVaultUnlockHandler { func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async { let masterkey: Masterkey do { - masterkey = try JWEHelper.decrypt(jwe: response.jwe, with: response.privateKey) + masterkey = try JWEHelper.decryptVaultKey(jwe: response.jwe, with: response.privateKey) } catch { await delegate?.failedToProcessUnlockedVault(error: error) return diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift index e7a6775ba..6476e482a 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift @@ -9,9 +9,10 @@ import CryptoKit import CryptomatorCryptoLib import Foundation import JOSESwift +import SwiftECC public enum JWEHelper { - public static func decrypt(jwe: JWE, with privateKey: P384.KeyAgreement.PrivateKey) throws -> Masterkey { + public static func decryptVaultKey(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] @@ -31,22 +32,36 @@ public enum JWEHelper { return Masterkey.createFromRaw(rawKey: [UInt8](masterkeyData)) } - public static func decryptUserKey(jwe: JWE, setupCode: String) throws -> String { - guard let decrypter = Decrypter(keyManagementAlgorithm: .PBES2_HS512_A256KW, contentEncryptionAlgorithm: .A256GCM, decryptionKey: setupCode) else { + public static func decryptUserKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) throws -> P384.KeyAgreement.PrivateKey { + 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 { // TODO: Change Error throw VaultManagerError.invalidDecrypter } let payload = try jwe.decrypt(using: decrypter) - let payloadData = payload.data() - guard let jsonObject = try JSONSerialization.jsonObject(with: payloadData, options: []) as? [String: Any], - let key = jsonObject["key"] as? String else { + return try decodeUserKey(payload: payload) + } + + public static func decryptUserKey(jwe: JWE, setupCode: String) throws -> P384.KeyAgreement.PrivateKey { + guard let decrypter = Decrypter(keyManagementAlgorithm: .PBES2_HS512_A256KW, + contentEncryptionAlgorithm: .A256GCM, + decryptionKey: setupCode) else { // TODO: Change Error - throw VaultManagerError.invalidPayloadMasterkey + throw VaultManagerError.invalidDecrypter } - return key + let payload = try jwe.decrypt(using: decrypter) + return try decodeUserKey(payload: payload) } - public static func encryptUserKey(userKey: String, deviceKey: P384.KeyAgreement.PublicKey) throws -> JWE { + public static func encryptUserKey(userKey: P384.KeyAgreement.PrivateKey, deviceKey: P384.KeyAgreement.PublicKey) throws -> JWE { let header = JWEHeader(keyManagementAlgorithm: .ECDH_ES, contentEncryptionAlgorithm: .A256GCM) let x = deviceKey.x963Representation[1 ..< 49] let y = deviceKey.x963Representation[49 ..< 97] @@ -59,11 +74,30 @@ public enum JWEHelper { // TODO: Change Error throw VaultManagerError.invalidDecrypter } - guard let userKey = userKey.data(using: .utf8) else { - // TODO: Change Error - throw VaultManagerError.invalidDecrypter - } - let payload = Payload(userKey) + let payloadKey = try PayloadMasterkey(key: userKey.derPkcs8().base64EncodedString()) + let payload = try Payload(JSONEncoder().encode(payloadKey)) return try JWE(header: header, payload: payload, encrypter: encrypter) } + + private static func decodeUserKey(payload: Payload) throws -> P384.KeyAgreement.PrivateKey { + let decodedPayload = try JSONDecoder().decode(PayloadMasterkey.self, from: payload.data()) + + guard let privateKeyData = Data(base64Encoded: decodedPayload.key) else { + // TODO: Change + fatalError() + } + return try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: privateKeyData) + } +} + +public extension P384.KeyAgreement.PrivateKey { + init(pkcs8DerRepresentation: Data) throws { + let privateKey = try ECPrivateKey(der: Array(pkcs8DerRepresentation), pkcs8: true) + try self.init(pemRepresentation: privateKey.pem) + } + + func derPkcs8() throws -> Data { + let privateKey = try ECPrivateKey(pem: pemRepresentation) + return Data(privateKey.derPkcs8) + } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift index 2b146f7e9..85ca0bbf5 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift @@ -312,7 +312,7 @@ public class VaultDBManager: VaultManager { let masterkey: Masterkey do { let jwe = try JWE(compactSerialization: jweData) - masterkey = try JWEHelper.decrypt(jwe: jwe, with: vault.privateKey) + masterkey = try JWEHelper.decryptVaultKey(jwe: jwe, with: vault.privateKey) } catch { return Promise(error) } From fcdc134b322a4a97a901ec243f9c7b2ed1c1ce7e Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Tue, 26 Dec 2023 00:39:52 +0100 Subject: [PATCH 68/92] Add decrypt user key PBES2 tests --- .../Hub/JWEHelperTests.swift | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift new file mode 100644 index 000000000..bb2c7978d --- /dev/null +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift @@ -0,0 +1,117 @@ +// +// JWEHelperTests.swift +// CryptomatorCommonCoreTests +// +// Created by Philipp Schmid on 25.12.23. +// Copyright © 2023 Skymatic GmbH. All rights reserved. + +import CryptomatorCommonCore +import JOSESwift +import SwiftECC +import XCTest + +final class JWEHelperTests: XCTestCase { + // key pairs from frontend tests (crypto.spec.ts): + private let userPrivKey = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDCi4K1Ts3DgTz/ufkLX7EGMHjGpJv+WJmFgyzLwwaDFSfLpDw0Kgf3FKK+LAsV8r+hZANiAARLOtFebIjxVYUmDV09Q1sVxz2Nm+NkR8fu6UojVSRcCW13tEZatx8XGrIY9zC7oBCEdRqDc68PMSvS5RA0Pg9cdBNc/kgMZ1iEmEv5YsqOcaNADDSs0bLlXb35pX7Kx5Y=" + private let devicePrivKey = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB2bmFCWy2p+EbAn8NWS5Om+GA7c5LHhRZb8g2pSMSf0fsd7k7dZDVrnyHFiLdd/YGhZANiAAR6bsjTEdXKWIuu1Bvj6Y8wySlIROy7YpmVZTY128ItovCD8pcR4PnFljvAIb2MshCdr1alX4g6cgDOqcTeREiObcSfucOU9Ry1pJ/GnX6KA0eSljrk6rxjSDos8aiZ6Mg=" + + // used for JWE generation in frontend: (jwe.spec.ts): + private let privKey = "ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ" + + override func setUpWithError() throws {} + + func testDecryptUserKeyECDHES() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\ + 0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\ + NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\ + jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\ + L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\ + TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\ + PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\ + QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\ + q2koh2NbQ + """) + + let data = Data(base64Encoded: devicePrivKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + let userKey = try JWEHelper.decryptUserKey(jwe: jwe, privateKey: privateKey) + + let x = userKey.x963Representation[1 ..< 49] + let y = userKey.x963Representation[49 ..< 97] + let k = userKey.x963Representation[97 ..< 145] + + /// PKSCS #8: MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDCi4K1Ts3DgTz/ufkLX7EGMHjGpJv+WJmFgyzLwwaDFSfLpDw0Kgf3FKK+LAsV8r+hZANiAARLOtFebIjxVYUmDV09Q1sVxz2Nm+NkR8fu6UojVSRcCW13tEZatx8XGrIY9zC7oBCEdRqDc68PMSvS5RA0Pg9cdBNc/kgMZ1iEmEv5YsqOcaNADDSs0bLlXb35pX7Kx5Y= + /// see: (crypto.spec.ts) in the Hub Frontend + XCTAssertEqual(x.base64URLEncodedString(), "SzrRXmyI8VWFJg1dPUNbFcc9jZvjZEfH7ulKI1UkXAltd7RGWrcfFxqyGPcwu6AQ") + XCTAssertEqual(y.base64URLEncodedString(), "hHUag3OvDzEr0uUQND4PXHQTXP5IDGdYhJhL-WLKjnGjQAw0rNGy5V29-aV-yseW") + XCTAssertEqual(k.base64URLEncodedString(), "wouCtU7Nw4E8_7n5C1-xBjB4xqSb_liZhYMsy8MGgxUny6Q8NCoH9xSiviwLFfK_") + } + + func testDecryptUserKeyECDHESWrongKey() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\ + 0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\ + NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\ + jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\ + L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\ + TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\ + PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\ + QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\ + q2koh2NbQ + """) + + let data = Data(base64Encoded: userPrivKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptUserKey(jwe: jwe, privateKey: privateKey)) { error in + guard case JOSESwiftError.decryptingFailed = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } + + func testDecryptUserKeyPBES2() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\ + xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\ + aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\ + P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\ + mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\ + kDjUaxwUKqpvT7qaAQ + """) + + let userKey = try JWEHelper.decryptUserKey(jwe: jwe, setupCode: "123456") + + let x = userKey.x963Representation[1 ..< 49] + let y = userKey.x963Representation[49 ..< 97] + let k = userKey.x963Representation[97 ..< 145] + + /// PKSCS #8: ME8CAQAwEAYHKoZIzj0CAQYFK4EEACIEODA2AgEBBDEA6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ + /// see: (jwe.spec.ts) in the Hub Frontend + XCTAssertEqual(x.base64URLEncodedString(), "RxQR-NRN6Wga01370uBBzr2NHDbKIC56tPUEq2HX64RhITGhii8Zzbkb1HnRmdF0") + XCTAssertEqual(y.base64URLEncodedString(), "aq6uqmUy4jUhuxnKxsv59A6JeK7Unn-mpmm3pQAygjoGc9wrvoH4HWJSQYUlsXDu") + XCTAssertEqual(k.base64URLEncodedString(), "6QybmBitf94veD5aCLr7nlkF5EZpaXHCfq1AXm57AKQyGOjTDAF9EQB28fMywTDQ") + } + + func testDecryptUserKeyPBES2WrongKey() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJzIjoiT3hMY0Q\ + xX1pCODc1c2hvUWY2Q1ZHQSIsInAyYyI6MTAwMCwiYXB1IjoiIiwiYXB2IjoiIn0.FD4fcrP4Pb\ + aKOQ9ZfXl0gpMM6Fa2rfqAvL0K5ZyYUiVeHCNV-A02Rg.urT1ShSv6qQxh8X7.gEqAiUWD98a2E\ + P7ITCPTw4DJo6-BpqrxA73D6gNIj9z4d1hN-EP99Q4mWBWLH97H8ugbG5rGsm8xsjsBqpWORQqF\ + mJZR2AhlPiwFaC7n_MDDBupSy_swDnCfj731Lal297IP5WbkFcmozKsyhmwdkctxjf_VHA.fJki\ + kDjUaxwUKqpvT7qaAQ + """) + + XCTAssertThrowsError(try JWEHelper.decryptUserKey(jwe: jwe, setupCode: "654321")) { error in + guard case JOSESwiftError.decryptingFailed = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } +} From dbfc30ac624119ea9baf8f35d42b09e6b31d34ea Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 27 Dec 2023 12:50:37 +0100 Subject: [PATCH 69/92] Add test to decrypt vault key --- .../Hub/JWEHelperTests.swift | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift index bb2c7978d..684075880 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift @@ -5,10 +5,12 @@ // Created by Philipp Schmid on 25.12.23. // Copyright © 2023 Skymatic GmbH. All rights reserved. +import CryptoKit import CryptomatorCommonCore import JOSESwift import SwiftECC import XCTest +@testable import CryptomatorCryptoLib final class JWEHelperTests: XCTestCase { // key pairs from frontend tests (crypto.spec.ts): @@ -114,4 +116,42 @@ final class JWEHelperTests: XCTestCase { } } } + + func testDecryptVaultKey() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlA\ + tMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IllUcEY3bGtTc3JvZVVUVFdCb21LNzBTN0\ + FhVTJyc0ptMURpZ1ZzbjRMY2F5eUxFNFBabldkYmFVcE9jQVV5a1ciLCJ5IjoiLU5pS3loUktjSk52N\ + m02Z0ZJUWc4cy1Xd1VXUW9uT3A5dkQ4cHpoa2tUU3U2RzFlU2FUTVlhZGltQ2Q4V0ExMSJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..BECWGzd9UvhHcTJC.znt4TlS-qiNEjxiu2v-du_E1QOBnyBR6LCt865SHxD\ + -kwRc1JwX_Lq9XVoFj2GnK9-9CgxhCLGurg5Jt9g38qv2brGAzWL7eSVeY1fIqdO_kUhLpGslRTN6h2\ + U0NHJi2-iE.WDVI2kOk9Dy3PWHyIg8gKA + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + let masterkey = try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey) + + let expectedEncKey = [UInt8](repeating: 0x55, count: 32) + let expectedMacKey = [UInt8](repeating: 0x77, count: 32) + + XCTAssertEqual(masterkey.aesMasterKey, expectedEncKey) + XCTAssertEqual(masterkey.macMasterKey, expectedMacKey) + } + + func testDecryptInvalidVaultKey_wrongKey() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImdodGR3VnNoUU8wRGFBdjVBOXBiZ1NCTW0yYzZKWVF4dkloR3p6RVdQTncxczZZcEFYeTRQTjBXRFJUWExtQ2wiLCJ5IjoiN3Rncm1Gd016NGl0ZmVQNzBndkpLcjRSaGdjdENCMEJHZjZjWE9WZ2M0bjVXMWQ4dFgxZ1RQakdrczNVSm1zUiJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..x6JWRGSojUJUJYpp.5BRuzcaV.lLIhGH7Wz0n_iTBAubDFZA + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey)) { error in + guard case JOSESwiftError.decryptingFailed = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } } From d71b6a8f33f7797eb6739c4d9ce79c08ef83ea7c Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 27 Dec 2023 14:03:01 +0100 Subject: [PATCH 70/92] Add tests for decrypting vault key --- .../Hub/JWEHelperTests.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift index 684075880..9c958a524 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift @@ -154,4 +154,68 @@ final class JWEHelperTests: XCTestCase { } } } + + func testDecryptInvalidVaultKey_payloadIsNotJSON() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkM2bWhsNE5BTHhEdHMwUlFlNXlyZWxQVDQyOGhDVzJNeUNYS3EwdUI0TDFMdnpXRHhVaVk3YTdZcEhJakJXcVoiLCJ5IjoiakM2dWc1NE9tbmdpNE9jUk1hdkNrczJpcFpXQjdkUmotR3QzOFhPSDRwZ2tpQ0lybWNlUnFxTnU3Z0c3Qk1yOSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..HNJJghL-SvERFz2v.N0z8YwFg.rYw29iX4i8XujdM4P4KKWg + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey)) { error in + guard case DecodingError.dataCorrupted = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } + + func testDecryptInvalidVaultKey_jsonDoesNotContainKey() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6InB3R05vcXRnY093MkJ6RDVmSnpBWDJvMzUwSWNsY3A5cFdVTHZ5VDRqRWVCRWdCc3hhTVJXQ1ZyNlJMVUVXVlMiLCJ5IjoiZ2lIVEE5MlF3VU5lbmg1OFV1bWFfb09BX3hnYmFDVWFXSlRnb3Z4WjU4R212TnN4eUlQRElLSm9WV1h5X0R6OSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..jDbzdI7d67_cUjGD.01BPnMq_tQ.aG_uFA6FYqoPS64QAJ4VBQ + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey)) { error in + guard case DecodingError.keyNotFound = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } + + func testDecryptInvalidVaultKey_jsonKeyIsNotAString() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6IkJyYm9UQkl5Y0NDUEdJQlBUekU2RjBnbTRzRjRCamZPN1I0a2x0aWlCaThKZkxxcVdXNVdUSVBLN01yMXV5QVUiLCJ5IjoiNUpGVUI0WVJiYjM2RUZpN2Y0TUxMcFFyZXd2UV9Tc3dKNHRVbFd1a2c1ZU04X1ZyM2pkeml2QXI2WThRczVYbSJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..QEq4Z2m6iwBx2ioS.IBo8TbKJTS4pug.61Z-agIIXgP8bX10O_yEMA + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey)) { error in + guard case DecodingError.typeMismatch = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } + + func testDecryptInvalidVaultKey_invalidBase64Data() throws { + let jwe = try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImNZdlVFZm9LYkJjenZySE5zQjUxOGpycUxPMGJDOW5lZjR4NzFFMUQ5dk95MXRqd1piZzV3cFI0OE5nU1RQdHgiLCJ5IjoiaWRJekhCWERzSzR2NTZEeU9yczJOcDZsSG1zb29fMXV0VTlzX3JNdVVkbkxuVXIzUXdLZkhYMWdaVXREM1RKayJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..0VZqu5ei9U3blGtq.eDvhU6drw7mIwvXu6Q.f05QnhI7JWG3IYHvexwdFQ + """) + + let data = Data(base64Encoded: privKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + + XCTAssertThrowsError(try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey)) { error in + guard case VaultManagerError.invalidPayloadMasterkey = error else { + XCTFail("Unexpected error: \(error)") + return + } + } + } } From 9476edbd0792134eb2750212e192b8a8f15865f0 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 27 Dec 2023 16:21:51 +0100 Subject: [PATCH 71/92] Refined errors --- .../Hub/CryptomatorHubAuthenticator.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 7d5b0bd7c..35ebc286a 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -33,6 +33,9 @@ public enum CryptomatorHubAuthenticatorError: Error { case unexpectedResponse case deviceNameAlreadyExists + case unexpectedPrivateKeyFormat + case invalidVaultConfig + case invalidHubConfig case invalidBaseURL case invalidDeviceResourceURL case missingAccessToken @@ -48,13 +51,11 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving public func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { guard let hubConfig = vaultConfig.allegedHubConfig, let vaultBaseURL = getVaultBaseURL(from: vaultConfig) else { - // handle error - fatalError() + throw CryptomatorHubAuthenticatorError.invalidVaultConfig } guard let apiBaseURL = hubConfig.getAPIBaseURL(), let webAppURL = hubConfig.getWebAppURL() else { - // TODO: More specific error - throw CryptomatorHubAuthenticatorError.invalidBaseURL + throw CryptomatorHubAuthenticatorError.invalidHubConfig } guard try await hubInstanceHasMinimumAPILevel(of: Self.minimumHubVersion, apiBaseURL: apiBaseURL, authState: authState) else { @@ -97,7 +98,6 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving public func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String) async throws { guard let apiBaseURL = hubConfig.getAPIBaseURL() else { - // TODO: More specific error throw CryptomatorHubAuthenticatorError.invalidBaseURL } @@ -140,8 +140,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving private func getEncryptedUserKeyJWE(userDto: UserDTO, setupCode: String, publicKey: P384.KeyAgreement.PublicKey) throws -> JWE { guard let privateKey = userDto.privateKey.data(using: .utf8) else { - // TODO: Throw proper error - fatalError() + throw CryptomatorHubAuthenticatorError.unexpectedPrivateKeyFormat } let jwe = try JWE(compactSerialization: privateKey) From e797b2ad0da262f32c9fd732dc1dc5d82c08e9c1 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 27 Dec 2023 16:34:51 +0100 Subject: [PATCH 72/92] Added documentation to registerDevice --- .../Hub/CryptomatorHubAuthenticator.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 35ebc286a..88090d620 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -96,7 +96,19 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return .success(.init(encryptedUserKey: encryptedUserKeyJWE, encryptedVaultKey: encryptedVaultKeyJWE, header: [:])) } - public func registerDevice(withName name: String, hubConfig: HubConfig, authState: OIDAuthState, setupCode: String) async throws { + /** Registers a new device. + + Registers a new mobile device at the hub instance derived from the `hubConfig` with the given `name`. + + The device registration consists of two requests: + + 1. Request the encrypted user key which can be decrypted by using the `setupCode`. + 2. Send a Create Device request to the hub instance which contains the user key encrypted with the device key pair + */ + public func registerDevice(withName name: String, + hubConfig: HubConfig, + authState: OIDAuthState, + setupCode: String) async throws { guard let apiBaseURL = hubConfig.getAPIBaseURL() else { throw CryptomatorHubAuthenticatorError.invalidBaseURL } From e523906d9dfbe26ce920e682251b014d8a97f333 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Thu, 28 Dec 2023 02:12:58 +0100 Subject: [PATCH 73/92] Update HubAuthenticationViewModelTests --- .../Hub/HubAuthenticationViewModelTests.swift | 73 ++++++++++++++++--- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift index 7f0266703..56fcbd80e 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift @@ -7,6 +7,7 @@ import AppAuthCore import CryptoKit +import JOSESwift import XCTest @testable import CryptomatorCloudAccessCore @testable import CryptomatorCommonCore @@ -45,7 +46,7 @@ final class HubAuthenticationViewModelTests: XCTestCase { let calledReceiveKey = XCTestExpectation() hubKeyServiceMock.receiveKeyAuthStateVaultConfigClosure = { _, _ in calledReceiveKey.fulfill() - return .success(Data(), [:]) + return try .successMock() } let calledShowLoadingIndicator = XCTestExpectation() @@ -103,8 +104,12 @@ final class HubAuthenticationViewModelTests: XCTestCase { // 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) + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = try .successMock(header: ["hub-subscription-state": "ACTIVE"]) + + let devicePrivKey = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB2bmFCWy2p+EbAn8NWS5Om+GA7c5LHhRZb8g2pSMSf0fsd7k7dZDVrnyHFiLdd/YGhZANiAAR6bsjTEdXKWIuu1Bvj6Y8wySlIROy7YpmVZTY128ItovCD8pcR4PnFljvAIb2MshCdr1alX4g6cgDOqcTeREiObcSfucOU9Ry1pJ/GnX6KA0eSljrk6rxjSDos8aiZ6Mg=" + let data = Data(base64Encoded: devicePrivKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + hubKeyProviderMock.getPrivateKeyReturnValue = privateKey // WHEN // continue the access check @@ -117,14 +122,15 @@ final class HubAuthenticationViewModelTests: XCTestCase { XCTAssertEqual(receivedResponse?.subscriptionState, .active) } - func testContinueToAccessCheck_success_hubSubscriptionStateIsInactive() async throws { + // TODO: Don't skip test as soon as the hub instance does return a valid header and we no longer hardcode a active hub subscription state! + func skip_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"]) + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = try .successMock(header: ["hub-subscription-state": "INACTIVE"]) hubKeyProviderMock.getPrivateKeyReturnValue = P384.KeyAgreement.PrivateKey(compactRepresentable: false) // WHEN @@ -145,7 +151,7 @@ final class HubAuthenticationViewModelTests: XCTestCase { // GIVEN // the hub key service returns success with an unknown Cryptomator Hub subscription state - hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = .success(validHubResponseData(), ["hub-subscription-state": "FOO"]) + hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = try .successMock(header: ["hub-subscription-state": "foo"]) hubKeyProviderMock.getPrivateKeyReturnValue = P384.KeyAgreement.PrivateKey(compactRepresentable: false) // WHEN @@ -224,8 +230,8 @@ final class HubAuthenticationViewModelTests: XCTestCase { // THEN // the registerDevice got called on the device registering servie - let receivedArguments = deviceRegisteringMock.registerDeviceWithNameHubConfigAuthStateReceivedArguments - XCTAssertEqual(deviceRegisteringMock.registerDeviceWithNameHubConfigAuthStateCallsCount, 1) + let receivedArguments = deviceRegisteringMock.registerDeviceWithNameHubConfigAuthStateSetupCodeReceivedArguments + XCTAssertEqual(deviceRegisteringMock.registerDeviceWithNameHubConfigAuthStateSetupCodeCallsCount, 1) // with the name set by the user XCTAssertEqual(receivedArguments?.name, "My Device 123") } @@ -233,7 +239,7 @@ final class HubAuthenticationViewModelTests: XCTestCase { private struct TestError: Error {} private func validHubVaultConfig() -> Data { - "eyJraWQiOiJodWIraHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMjkvYXBpL3ZhdWx0cy9mYjUzMDdmMC1jOWI4LTRjNWYtYjJiMi03ZDM4ODE4ZjZhNGIiLCJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiIsImh1YiI6eyJjbGllbnRJZCI6ImNyeXB0b21hdG9yIiwiYXV0aEVuZHBvaW50IjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcva2MvcmVhbG1zL2h1YjI5L3Byb3RvY29sL29wZW5pZC1jb25uZWN0L2F1dGgiLCJ0b2tlbkVuZHBvaW50IjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcva2MvcmVhbG1zL2h1YjI5L3Byb3RvY29sL29wZW5pZC1jb25uZWN0L3Rva2VuIiwiZGV2aWNlc1Jlc291cmNlVXJsIjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMjkvYXBpL2RldmljZXMvIiwiYXV0aFN1Y2Nlc3NVcmwiOiJodHRwczovL3Rlc3RpbmcuaHViLmNyeXB0b21hdG9yLm9yZy9odWIyOS9hcHAvdW5sb2NrLXN1Y2Nlc3M_dmF1bHQ9ZmI1MzA3ZjAtYzliOC00YzVmLWIyYjItN2QzODgxOGY2YTRiIiwiYXV0aEVycm9yVXJsIjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMjkvYXBwL3VubG9jay1lcnJvcj92YXVsdD1mYjUzMDdmMC1jOWI4LTRjNWYtYjJiMi03ZDM4ODE4ZjZhNGIifX0.eyJqdGkiOiJmYjUzMDdmMC1jOWI4LTRjNWYtYjJiMi03ZDM4ODE4ZjZhNGIiLCJmb3JtYXQiOjgsImNpcGhlckNvbWJvIjoiU0lWX0dDTSIsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMH0.2iFWE4Jj5lV6iaVTPOzGovnrNreuuAJCy_gPmK90MMU".data(using: .utf8)! + "eyJraWQiOiJodWIraHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMzAvYXBpL3ZhdWx0cy83NWFmMjFiNy00ODQ5LTQ1NTgtYjA1Yy1kZTZkYzkwNzdhNjciLCJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiIsImh1YiI6eyJjbGllbnRJZCI6ImNyeXB0b21hdG9yIiwiYXV0aEVuZHBvaW50IjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcva2MvcmVhbG1zL2h1YjMwL3Byb3RvY29sL29wZW5pZC1jb25uZWN0L2F1dGgiLCJ0b2tlbkVuZHBvaW50IjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcva2MvcmVhbG1zL2h1YjMwL3Byb3RvY29sL29wZW5pZC1jb25uZWN0L3Rva2VuIiwiYXV0aFN1Y2Nlc3NVcmwiOiJodHRwczovL3Rlc3RpbmcuaHViLmNyeXB0b21hdG9yLm9yZy9odWIzMC9hcHAvdW5sb2NrLXN1Y2Nlc3M_dmF1bHQ9NzVhZjIxYjctNDg0OS00NTU4LWIwNWMtZGU2ZGM5MDc3YTY3IiwiYXV0aEVycm9yVXJsIjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMzAvYXBwL3VubG9jay1lcnJvcj92YXVsdD03NWFmMjFiNy00ODQ5LTQ1NTgtYjA1Yy1kZTZkYzkwNzdhNjciLCJhcGlCYXNlVXJsIjoiaHR0cHM6Ly90ZXN0aW5nLmh1Yi5jcnlwdG9tYXRvci5vcmcvaHViMzAvYXBpLyIsImRldmljZXNSZXNvdXJjZVVybCI6Imh0dHBzOi8vdGVzdGluZy5odWIuY3J5cHRvbWF0b3Iub3JnL2h1YjMwL2FwaS9kZXZpY2VzLyJ9fQ.eyJqdGkiOiI3NWFmMjFiNy00ODQ5LTQ1NTgtYjA1Yy1kZTZkYzkwNzdhNjciLCJmb3JtYXQiOjgsImNpcGhlckNvbWJvIjoiU0lWX0dDTSIsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMH0.Z0x_5D073zo3smZq5q5wgDRheewcapCrIqg_0iD5qwM".data(using: .utf8)! } private func validHubResponseData() -> Data { @@ -258,6 +264,37 @@ private extension HubAuthenticationViewModel.State { } } +private extension JWE { + static func encryptedUserKeyStub() throws -> JWE { + try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrZXlfb3BzIjpbXSwiZXh0Ijp\ + 0cnVlLCJrdHkiOiJFQyIsIngiOiJoeHpiSWh6SUJza3A5ZkZFUmJSQ2RfOU1fbWYxNElqaDZhcnNoVX\ + NkcEEyWno5ejZZNUs4NHpZR2I4b2FHemNUIiwieSI6ImJrMGRaNWhpelZ0TF9hN2hNejBjTUduNjhIR\ + jZFdWlyNHdlclNkTFV5QWd2NWUzVzNYSG5sdHJ2VlRyU3pzUWYiLCJjcnYiOiJQLTM4NCJ9LCJhcHUi\ + OiIiLCJhcHYiOiIifQ..pu3Q1nR_yvgRAapG.4zW0xm0JPxbcvZ66R-Mn3k841lHelDQfaUvsZZAtWs\ + L2w4FMi6H_uu6ArAWYLtNREa_zfcPuyuJsFferYPSNRUWt4OW6aWs-l_wfo7G1ceEVxztQXzQiwD30U\ + TA8OOdPcUuFfEq2-d9217jezrcyO6m6FjyssEZIrnRArUPWKzGdghXccGkkf0LTZcGJoHeKal-RtyP8\ + PfvEAWTjSOCpBlSdUJ-1JL3tyd97uVFNaVuH3i7vvcMoUP_bdr0XW3rvRgaeC6X4daPLUvR1hK5Msut\ + QMtM2vpFghS_zZxIQRqz3B2ECxa9Bjxhmn8kLX5heZ8fq3lH-bmJp1DxzZ4V1RkWk.yVwXG9yARa5Ih\ + q2koh2NbQ + """) + } + + static func encryptedVaultKeyStub() throws -> JWE { + try JWE(compactSerialization: """ + eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImVwayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMzg0Iiwia2V5X29wcyI6W10sImV4dCI6dHJ1ZSwieCI6ImNZdlVFZm9LYkJjenZySE5zQjUxOGpycUxPMGJDOW5lZjR4NzFFMUQ5dk95MXRqd1piZzV3cFI0OE5nU1RQdHgiLCJ5IjoiaWRJekhCWERzSzR2NTZEeU9yczJOcDZsSG1zb29fMXV0VTlzX3JNdVVkbkxuVXIzUXdLZkhYMWdaVXREM1RKayJ9LCJhcHUiOiIiLCJhcHYiOiIifQ..0VZqu5ei9U3blGtq.eDvhU6drw7mIwvXu6Q.f05QnhI7JWG3IYHvexwdFQ + """) + } +} + +private extension HubAuthenticationFlow { + static func successMock(header: [AnyHashable: Any] = [:]) throws -> HubAuthenticationFlow { + try .success(.init(encryptedUserKey: .encryptedUserKeyStub(), + encryptedVaultKey: .encryptedVaultKeyStub(), + header: header)) + } +} + // MARK: - HubAuthenticationViewModelDelegateMock - // swiftlint: disable all @@ -289,6 +326,24 @@ final class HubAuthenticationViewModelDelegateMock: HubAuthenticationViewModelDe hubAuthenticationViewModelWantsToHideLoadingIndicatorCallsCount += 1 hubAuthenticationViewModelWantsToHideLoadingIndicatorClosure?() } + + // MARK: - hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert + + var hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLCallsCount = 0 + var hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLCalled: Bool { + hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLCallsCount > 0 + } + + var hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLReceivedProfileURL: URL? + var hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLReceivedInvocations: [URL] = [] + var hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLClosure: ((URL) -> Void)? + + func hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert(profileURL: URL) { + hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLCallsCount += 1 + hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLReceivedProfileURL = profileURL + hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLReceivedInvocations.append(profileURL) + hubAuthenticationViewModelWantsToShowNeedsAccountInitAlertProfileURLClosure?(profileURL) + } } // swiftlint: enable all From 7b98ba37b7f5dfc3b75a8416dbe0e08529fabfb1 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Thu, 28 Dec 2023 02:32:51 +0100 Subject: [PATCH 74/92] Add handling for legacy hub --- .../Hub/CryptomatorHubAuthenticator.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 88090d620..bba103495 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -202,6 +202,10 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return digest.data.base16EncodedString } + /** Checks if the Cryptomator Hub Instance at `apiBaseURL` has at least the API level of `minimumLevel`. + + - Note: The legacy Hub which is not supported returns a 0 + */ private func hubInstanceHasMinimumAPILevel(of minimumLevel: Int, apiBaseURL: URL, authState: OIDAuthState) async throws -> Bool { let url = apiBaseURL.appendingPathComponent("config") let (accessToken, _) = try await authState.performAction() @@ -355,7 +359,13 @@ extension String { extension HubConfig { func getAPIBaseURL() -> URL? { - return URL(string: apiBaseUrl) + if let apiBaseUrl { + return URL(string: apiBaseUrl) + } + guard let deviceResourceURL = URL(string: devicesResourceUrl) else { + return nil + } + return deviceResourceURL.deletingLastPathComponent() } func getWebAppURL() -> URL? { From f0bb27a2cb4eec1ae718f5dcccbbf6602f2fe68d Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Thu, 28 Dec 2023 12:22:21 +0100 Subject: [PATCH 75/92] Update swift-tools-version from 5.7 to 5.9 needed for SwiftECC --- CryptomatorCommon/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index e4a11411a..0409f44e3 100644 --- a/CryptomatorCommon/Package.swift +++ b/CryptomatorCommon/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.9 // // Package.swift From b1b8e0a6d4a47e997c45dc85b5a86ab92f725e75 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Sat, 30 Dec 2023 13:57:11 +0100 Subject: [PATCH 76/92] Updated localizations --- .../Hub/HubDeviceRegistrationView.swift | 3 +-- SharedResources/en.lproj/Localizable.strings | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift index 5da7c5d0b..92bf6ce50 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegistrationView.swift @@ -35,8 +35,7 @@ struct HubDeviceRegistrationView: View { .focusedLegacy($field, equals: .accountKey) .backportedSubmitlabel(.done) } footer: { - // TODO: Add localization - Text("Your Account Key is required to login from other apps or browsers. It can be found in your profile.") + Text(LocalizedString.getValue("hubAuthentication.deviceRegistration.accountKey.footer.title")) } } .setListBackgroundColor(.cryptomatorBackground) diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 65ea219a7..5a1eb77f1 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -119,11 +119,12 @@ "hubAuthentication.licenseExceeded" = "Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license."; "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.accountKey.footer.title" = "Your Account Key is required to login from new apps or browsers. It can be found in your profile."; "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."; -"hubAuthentication.requireAccountInit.alert.title" = "Action required"; +"hubAuthentication.requireAccountInit.alert.title" = "Action Required"; "hubAuthentication.requireAccountInit.alert.message" = "To proceed, please complete the steps required in your Hub user profile."; -"hubAuthentication.requireAccountInit.alert.actionButton" = "Go to profile"; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Go to Profile"; "intents.saveFile.missingFile" = "The provided file is not valid."; "intents.saveFile.invalidFolder" = "The provided folder is not valid."; From 6f92d49826bb23bb9655b85eb5c67684ec73be2e Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Sat, 30 Dec 2023 15:17:02 +0100 Subject: [PATCH 77/92] Minor refactorings and reformattings --- .../Hub/CryptomatorHubAuthenticator.swift | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index bba103495..0420202f9 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -96,14 +96,15 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return .success(.init(encryptedUserKey: encryptedUserKeyJWE, encryptedVaultKey: encryptedVaultKeyJWE, header: [:])) } - /** Registers a new device. + /** + Registers a new device. - Registers a new mobile device at the hub instance derived from the `hubConfig` with the given `name`. + Registers a new mobile device at the Hub instance derived from the `hubConfig` with the given `name`. - The device registration consists of two requests: + The device registration consists of two requests: - 1. Request the encrypted user key which can be decrypted by using the `setupCode`. - 2. Send a Create Device request to the hub instance which contains the user key encrypted with the device key pair + 1. Request the encrypted user key which can be decrypted by using the `setupCode`. + 2. Send a Create Device request to the Hub instance which contains the user key encrypted with the device key pair. */ public func registerDevice(withName name: String, hubConfig: HubConfig, @@ -134,7 +135,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving try await createDevice(dto, apiBaseURL: apiBaseURL, authState: authState) } - private func getUser(apiBaseURL: URL, authState: OIDAuthState) async throws -> UserDTO { + private func getUser(apiBaseURL: URL, authState: OIDAuthState) async throws -> UserDto { let url = apiBaseURL.appendingPathComponent("users/me") let (accessToken, _) = try await authState.performAction() guard let accessToken = accessToken else { @@ -147,10 +148,10 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving guard httpResponse?.statusCode == 200 else { throw CryptomatorHubAuthenticatorError.unexpectedResponse } - return try JSONDecoder().decode(UserDTO.self, from: data) + return try JSONDecoder().decode(UserDto.self, from: data) } - private func getEncryptedUserKeyJWE(userDto: UserDTO, setupCode: String, publicKey: P384.KeyAgreement.PublicKey) throws -> JWE { + private func getEncryptedUserKeyJWE(userDto: UserDto, setupCode: String, publicKey: P384.KeyAgreement.PublicKey) throws -> JWE { guard let privateKey = userDto.privateKey.data(using: .utf8) else { throw CryptomatorHubAuthenticatorError.unexpectedPrivateKeyFormat } @@ -202,9 +203,10 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return digest.data.base16EncodedString } - /** Checks if the Cryptomator Hub Instance at `apiBaseURL` has at least the API level of `minimumLevel`. + /** + Checks if the Hub instance at `apiBaseURL` has at least the API level of `minimumLevel`. - - Note: The legacy Hub which is not supported returns a 0 + - Note: The legacy Hub which is not supported returns a 0. */ private func hubInstanceHasMinimumAPILevel(of minimumLevel: Int, apiBaseURL: URL, authState: OIDAuthState) async throws -> Bool { let url = apiBaseURL.appendingPathComponent("config") @@ -373,7 +375,7 @@ extension HubConfig { } } -private struct UserDTO: Codable { +private struct UserDto: Codable { let id: String let name: String let publicKey: String From cace28b083109a656560450e08891a5771b14ceb Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sat, 30 Dec 2023 21:29:36 +0100 Subject: [PATCH 78/92] Use Xcode 15.1 --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7225ba82d..e2cfc5839 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,8 @@ jobs: run: | cd fastlane ./scripts/create-cloud-access-secrets.sh + - name: Select Xcode 15.1 + run: sudo xcode-select -s /Applications/Xcode_15.1.app - name: Configuration for freemium if: ${{ matrix.config == 'freemium' }} run: | From 3c98ece2b96e97a7dd5c69d7729981060c673eff Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Sun, 31 Dec 2023 18:33:19 +0100 Subject: [PATCH 79/92] Use latest cloud-access-swift version --- .../xcshareddata/swiftpm/Package.resolved | 52 ++++++++++++++++--- CryptomatorCommon/Package.swift | 2 +- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d69681916..b87dbea32 100644 --- a/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Cryptomator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,6 +9,15 @@ "version" : "1.6.2" } }, + { + "identity" : "asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/leif-ibsen/ASN1", + "state" : { + "revision" : "5bb6eca2e4b250995f189c3d04ec53b6cd8257c5", + "version" : "2.2.0" + } + }, { "identity" : "asn1swift", "kind" : "remoteSourceControl", @@ -23,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/aws-amplify/aws-sdk-ios-spm.git", "state" : { - "revision" : "ca31418963a90bac80538e13f6b7af87ea14d279", - "version" : "2.33.4" + "revision" : "59fdc9ca7ff3f5d38e07af27526a527c199b8de6", + "version" : "2.33.7" } }, { @@ -36,13 +45,22 @@ "version" : "0.9.0" } }, + { + "identity" : "bigint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/leif-ibsen/BigInt", + "state" : { + "revision" : "3fe07ec38afa732e86d4f3e867cb43b05d004941", + "version" : "1.14.0" + } + }, { "identity" : "cloud-access-swift", "kind" : "remoteSourceControl", "location" : "https://github.com/cryptomator/cloud-access-swift.git", "state" : { - "revision" : "1fe06a85f9ea38d9b22a84fb7dbd8de127c65f82", - "version" : "1.8.1" + "revision" : "63fd1cfee9e4d1c0a8d585dd0c7008eb37d2f037", + "version" : "1.9.0" } }, { @@ -63,6 +81,15 @@ "version" : "1.1.0" } }, + { + "identity" : "digest", + "kind" : "remoteSourceControl", + "location" : "https://github.com/leif-ibsen/Digest", + "state" : { + "revision" : "fd501645c5f14c17207c4ada4281a1e6b7cb03df", + "version" : "1.1.0" + } + }, { "identity" : "dropbox-sdk-obj-c-spm", "kind" : "remoteSourceControl", @@ -113,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/tobihagemann/JOSESwift.git", "state" : { - "revision" : "11442e7f1f803ef42281909c68f386b38afc5096", - "version" : "2.4.0-cryptomator" + "revision" : "3544f8117908ef12ea13b1c0927e0e3c0d30ee01", + "version" : "2.4.1-cryptomator" } }, { @@ -122,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc.git", "state" : { - "revision" : "35846731c0971694f162b28fe8494c03b615ae74", - "version" : "1.2.16" + "revision" : "e9ef281b2f281c3ba2d32608138b1431cba5e4df", + "version" : "1.2.20" } }, { @@ -180,6 +207,15 @@ "version" : "1.5.3" } }, + { + "identity" : "swiftecc", + "kind" : "remoteSourceControl", + "location" : "https://github.com/leif-ibsen/SwiftECC", + "state" : { + "revision" : "18c0e462882d0a4fa910472a0a6cc13ef97bbc21", + "version" : "5.0.0" + } + }, { "identity" : "swiftui-introspect", "kind" : "remoteSourceControl", diff --git a/CryptomatorCommon/Package.swift b/CryptomatorCommon/Package.swift index 0409f44e3..ecb713a4b 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.8.0")), + .package(url: "https://github.com/cryptomator/cloud-access-swift.git", .upToNextMinor(from: "1.9.0")), .package(url: "https://github.com/CocoaLumberjack/CocoaLumberjack.git", .upToNextMinor(from: "3.8.0")), .package(url: "https://github.com/PhilLibs/simple-swift-dependencies", .upToNextMajor(from: "0.1.0")), .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", .upToNextMajor(from: "0.3.0")), From 1b6089c773f25446e81a649fc5d8c9b2593718e5 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 3 Jan 2024 11:22:00 +0100 Subject: [PATCH 80/92] Resolve error handling todos --- .../CryptomatorCommonCore/JWEHelper.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift index 6476e482a..213a63676 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift @@ -11,6 +11,11 @@ import Foundation import JOSESwift import SwiftECC +public enum JWEHelperError: Error { + case invalidDecrypter + case invalidMasterkeyPayload +} + public enum JWEHelper { public static func decryptVaultKey(jwe: JWE, with privateKey: P384.KeyAgreement.PrivateKey) throws -> Masterkey { // see https://developer.apple.com/forums/thread/680554 @@ -20,8 +25,7 @@ public enum JWEHelper { 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 { - // TODO: Change Error - throw VaultManagerError.invalidDecrypter + throw JWEHelperError.invalidDecrypter } let payload = try jwe.decrypt(using: decrypter) let payloadMasterkey = try JSONDecoder().decode(PayloadMasterkey.self, from: payload.data()) @@ -43,8 +47,7 @@ public enum JWEHelper { guard let decrypter = Decrypter(keyManagementAlgorithm: .ECDH_ES, contentEncryptionAlgorithm: .A256GCM, decryptionKey: decryptionKey) else { - // TODO: Change Error - throw VaultManagerError.invalidDecrypter + throw JWEHelperError.invalidDecrypter } let payload = try jwe.decrypt(using: decrypter) return try decodeUserKey(payload: payload) @@ -54,8 +57,7 @@ public enum JWEHelper { guard let decrypter = Decrypter(keyManagementAlgorithm: .PBES2_HS512_A256KW, contentEncryptionAlgorithm: .A256GCM, decryptionKey: setupCode) else { - // TODO: Change Error - throw VaultManagerError.invalidDecrypter + throw JWEHelperError.invalidDecrypter } let payload = try jwe.decrypt(using: decrypter) return try decodeUserKey(payload: payload) @@ -71,8 +73,7 @@ public enum JWEHelper { guard let encrypter = Encrypter(keyManagementAlgorithm: .ECDH_ES, contentEncryptionAlgorithm: .A256GCM, encryptionKey: encryptionKey) else { - // TODO: Change Error - throw VaultManagerError.invalidDecrypter + throw JWEHelperError.invalidDecrypter } let payloadKey = try PayloadMasterkey(key: userKey.derPkcs8().base64EncodedString()) let payload = try Payload(JSONEncoder().encode(payloadKey)) @@ -83,8 +84,7 @@ public enum JWEHelper { let decodedPayload = try JSONDecoder().decode(PayloadMasterkey.self, from: payload.data()) guard let privateKeyData = Data(base64Encoded: decodedPayload.key) else { - // TODO: Change - fatalError() + throw JWEHelperError.invalidMasterkeyPayload } return try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: privateKeyData) } From ebde6294e89228d5e560e6fd4f70cb5a7fdcd103 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 3 Jan 2024 11:22:38 +0100 Subject: [PATCH 81/92] Derive Hub subscription state again from the header --- .../Hub/HubAuthenticationViewModel.swift | 2 +- .../Hub/HubAuthenticationViewModelTests.swift | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index 88dac11a9..ee81e7354 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -117,7 +117,7 @@ public final class HubAuthenticationViewModel: ObservableObject { do { let deviceKey = try cryptomatorHubKeyProvider.getPrivateKey() userKey = try JWEHelper.decryptUserKey(jwe: flowResponse.encryptedUserKey, privateKey: deviceKey) - subscriptionState = .active // try getSubscriptionState(from: flowResponse.header) // TODO: Revert this after Cryptomator Hub adds the subscription state back to the header + subscriptionState = try getSubscriptionState(from: flowResponse.header) } catch { await setStateToErrorState(with: error) return diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift index 56fcbd80e..a0710a866 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/HubAuthenticationViewModelTests.swift @@ -122,16 +122,19 @@ final class HubAuthenticationViewModelTests: XCTestCase { XCTAssertEqual(receivedResponse?.subscriptionState, .active) } - // TODO: Don't skip test as soon as the hub instance does return a valid header and we no longer hardcode a active hub subscription state! - func skip_testContinueToAccessCheck_success_hubSubscriptionStateIsInactive() async throws { + 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 + // the hub key service returns success with an active Cryptomator Hub subscription state hubKeyServiceMock.receiveKeyAuthStateVaultConfigReturnValue = try .successMock(header: ["hub-subscription-state": "INACTIVE"]) - hubKeyProviderMock.getPrivateKeyReturnValue = P384.KeyAgreement.PrivateKey(compactRepresentable: false) + + let devicePrivKey = "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB2bmFCWy2p+EbAn8NWS5Om+GA7c5LHhRZb8g2pSMSf0fsd7k7dZDVrnyHFiLdd/YGhZANiAAR6bsjTEdXKWIuu1Bvj6Y8wySlIROy7YpmVZTY128ItovCD8pcR4PnFljvAIb2MshCdr1alX4g6cgDOqcTeREiObcSfucOU9Ry1pJ/GnX6KA0eSljrk6rxjSDos8aiZ6Mg=" + let data = Data(base64Encoded: devicePrivKey)! + let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) + hubKeyProviderMock.getPrivateKeyReturnValue = privateKey // WHEN // continue the access check From 8ebb9b58a9c0bc460df765afba40c71ff4b47070 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Wed, 3 Jan 2024 11:26:14 +0100 Subject: [PATCH 82/92] Error cleanup --- CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift | 2 +- .../Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift | 2 -- .../Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift index 213a63676..62b8a1037 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/JWEHelper.swift @@ -31,7 +31,7 @@ public enum JWEHelper { let payloadMasterkey = try JSONDecoder().decode(PayloadMasterkey.self, from: payload.data()) guard let masterkeyData = Data(base64Encoded: payloadMasterkey.key) else { - throw VaultManagerError.invalidPayloadMasterkey + throw JWEHelperError.invalidMasterkeyPayload } return Masterkey.createFromRaw(rawKey: [UInt8](masterkeyData)) } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift index 85ca0bbf5..da860ef53 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift @@ -21,8 +21,6 @@ public enum VaultManagerError: Error { case vaultVersionNotSupported case fileProviderDomainNotFound case moveVaultInsideItself - case invalidDecrypter - case invalidPayloadMasterkey case missingVaultConfigToken } diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift index 9c958a524..311bb051c 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Hub/JWEHelperTests.swift @@ -212,7 +212,7 @@ final class JWEHelperTests: XCTestCase { let privateKey = try P384.KeyAgreement.PrivateKey(pkcs8DerRepresentation: data) XCTAssertThrowsError(try JWEHelper.decryptVaultKey(jwe: jwe, with: privateKey)) { error in - guard case VaultManagerError.invalidPayloadMasterkey = error else { + guard case JWEHelperError.invalidMasterkeyPayload = error else { XCTFail("Unexpected error: \(error)") return } From a7dcd5cc293844cf9822b632796a83d6260448c1 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Wed, 3 Jan 2024 20:31:41 +0100 Subject: [PATCH 83/92] Made `SIV_GCM` the default cipher combo for new vaults Fixes #288 --- .../CryptomatorCommonCore/Manager/VaultDBManager.swift | 2 +- .../Manager/VaultManagerTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift index da860ef53..1c24c9aba 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift @@ -79,7 +79,7 @@ public class VaultDBManager: VaultManager { */ public func createNewVault(withVaultUID vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString, isDirectory: true) - let cipherCombo = CryptorScheme.sivCtrMac + let cipherCombo = CryptorScheme.sivGcm let vaultConfig = VaultConfig.createNew(format: 8, cipherCombo: cipherCombo, shorteningThreshold: 220) let masterkey: Masterkey let provider: LocalizedCloudProviderDecorator diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift index 946e967e7..a4a00b0fe 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift @@ -125,11 +125,11 @@ class VaultManagerTests: XCTestCase { } XCTAssertEqual(uploadedVaultConfigToken, savedVaultConfigToken) let vaultConfig = try UnverifiedVaultConfig(token: savedVaultConfigToken) - XCTAssertEqual("SIV_CTRMAC", vaultConfig.allegedCipherCombo) + XCTAssertEqual("SIV_GCM", vaultConfig.allegedCipherCombo) XCTAssertEqual(8, vaultConfig.allegedFormat) let uploadedRootDirIdFile = try getUploadedData(at: CloudPath(cloudProviderMock.createdFolders[3]).appendingPathComponent("dirid.c9r")) - XCTAssertEqual(88, uploadedRootDirIdFile.count) + XCTAssertEqual(68, uploadedRootDirIdFile.count) }.catch { error in XCTFail("Promise failed with error: \(error)") }.always { From 47e30226b8da2f79ac9f63a9716d92e48f6b24b7 Mon Sep 17 00:00:00 2001 From: Cryptobot Date: Thu, 11 Jan 2024 09:59:39 +0100 Subject: [PATCH 84/92] New Crowdin updates (#314) [ci skip] --- CryptomatorIntents/ar.lproj/Intents.strings | 2 +- CryptomatorIntents/ba.lproj/Intents.strings | 33 ++ CryptomatorIntents/be.lproj/Intents.strings | 8 + CryptomatorIntents/bg.lproj/Intents.strings | 1 + CryptomatorIntents/cs.lproj/Intents.strings | 6 + CryptomatorIntents/fa.lproj/Intents.strings | 20 ++ CryptomatorIntents/fi.lproj/Intents.strings | 1 + CryptomatorIntents/fil.lproj/Intents.strings | 32 ++ CryptomatorIntents/hu.lproj/Intents.strings | 20 ++ CryptomatorIntents/ja.lproj/Intents.strings | 2 +- CryptomatorIntents/ko.lproj/Intents.strings | 15 + CryptomatorIntents/mk.lproj/Intents.strings | 1 + CryptomatorIntents/pa.lproj/Intents.strings | 30 ++ CryptomatorIntents/ta.lproj/Intents.strings | 1 + .../zh-Hant.lproj/Intents.strings | 2 +- SharedResources/ar.lproj/Localizable.strings | 42 ++- SharedResources/ba.lproj/Localizable.strings | 305 ++++++++++++++++++ SharedResources/be.lproj/Localizable.strings | 9 + SharedResources/bg.lproj/Localizable.strings | 52 +++ SharedResources/bn.lproj/Localizable.strings | 1 + SharedResources/ca.lproj/Localizable.strings | 6 + SharedResources/cs.lproj/Localizable.strings | 10 + SharedResources/da.lproj/Localizable.strings | 22 +- SharedResources/de.lproj/Localizable.strings | 22 +- SharedResources/el.lproj/Localizable.strings | 16 + SharedResources/es.lproj/Localizable.strings | 28 +- SharedResources/fa.lproj/Localizable.strings | 5 + SharedResources/fi.lproj/Localizable.strings | 20 ++ SharedResources/fil.lproj/Localizable.strings | 258 +++++++++++++++ SharedResources/fr.lproj/Localizable.strings | 16 + SharedResources/gl.lproj/Localizable.strings | 1 + SharedResources/he.lproj/Localizable.strings | 6 + SharedResources/hi.lproj/Localizable.strings | 2 + SharedResources/hr.lproj/Localizable.strings | 2 + SharedResources/hu.lproj/Localizable.strings | 188 +++++++++++ SharedResources/id.lproj/Localizable.strings | 2 + SharedResources/it.lproj/Localizable.strings | 12 + SharedResources/ja.lproj/Localizable.strings | 12 + SharedResources/ko.lproj/Localizable.strings | 22 ++ SharedResources/lv.lproj/Localizable.strings | 1 + SharedResources/mk.lproj/Localizable.strings | 6 + SharedResources/nb.lproj/Localizable.strings | 12 + SharedResources/nl.lproj/Localizable.strings | 16 + SharedResources/pa.lproj/Localizable.strings | 178 ++++++++++ SharedResources/pl.lproj/Localizable.strings | 15 + .../pt-BR.lproj/Localizable.strings | 34 +- SharedResources/pt.lproj/Localizable.strings | 16 + SharedResources/ro.lproj/Localizable.strings | 8 +- SharedResources/ru.lproj/Localizable.strings | 16 + SharedResources/sk.lproj/Localizable.strings | 16 + SharedResources/sl.lproj/Localizable.strings | 1 + SharedResources/sv.lproj/Localizable.strings | 12 + .../sw-TZ.lproj/Localizable.strings | 6 + SharedResources/ta.lproj/Localizable.strings | 1 + SharedResources/te.lproj/Localizable.strings | 1 + SharedResources/tr.lproj/Localizable.strings | 16 + SharedResources/uk.lproj/Localizable.strings | 10 + SharedResources/vi.lproj/Localizable.strings | 13 + .../zh-HK.lproj/Localizable.strings | 6 + .../zh-Hans.lproj/Localizable.strings | 20 +- .../zh-Hant.lproj/Localizable.strings | 6 + 61 files changed, 1607 insertions(+), 36 deletions(-) create mode 100644 CryptomatorIntents/ba.lproj/Intents.strings create mode 100644 CryptomatorIntents/bg.lproj/Intents.strings create mode 100644 CryptomatorIntents/fi.lproj/Intents.strings create mode 100644 SharedResources/ba.lproj/Localizable.strings create mode 100644 SharedResources/bg.lproj/Localizable.strings create mode 100644 SharedResources/fi.lproj/Localizable.strings diff --git a/CryptomatorIntents/ar.lproj/Intents.strings b/CryptomatorIntents/ar.lproj/Intents.strings index 844de668e..aa2d90733 100644 --- a/CryptomatorIntents/ar.lproj/Intents.strings +++ b/CryptomatorIntents/ar.lproj/Intents.strings @@ -1 +1 @@ -"common.vault" = "مخزن"; +"common.vault" = "الخزينة"; diff --git a/CryptomatorIntents/ba.lproj/Intents.strings b/CryptomatorIntents/ba.lproj/Intents.strings new file mode 100644 index 000000000..3b50fb0cb --- /dev/null +++ b/CryptomatorIntents/ba.lproj/Intents.strings @@ -0,0 +1,33 @@ +"common.failureReason" = "Уңышһыҙлыҡ сәбәбе"; +"common.false" = "юҡ"; +"common.folder" = "Каталог"; +"common.true" = "эйе"; +"common.vault" = "Һаҡлағыс"; + +"getFolderIntent.description" = "Күрһәтелгән һаҡлағыста бирелгән юл өсөн каталог объектын кире ҡайтара."; +"getFolderIntent.path" = "Юл"; +"getFolderIntent.text" = "${vault} эсендәге ${path} юллы каталогты ал"; +"getFolderIntent.title" = "Каталогты ал"; + +"isUnlockedIntent.description" = "Күрһәтелгән һаҡлағыстың асыҡмы икәнен күрһәтә."; +"isUnlockedIntent.title" = "Бик асыу"; + +"isVaultLockedIntent.title" = "Һаҡлағыс биге асыҡ"; +"isVaultUnlockedIntent.text" = "${vault} биге асыҡмы?"; + +"lockVaultIntent.description" = "Күрһәтелгән һаҡлағысты бикләй."; +"lockVaultIntent.text" = "${vault} биклә"; +"lockVaultIntent.title" = "Һаҡлағысты бикләү"; + +"openVaultIntent.description" = "Файлдар ҡушымтаһында күрһәтелгән һаҡлағысты аса."; +"openVaultIntent.text" = "Файлдар ҡушымтаһында ${vault} ас"; +"openVaultIntent.title" = "Һаҡлағыс асыу"; + +"saveFileIntent.description" = "Файлды һаҡлағысҡа һаҡлай."; +"saveFileIntent.file" = "Файл"; +"saveFileIntent.parameter.ignoreExisting" = "Бер иш исемле файлды иғтибарһыҙ ҡалдырырға"; +"saveFileIntent.text" = "${file} файлы ${folder} каталогына һаҡлай"; +"saveFileIntent.title" = "Файлды һаҡлау"; + +"vaultFolder.displayName" = "Һаҡлағыс каталогы"; +"vaultFolder.vaultIdentifier" = "Һаҡлағыс идентификаторы"; diff --git a/CryptomatorIntents/be.lproj/Intents.strings b/CryptomatorIntents/be.lproj/Intents.strings index 02ccf3728..ae03bff71 100644 --- a/CryptomatorIntents/be.lproj/Intents.strings +++ b/CryptomatorIntents/be.lproj/Intents.strings @@ -3,3 +3,11 @@ "isVaultLockedIntent.title" = "Ці разамкнёна скарбніца"; "isVaultUnlockedIntent.text" = "Ці ${vault} разамкнёны?"; +"openVaultIntent.title" = "Адчыніць скарбніцу"; + +"saveFileIntent.description" = "Захаваць файл у скарбніцы."; +"saveFileIntent.file" = "Файл"; +"saveFileIntent.title" = "Захаваць файл"; + +"vaultFolder.displayName" = "Тэчка скарбніцы"; +"vaultFolder.vaultIdentifier" = "Ідэнтыфікатар скарбніцы"; diff --git a/CryptomatorIntents/bg.lproj/Intents.strings b/CryptomatorIntents/bg.lproj/Intents.strings new file mode 100644 index 000000000..3c6af9eda --- /dev/null +++ b/CryptomatorIntents/bg.lproj/Intents.strings @@ -0,0 +1 @@ +"common.vault" = "Хранилище"; diff --git a/CryptomatorIntents/cs.lproj/Intents.strings b/CryptomatorIntents/cs.lproj/Intents.strings index a78d3bc66..d0d2470bd 100644 --- a/CryptomatorIntents/cs.lproj/Intents.strings +++ b/CryptomatorIntents/cs.lproj/Intents.strings @@ -9,6 +9,12 @@ "getFolderIntent.text" = "Získat složku, nacházející se v ${path} v ${vault}"; "getFolderIntent.title" = "Získat složku"; +"isUnlockedIntent.description" = "Vrátí, zda je daný trezor odemčený."; +"isUnlockedIntent.title" = "Je Odemčen"; + +"isVaultLockedIntent.title" = "Je trezor odemčen"; +"isVaultUnlockedIntent.text" = "Je ${vault} odemčen?"; + "lockVaultIntent.description" = "Uzamkne daný trezor."; "lockVaultIntent.text" = "Zamknout ${vault}"; "lockVaultIntent.title" = "Uzamknout trezor"; diff --git a/CryptomatorIntents/fa.lproj/Intents.strings b/CryptomatorIntents/fa.lproj/Intents.strings index 369e3fabf..60dbd875d 100644 --- a/CryptomatorIntents/fa.lproj/Intents.strings +++ b/CryptomatorIntents/fa.lproj/Intents.strings @@ -1 +1,21 @@ +"common.false" = "غلط"; +"common.folder" = "پوشه"; +"common.true" = "درست"; "common.vault" = "گاوصندوق"; +"getFolderIntent.path" = "مسیر"; +"getFolderIntent.title" = "دریافت پوشه"; +"isUnlockedIntent.title" = "باز شده است"; + +"isVaultLockedIntent.title" = "گاوصندوق باز شده است"; +"isVaultUnlockedIntent.text" = "آیا ${vault} باز شده است؟"; +"lockVaultIntent.text" = "قفل کردن ${vault}"; +"lockVaultIntent.title" = "قفل کردن گاوصندوق"; +"openVaultIntent.title" = "باز کردن گاوصندوق"; + +"saveFileIntent.description" = "یک فایل در گاوصندوق ذخیره میشود."; +"saveFileIntent.file" = "فایل"; +"saveFileIntent.text" = "ذخیره ${file} در ${folder}"; +"saveFileIntent.title" = "دخیرهٔ پرونده‌"; + +"vaultFolder.displayName" = "پوشه گاوصندوق"; +"vaultFolder.vaultIdentifier" = "مشخص کننده گاوصندوق"; diff --git a/CryptomatorIntents/fi.lproj/Intents.strings b/CryptomatorIntents/fi.lproj/Intents.strings new file mode 100644 index 000000000..c369e1822 --- /dev/null +++ b/CryptomatorIntents/fi.lproj/Intents.strings @@ -0,0 +1 @@ +"common.vault" = "Vault"; diff --git a/CryptomatorIntents/fil.lproj/Intents.strings b/CryptomatorIntents/fil.lproj/Intents.strings index c369e1822..7bad93386 100644 --- a/CryptomatorIntents/fil.lproj/Intents.strings +++ b/CryptomatorIntents/fil.lproj/Intents.strings @@ -1 +1,33 @@ +"common.failureReason" = "Dahilan ng Pagkabigo"; +"common.false" = "mali"; +"common.folder" = "Folder"; +"common.true" = "totoo"; "common.vault" = "Vault"; + +"getFolderIntent.description" = "Nagbabalik ng folder object para sa ibinigay na path sa ibinigay na vault."; +"getFolderIntent.path" = "Daan"; +"getFolderIntent.text" = "Kunin ang folder na matatagpuan sa ${path} sa ${vault}"; +"getFolderIntent.title" = "Kumuha ng Folder"; + +"isUnlockedIntent.description" = "Ibinabalik kung naka-unlock ang ibinigay na vault."; +"isUnlockedIntent.title" = "Naka-unlock"; + +"isVaultLockedIntent.title" = "Naka-unlock ang vault"; +"isVaultUnlockedIntent.text" = "Naka-unlock ba ang ${vault}?"; + +"lockVaultIntent.description" = "Nila-lock ang ibinigay na vault."; +"lockVaultIntent.text" = "I-lock ang ${vault}"; +"lockVaultIntent.title" = "Lock Vault"; + +"openVaultIntent.description" = "Binubuksan ang ibinigay na vault sa Files app."; +"openVaultIntent.text" = "Buksan ang ${vault} sa Files app"; +"openVaultIntent.title" = "Buksan ang Vault"; + +"saveFileIntent.description" = "Nagse-save ng file sa isang vault."; +"saveFileIntent.file" = "file"; +"saveFileIntent.parameter.ignoreExisting" = "Huwag pansinin ang umiiral na file na may parehong pangalan"; +"saveFileIntent.text" = "I-save ang ${file} sa ${folder}"; +"saveFileIntent.title" = "I-save ang File"; + +"vaultFolder.displayName" = "Vault Folder"; +"vaultFolder.vaultIdentifier" = "Vault Identifier"; diff --git a/CryptomatorIntents/hu.lproj/Intents.strings b/CryptomatorIntents/hu.lproj/Intents.strings index 5398e7ff9..92eddd9b1 100644 --- a/CryptomatorIntents/hu.lproj/Intents.strings +++ b/CryptomatorIntents/hu.lproj/Intents.strings @@ -1 +1,21 @@ +"common.false" = "hamis"; +"common.folder" = "Mappa"; +"common.true" = "igaz"; "common.vault" = "Széf"; + +"getFolderIntent.description" = "Visszaad egy mappa objektumot a megadott útvonalhoz a megadott széfben."; +"getFolderIntent.path" = "Útvonal"; +"getFolderIntent.text" = "Visszaadja a mappát ezen a helyen: ${path}, ebben a széfben: ${vault}"; + +"isUnlockedIntent.description" = "Visszaadja, hogy az adott széf fel van-e oldva."; +"lockVaultIntent.text" = "${vault} zárolása"; +"lockVaultIntent.title" = "Széf zárolása"; + +"openVaultIntent.description" = "Megnyitja az adott széfet a Fájlok appban."; +"openVaultIntent.text" = "${vault} megnyitása a Fájlok appban"; +"openVaultIntent.title" = "Széf megnyitása"; + +"saveFileIntent.description" = "Elment egy fájlt egy széfbe."; + +"vaultFolder.displayName" = "Széf mappa"; +"vaultFolder.vaultIdentifier" = "Széf azonosító"; diff --git a/CryptomatorIntents/ja.lproj/Intents.strings b/CryptomatorIntents/ja.lproj/Intents.strings index f1e94a15e..79aa1b66b 100644 --- a/CryptomatorIntents/ja.lproj/Intents.strings +++ b/CryptomatorIntents/ja.lproj/Intents.strings @@ -10,7 +10,7 @@ "getFolderIntent.title" = "フォルダーを戻す"; "isUnlockedIntent.description" = "金庫が解錠かどうか戻します。"; -"isUnlockedIntent.title" = "解錠です"; +"isUnlockedIntent.title" = "解錠済"; "isVaultLockedIntent.title" = "金庫が解錠ですか?"; "isVaultUnlockedIntent.text" = "${vault} が解錠してありますか?"; diff --git a/CryptomatorIntents/ko.lproj/Intents.strings b/CryptomatorIntents/ko.lproj/Intents.strings index c369e1822..f302a3d9a 100644 --- a/CryptomatorIntents/ko.lproj/Intents.strings +++ b/CryptomatorIntents/ko.lproj/Intents.strings @@ -1 +1,16 @@ +"common.failureReason" = "실패 사유"; +"common.folder" = "폴더"; "common.vault" = "Vault"; +"getFolderIntent.path" = "경로"; +"getFolderIntent.title" = "폴더 열기"; + +"lockVaultIntent.description" = "해당 vault를 잠급니다."; +"lockVaultIntent.title" = "Vault 잠그기"; +"openVaultIntent.title" = "Vault 열기"; +"saveFileIntent.file" = "파일"; +"saveFileIntent.parameter.ignoreExisting" = "같은 이름을 가진 기존 파일 무시하기"; +"saveFileIntent.text" = "${file}을 ${folder}에 저장"; +"saveFileIntent.title" = "파일 저장하기"; + +"vaultFolder.displayName" = "Vault 폴더"; +"vaultFolder.vaultIdentifier" = "Vault 식별자"; diff --git a/CryptomatorIntents/mk.lproj/Intents.strings b/CryptomatorIntents/mk.lproj/Intents.strings index e69de29bb..74c0da3a1 100644 --- a/CryptomatorIntents/mk.lproj/Intents.strings +++ b/CryptomatorIntents/mk.lproj/Intents.strings @@ -0,0 +1 @@ +"common.vault" = "Сеф"; diff --git a/CryptomatorIntents/pa.lproj/Intents.strings b/CryptomatorIntents/pa.lproj/Intents.strings index ac591e060..b17370b42 100644 --- a/CryptomatorIntents/pa.lproj/Intents.strings +++ b/CryptomatorIntents/pa.lproj/Intents.strings @@ -1 +1,31 @@ +"common.failureReason" = "ਫੇਲ੍ਹ ਹੋਣ ਦਾ ਕਾਰਨ"; +"common.false" = "ਅਸਫ਼ਲ"; +"common.folder" = "ਫੋਲਡਰ"; +"common.true" = "ਸੱਚ"; "common.vault" = "ਵਾਲਟ"; + +"getFolderIntent.description" = "ਦਿੱਤੇ ਵਾਲਟ ਵਿੱਚ ਦਿੱਤੇ ਹੋਏ ਮਾਰਗ ਲਈ ਫੋਲਡਰ ਆਬਜੈਕਟ ਵਾਪਸ ਦਿੰਦਾ ਹੈ।"; +"getFolderIntent.path" = "ਮਾਰਗ"; +"getFolderIntent.text" = "${vault} ਵਿੱਚ ${path} ਉੱਤੇ ਮੌਜੂਦ ਫੋਲਡਰ ਲਵੋ"; +"getFolderIntent.title" = "ਫੋਲਡਰ ਲਵੋ"; +"isUnlockedIntent.title" = "ਅਣ-ਲਾਕ ਹੈ"; + +"isVaultLockedIntent.title" = "ਵਾਲਟ ਅਣ-ਲਾਕ ਹੈ"; +"isVaultUnlockedIntent.text" = "${vault} ਅਣ-ਲਾਕ ਕਰਨਾ ਹੈ?"; + +"lockVaultIntent.description" = "ਦਿੱਤੇ ਵਾਲਟ ਨੂੰ ਲਾਕ ਕਰਦਾ ਹੈ।"; +"lockVaultIntent.text" = "${vault} ਨੂੰ ਲਾਕ ਕਰੋ"; +"lockVaultIntent.title" = "ਵਾਲਟ ਲਾਕ ਕਰੋ"; + +"openVaultIntent.description" = "ਫਾਇਲ ਐਪ ਵਿੱਚ ਦਿੱਤਾ ਵਾਲਟ ਖੋਲ੍ਹੋ।"; +"openVaultIntent.text" = "ਫਾਇਲਾਂ ਐਪ ਵਿੱਚ ${vault} ਨੂੰ ਖੋਲ੍ਹੋ"; +"openVaultIntent.title" = "ਵਾਲਟ ਖੋਲ੍ਹੋ"; + +"saveFileIntent.description" = "ਫਾਇਲ ਨੂੰ ਵਾਲਟ ਵਿੱਚ ਖੋਲ੍ਹੋ।"; +"saveFileIntent.file" = "ਫਾਇਲ"; +"saveFileIntent.parameter.ignoreExisting" = "ਇੱਕੋ ਨਾਂ ਦੀ ਮੌਜੂਦਾ ਫਾਇਲ ਨੂੰ ਅਣਡਿੱਠਾ ਕਰੋ"; +"saveFileIntent.text" = "${folder} ਵਿੱਚ ${file} ਸੰਭਾਲੋ"; +"saveFileIntent.title" = "ਫਾਇਲ ਸੰਭਾਲੋ"; + +"vaultFolder.displayName" = "ਵਾਲਟ ਫੋਲਡਰ"; +"vaultFolder.vaultIdentifier" = "ਵਾਲਟ ਪਛਾਣਕਰਤਾ"; diff --git a/CryptomatorIntents/ta.lproj/Intents.strings b/CryptomatorIntents/ta.lproj/Intents.strings index 79f04a5ed..159544fa2 100644 --- a/CryptomatorIntents/ta.lproj/Intents.strings +++ b/CryptomatorIntents/ta.lproj/Intents.strings @@ -1 +1,2 @@ +"common.false" = "தவறு"; "common.vault" = "பெட்டகம்"; diff --git a/CryptomatorIntents/zh-Hant.lproj/Intents.strings b/CryptomatorIntents/zh-Hant.lproj/Intents.strings index e7ec1c531..bdba1f45f 100644 --- a/CryptomatorIntents/zh-Hant.lproj/Intents.strings +++ b/CryptomatorIntents/zh-Hant.lproj/Intents.strings @@ -2,7 +2,7 @@ "common.false" = "否"; "common.folder" = "資料夾"; "common.true" = "是"; -"common.vault" = "保险库"; +"common.vault" = "加密檔案庫"; "getFolderIntent.description" = "返回特定加密檔案庫內指定路徑的資料夾。"; "getFolderIntent.path" = "路徑"; diff --git a/SharedResources/ar.lproj/Localizable.strings b/SharedResources/ar.lproj/Localizable.strings index ca085911a..d277bdefa 100644 --- a/SharedResources/ar.lproj/Localizable.strings +++ b/SharedResources/ar.lproj/Localizable.strings @@ -7,9 +7,10 @@ "common.alert.error.title" = "خطأ"; "common.alert.attention.title" = "انتباه"; -"common.button.cancel" = "الغاء"; +"common.button.cancel" = "إلغاء"; "common.button.change" = "تغيير"; "common.button.choose" = "اختر"; +"common.button.clear" = "مسح"; "common.button.close" = "إغلاق"; "common.button.confirm" = "تأكيد"; "common.button.create" = "إنشاء"; @@ -20,6 +21,7 @@ "common.button.enable" = "تفعيل"; "common.button.next" = "التالي"; "common.button.ok" = "موافق"; +"common.button.refresh" = "تحديث"; "common.button.remove" = "حذف"; "common.button.retry" = "اعد المحاولة"; "common.button.signOut" = "تسجيل الخروج"; @@ -29,6 +31,7 @@ "common.cells.url" = "الرابط"; "common.cells.username" = "اسم المستخدم"; "common.footer.learnMore" = "معرفة المزيد."; +"common.hud.authenticating" = "المصادقة…"; "accountList.header.title" = "المصادقة"; "accountList.emptyList.message" = "انقر هنا لإضافة حساب"; @@ -38,7 +41,7 @@ "addVault.title" = "أضِف مخزنًا"; "addVault.createNewVault.title" = "إنشاء مخزن جديد"; "addVault.createNewVault.setVaultName.header.title" = "اختر اسماً للمخزن."; -"addVault.createNewVault.setVaultName.cells.name" = "اسم المخزن"; +"addVault.createNewVault.setVaultName.cells.name" = "اسم الخزينة"; "addVault.createNewVault.setVaultName.error.emptyVaultName" = "اسم المخزن لا يمكن أن يكون فارغاً."; "addVault.createNewVault.chooseCloud.header" = "أين يجب على Cryptomator تخزين الملفات المشفرة للمخزن الخاص بك؟"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" موجود مسبقاً في هذا الموقع. اختر اسم مخزن أو موقع مختلف."; @@ -95,6 +98,13 @@ "fileProvider.error.biometricalAuthWrongPassword.message" = "كلمة المرور التي تم حفظها لـ %@ خاطئة. الرجاء المحاولة مرة أخرى وإدخال كلمة المرور الخاصة بك لإعادة تمكين %@."; "fileProvider.error.defaultLock.title" = "يلزم فك القفل"; "fileProvider.error.unlockButton" = "افتح"; +"fileProvider.uploadProgress.connecting" = "جاري الاتصال…"; +"hubAuthentication.accessNotGranted" = "لم يتم بعد منح الإذن لجهازك بالوصول إلى هذا المخزن. اطلب من مالك المخزن أن يأذن بذلك."; +"hubAuthentication.licenseExceeded" = "نموذج المركز Cryptomator الخاص بك لديه ترخيص غير صالح. الرجاء إبلاغ مسؤول مركز لترقية أو تجديد الترخيص."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "اسم الجهاز"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "للدخول إلى الخزينة، يحتاج جهازك إلى إذن من مالك الخزينة."; +"keepUnlockedDuration.auto.shortDisplayName" = "تلقائيًا"; +"keepUnlockedDuration.indefinite" = "دائم"; "localFileSystemAuthentication.createNewVault.header" = "في الشاشة التالية، اختر موقع التخزين للمخزن الجديد الخاص بك."; "localFileSystemAuthentication.createNewVault.button" = "حدد موقع التخزين"; @@ -104,7 +114,24 @@ "localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "المجلد المحدد ليس مخزن. الرجاء المحاولة مرة أخرى مع مجلد مختلف."; "onboarding.title" = "أهلاً وسهلاً"; +"onboarding.button.continue" = "استمرار"; +"purchase.footer.privacyPolicy" = "سياسة الخصوصية"; +"purchase.footer.termsOfUse" = "شروط الإستخدام"; +"purchase.header.feature.familySharing" = "مشاركة عائلية"; +"purchase.header.feature.openSource" = "تطوير مفتوح المصدر"; +"purchase.product.donateAndUpgrade" = "تبرع و ترقية"; +"purchase.product.freeUpgrade" = "ترقية مجانية"; +"purchase.product.lifetimeLicense" = "رخصة لمدى الحياة"; +"purchase.product.lifetimeLicense.duration" = "لمرة واحدة"; +"purchase.product.pricing.free" = "مجاناً"; +"purchase.product.trial" = "تجربة مجانية لمدة 30 يوم"; +"purchase.product.trial.expirationDate" = "تاريخ إنتهاء الصلاحية: %@"; +"purchase.product.trial.duration" = "لمدة 30 يوماً"; +"purchase.product.yearlySubscription.duration" = "سنوياً"; +"purchase.readOnlyMode.alert.title" = "وضع القراءة فقط"; +"purchase.restorePurchase.button" = "استعادة المشتريات"; "purchase.retry.button" = "اعد المحاولة"; +"purchase.unlockedFullVersion.title" = "شكراً لك"; "settings.title" = "الإعدادات"; "settings.aboutCryptomator.title" = "الإصدار %@ (%@)"; @@ -112,8 +139,10 @@ "settings.clearCache" = "مسح بيانات الذاكرة المؤقتة"; "settings.cloudServices" = "الخدمات السحابية"; "settings.debugMode.alert.message" = "في هذا الوضع، يمكن كتابة البيانات الحساسة إلى ملف تسجيل على جهازك (على سبيل المثال أسماء الملفات والمسارات). كلمة المرور، ملفات تعريف الارتباط، إلخ. مستبعدة صراحة.\n\nتذكر تعطيل وضع التصحيح في أقرب وقت ممكن."; +"settings.manageSubscriptions" = "إدارة الاشتراك"; "settings.rateApp" = "تقييم التطبيق"; "settings.sendLogFile" = "إرسال ملف السجل"; +"settings.shortcutsGuide" = "دليل الاختصارات"; "settings.unlockFullVersion" = "فتح النسخة الكاملة"; "s3Authentication.displayName" = "الاسم المعروض"; @@ -122,6 +151,10 @@ "s3Authentication.existingBucket" = "مخزن موجود مسبقاً"; "s3Authentication.endpoint" = "نقطة الوصول"; "s3Authentication.region" = "المنطقة"; +"s3Authentication.error.invalidCredentials" = "معلومات الدخول خاطئة."; + +"trialStatus.active" = "مفعل"; +"trialStatus.expired" = "منتهية الصلاحية"; "unlockVault.button.unlock" = "افتح"; "unlockVault.button.unlockVia" = "فتح عبر %@"; @@ -146,12 +179,17 @@ "vaultDetail.button.changeVaultPassword" = "تغيير كلمة المرور"; "vaultDetail.button.lock" = "قفل الآن"; "vaultDetail.button.moveVault" = "نقل"; +"vaultDetail.button.removeVault" = "إزالة من قائمة المخازن"; "vaultDetail.button.renameVault" = "إعادة تسمية"; "vaultDetail.disabledBiometricalUnlock.footer" = "إذا مكّنت %@، سيتم تخزين كلمة المرور الخاصة بك في سلسلة مفاتيح iOS."; +"vaultDetail.keepUnlocked.title" = "مدة الفتح"; +"vaultDetail.moveVault.progress" = "جاري النقل…"; +"vaultDetail.renameVault.progress" = "إعادة تسمية…"; "vaultDetail.unlockVault.footer" = "أدخل كلمة المرور ل \"%@\" لتخزينها في سلسلة مفاتيح iOS وتمكين %@."; "vaultList.header.title" = "المخازن"; "vaultList.emptyList.message" = "انقر هنا لإضافة مخزن"; +"vaultList.remove.alert.title" = "حذف المخزن؟"; "webDAVAuthentication.httpConnection.alert.title" = "إستخدام HTTPS؟"; "webDAVAuthentication.httpConnection.alert.message" = "استخدام HTTP غير آمن. نوصي باستخدام HTTPS بدلاً من ذلك. إذا كنت تعرف المخاطر، يمكنك الاستمرار مع HTTP."; diff --git a/SharedResources/ba.lproj/Localizable.strings b/SharedResources/ba.lproj/Localizable.strings new file mode 100644 index 000000000..0e23544b7 --- /dev/null +++ b/SharedResources/ba.lproj/Localizable.strings @@ -0,0 +1,305 @@ +/* + Localizable.strings + Cryptomator + + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"common.alert.error.title" = "Хата"; +"common.alert.attention.title" = "Иғтибар"; +"common.button.cancel" = "Кире ал"; +"common.button.change" = "Үҙгәрт"; +"common.button.choose" = "Һайла"; +"common.button.clear" = "Таҙала"; +"common.button.close" = "Яп"; +"common.button.confirm" = "Раҫла"; +"common.button.create" = "Яһа"; +"common.button.createFolder" = "Каталог өҫтәү"; +"common.button.done" = "Тамам"; +"common.button.download" = "Күсереп ал"; +"common.button.edit" = "Төҙәт"; +"common.button.enable" = "Ғәмәлгә индер"; +"common.button.next" = "Киләһе"; +"common.button.ok" = "Ярай"; +"common.button.refresh" = "Яңырт"; +"common.button.register" = "Теркәл"; +"common.button.remove" = "Алып ташлау"; +"common.button.retry" = "Ҡабатла"; +"common.button.signOut" = "Сыҡ"; +"common.button.verify" = "Тикшер"; +"common.cells.openInFilesApp" = "Файлдар ҡушымтаһында ас"; +"common.cells.password" = "Серһүҙ"; +"common.cells.url" = "URL"; +"common.cells.username" = "Ҡулланыусы исеме"; +"common.footer.learnMore" = "Артабан уҡырға."; +"common.hud.authenticating" = "Аутентиклау…"; + +"accountList.header.title" = "Аутентиклауҙар"; +"accountList.emptyList.message" = "Иҫәп яҙмаһы өҫтәү өсөн бында баҫығыҙ"; +"accountList.signOut.alert.title" = "Бәйле һаҡлағыстарҙы юйырғамы?"; +"accountList.signOut.alert.message" = "Сығыу менән бөтә бәйле һаҡлағыстар исемлектән алып ташланасаҡ. Шифрланған мәғлүмәттәр юйылмаясаҡ. Аҙаҡтан яңынан инеп һаҡлағыстарҙы ҡабаттан өҫтәй алаһығыҙ."; + +"addVault.title" = "Һаҡлағыс өҫтәү"; +"addVault.createNewVault.title" = "Яңы һаҡлағыс яһау"; +"addVault.createNewVault.purchase" = "Яңы һаҡлағыс яһау өсөн Cryptomator-ҙың тулы версияһы кәрәк."; +"addVault.createNewVault.setVaultName.header.title" = "Һаҡлағысҡа исем һайлағыҙ."; +"addVault.createNewVault.setVaultName.cells.name" = "Һаҡлағыс исеме"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "Һаҡлағыс исеме буш була алмай."; +"addVault.createNewVault.chooseCloud.header" = "Cryptomator һаҡлағысығыҙҙың шифрланған файлдарын ҡайҙа һаҡларға тейеш?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" был урында бар инде. Башҡа һаҡлағыс исеме йәки урын һайлағыҙ."; +"addVault.createNewVault.detectedMasterkey.text" = "Cryptomator был урында булған һаҡлағыс тапты.\nЯңы һаҡлағыс булдырыу өсөн кире ҡайтып икенсе каталог һайлағыҙ."; +"addVault.createNewVault.password.enterPassword.header" = "Яңы серһүҙ яҙығыҙ."; +"addVault.createNewVault.password.confirmPassword.header" = "Яңы серһүҙҙе раҫлағыҙ."; +"addVault.createNewVault.password.confirmPassword.alert.title" = "Серһүҙҙе раҫлайһығыҙмы?"; +"addVault.createNewVault.password.confirmPassword.alert.message" = "МӨҺИМ: әгәр серһүҙҙе онотһағыҙ, уны тергеҙеү ысулы юҡ."; +"addVault.createNewVault.password.error.emptyPassword" = "Серһүҙ буш була алмай."; +"addVault.createNewVault.password.error.nonMatchingPasswords" = "Серһүҙҙәр тап килмәй."; +"addVault.createNewVault.password.error.tooShortPassword" = "Серһүҙ кәмендә 8 билдәнән торорға тейеш."; +"addVault.createNewVault.progress" = "Һаҡлағыс яһау…"; +"addVault.openExistingVault.title" = "Булған һаҡлағысты асыу"; +"addVault.openExistingVault.chooseCloud.header" = "Һаҡлағыс урыны ҡайҙа?"; +"addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator \"%@\" һаҡлағысын тапты.\nБыл һаҡлағысты өҫтәргә теләйһегеҙме?"; +"addVault.openExistingVault.detectedMasterkey.add" = "Был һаҡлағысты өҫтә"; +"addVault.openExistingVault.downloadVault.progress" = "Һаҡлағыс күсерелә…"; +"addVault.openExistingVault.password.footer" = "\"%@\" серһүҙен яҙығыҙ."; +"addVault.openExistingVault.progress" = "Һаҡлағыс өҫтәү…"; +"addVault.success.info" = "\"%@\" һаҡлағысы өҫтәлде. \nФайлдар ҡушымтаһы аша һаҡлағысҡа инә алаһығыҙ."; +"addVault.success.footer" = "Быға тиклем Cryptomator-ҙы ғәмәлгә индермәгән булһағыҙ, уны Файлдар ҡушымтаһы аша эшләп була."; + +"biometryType.faceID" = "Face ID"; +"biometryType.touchID" = "Touch ID"; + +"changePassword.error.invalidOldPassword" = "Хәҙерге серһүҙ дөрөҫ түгел. Яңынан ҡабатлағыҙ."; +"changePassword.header.currentPassword.title" = "Хәҙерге серһүҙҙе яҙығыҙ."; +"changePassword.header.newPassword.title" = "Яңы серһүҙ яҙығыҙ."; +"changePassword.header.newPasswordConfirmation.title" = "Яңы серһүҙҙе раҫлағыҙ."; +"changePassword.progress" = "Серһүҙҙе үҙгәртеү…"; + +"chooseFolder.emptyFolder.footer" = "Каталог буш"; +"chooseFolder.createNewFolder.header.title" = "Каталог өсөн исем һайлағыҙ."; +"chooseFolder.createNewFolder.cells.name" = "Каталог исеме"; +"chooseFolder.createNewFolder.error.emptyFolderName" = "Каталог исеме буш була алмай."; +"chooseFolder.createNewFolder.progress" = "Каталог яһау…"; + +"cloudProvider.error.itemNotFound" = "\"%@\" табылманы."; +"cloudProvider.error.itemAlreadyExists" = "\"%@\" бар инде."; +"cloudProvider.error.itemTypeMismatch" = "\"%@\" көтөлмәгән элемент төрөнә эйә."; +"cloudProvider.error.parentFolderDoesNotExist" = "Төп каталог \"%@\" юҡ."; +"cloudProvider.error.pageTokenInvalid" = "Каталогтың йөкмәткеһен алыуҙы дауам итеп булмай."; +"cloudProvider.error.quotaInsufficient" = "Һаҡлағысығыҙҙа урын етмәй."; +"cloudProvider.error.unauthorized" = "Рөхсәтһеҙ ғәмәлде башҡарып булмай."; +"cloudProvider.error.noInternetConnection" = "Был ғәмәл өсөн интернет бәйләнеше кәрәк."; + +"cloudProviderType.localFileSystem" = "Башҡа файл провайдеры"; + +"fileProvider.onboarding.title" = "Рәхим итегеҙ"; +"fileProvider.onboarding.info" = "Файлдарығыҙҙы һаҡлау өсөн Cryptomator һайлағанығыҙ өсөн рәхмәт. Башлар өсөн төп ҡушымтаға инеп һаҡлағыс өҫтәгеҙ."; +"fileProvider.onboarding.button.openCryptomator" = "Cryptomator ас"; +"fileProvider.error.biometricalAuthCanceled.title" = "Биген асыу туҡтатылды"; +"fileProvider.error.biometricalAuthCanceled.message" = "%@ аша бик асыу уңышһыҙ булды. Яңынан ҡабатлап ҡарағыҙ."; +"fileProvider.error.biometricalAuthWrongPassword.title" = "Серһүҙ дөрөҫ түгел"; +"fileProvider.error.biometricalAuthWrongPassword.message" = "%@ өсөн һаҡланған серһүҙ дөрөҫ түгел. Ҡабатлап ҡарағыҙ һәм %@ ғәмәлғә инһен өсөн серһүҙҙе яңынан яҙығыҙ."; +"fileProvider.error.defaultLock.title" = "Бикте асыу кәрәкле"; +"fileProvider.error.defaultLock.message" = "Һаҡлағысығыҙға инеү һәм эстәлеген күреү өсөн, уны асырға кәрәк."; +"fileProvider.error.unlockButton" = "Биген ас"; +"fileProvider.clearFileFromCache.title" = "Файлды кэштан таҙалау"; +"fileProvider.clearFileFromCache.message" = "Был йыһазығыҙҙағы урындағы файлды ғына алып ташлай һәм болоттағы файлды юймай."; +"fileProvider.fileImporting.error.missingPremium" = "Һаҡлағыстарығыҙға яҙыу хоҡуғы алыу өсөн Cryptomator ҡушымтаһының тулы версияһын алырға."; +"fileProvider.uploadProgress.connecting" = "Тоташыу…"; +"fileProvider.uploadProgress.message" = "Ағымдағы прогресс:%@\n\nӘгәр йөкләү процесы тотҡарлана икән, йөкләүҙе ҡабаттан башларға мөмкин."; +"fileProvider.uploadProgress.missing" = "Прогресты билдәләп булмай. Артҡы планда һаман эшләп тороуы мөмкин."; +"fileProvider.uploadProgress.title" = "Тейәү…"; +"fileProvider.uploadProgress.missingDomainError" = "Доменды табып булмай."; + +"getFolderIntent.error.missingPath" = "Бер ниндәй ҙә юл бирелмәгән. Каталогты кире ҡайтарыу өсөн дөрөҫ юл биреүегеҙҙе һорайбыҙ."; +"getFolderIntent.error.noVaultSelected" = "Һаҡлағыс һайланмаған."; + +"hubAuthentication.title" = "Һаҡлағыс хабы"; +"hubAuthentication.accessNotGranted" = "Һеҙҙең йыһаз әлегә был һаҡлағысҡа инеү хоҡуғына эйә түгел. Һаҡлағыс хужаһынан рөхсәт һорағыҙ."; +"hubAuthentication.licenseExceeded" = "Һеҙҙең Cryptomator хабығыҙ ғәмәлдән тыш рөхсәтнамәгә эйә. Рөхсәтнамәне яңыртыу йәки оҙайтыу өсөн Хаб администраторына хәбәр итегеҙ."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Йыһаз исеме"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Был, күрәһең, был ҡоролманан Хабҡа тәү инеү ваҡыты. Инеүҙе рөхсәт итеү өсөн йыһазды асыҡлаусы исем атарға кәрәк."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Яңы ҡушымталар йәки браузерҙарҙан инеү өсөн Һеҙҙең иҫәп-хисап асҡысы кәрәк. Уны профилегеҙҙә табырға мөмкин."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Йыһазды теркәү уңышлы булды"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Һаҡлағысҡа инеү өсөн һаҡлағыс хужаһы йыһазығыҙға инеү рөхсәте бирергә тейеш."; +"hubAuthentication.requireAccountInit.alert.title" = "Кәрәкле эш-хәрәкәт"; +"hubAuthentication.requireAccountInit.alert.message" = "Артабан барыр өсөн, Hub ҡулланыусылар профилендә кәрәкле аҙымдарҙы тамамлағыҙ."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Профилгә күс"; + +"intents.saveFile.missingFile" = "Бирелгән файл ғәмәлдә түгел."; +"intents.saveFile.invalidFolder" = "Бирелгән каталог ғәмәлдә түгел."; +"intents.saveFile.missingTemporaryFolder" = "Ваҡытлыса каталогтар яһау уңышһыҙ тамамланды."; +"intents.saveFile.lockedVault" = "Был ҡыҫҡа юлды ҡулланыр өсөн һаҡлағысты асырға кәрәк."; +"intents.saveFile.selectedVaultNotFound" = "Һайланған һаҡлағысты табып булманы."; + +"keepUnlocked.alert.title" = "Һаҡлағысты бикләргәме?"; +"keepUnlocked.alert.message" = "Был үҙгәреш үҙ көсөнә инеү өсөн һаҡлағысты бикләүҙе талап итә."; +"keepUnlocked.alert.confirm" = "Раҫла һәм биклә"; +"keepUnlocked.header" = "Буш торғанда был һаҡлағыстың күпме ваҡыт бикһеҙ ҡалыуын һайлағыҙ."; +"keepUnlocked.footer.auto" = "iOS-ҡа ҡарар ҡабул итеүҙе рөхсәт итеү, хәтерҙе бушатыу өсөн теләһә ҡайһы ваҡытта Cryptomator-ҙы туҡтатырға мөмкин тигәнде аңлата, был һаҡлағысты автоматик рәүештә бикләй."; +"keepUnlocked.footer.on" = "Һайланған вариант ҡулланылғанда, һаҡлағыстың биге асылған саҡта, асҡыстың күсермәһе iOS-тың асҡыс сылбырында һаҡланырға тейеш."; +"keepUnlockedDuration.auto" = "iOS автоматик рәүештә ҡарар ҡабул итһен"; +"keepUnlockedDuration.auto.shortDisplayName" = "Авто"; +"keepUnlockedDuration.indefinite" = "Билдәһеҙ"; + +"localFileSystemAuthentication.createNewVault.header" = "Киләһе экранда яңы һаҡлағысығыҙ өсөн урын һайлағыҙ."; +"localFileSystemAuthentication.createNewVault.button" = "Һаҡлау урыны һайла"; +"localFileSystemAuthentication.createNewVault.error.detectedExistingVault" = "Был урында һаҡлағыс бар инде. Башҡа һаҡлау урыны менән ҡабатлап ҡарағыҙ."; +"localFileSystemAuthentication.openExistingVault.header" = "Киләһе экранда булған һаҡлағысығыҙ өсөн каталог һайлағыҙ."; +"localFileSystemAuthentication.openExistingVault.button" = "Һаҡлағыс каталогы һайла"; +"localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "Һайланған каталог һаҡлағыс түгел. Зинһар, икенсе каталог менән ҡабатлап ҡарағыҙ."; +"localFileSystemAuthentication.info.footer" = "Һоро төҫтәге файл провайдерҙары «каталогтар йыйыуҙы» хупламай. Был Cryptomator сикләүе түгел."; + +"maintenanceModeError.runningCloudTask" = "Ғәмәлде башҡарып булмай сөнки башта был һаҡлағыстың башҡа ғәмәлдәре тамамланырға тейеш. Һуңыраҡ ҡабатлап ҡарағыҙ."; + +"nameValidation.error.endsWithPeriod" = "Нөктә менән тамамланған исемде ҡулланырға ярамай. Икенсе исем һайлағыҙ."; +"nameValidation.error.endsWithSpace" = "Арауыҡ менән тамамланған исемде ҡулланырға ярамай. Икенсе исем һайлағыҙ."; +"nameValidation.error.containsIllegalCharacter" = "\"%@\" булған исемде ҡулланырға ярамай. Икенсе исем һайлағыҙ."; + +"onboarding.title" = "Рәхим итегеҙ"; +"onboarding.info" = "Файлдарығыҙҙы һаҡлау өсөн Cryptomator һайлағанығыҙ өсөн рәхмәтn\n\nCryptomator менән мәғлүмәттәрегеҙҙең асҡысы — һеҙҙең ҡулда. Cryptomator мәғлүмәттәрегеҙҙе тиҙ һәм еңел шифрлай.\n\nБыл ҡушымта тулыһынса Файлдар ҡушымтаһы менән берләштерелгән. Һуңыраҡ Файлдар ҡушымтаһында Cryptomator'ға һаҡлағыстарығыҙға инеү мөмкинлеген бирегеҙ."; +"onboarding.button.continue" = "Дауам ит"; + +"purchase.beginFreeTrial.alert.title" = "Һынау версияһы асылды"; +"purchase.expiredTrial" = "Һынау вакыты бөттө."; +"purchase.footer.privacyPolicy" = "Хосусилыҡ сәйәсәте"; +"purchase.footer.termsOfUse" = "Ҡулланыу шарттары"; +"purchase.header.feature.familySharing" = "Ғаилә менән уртаҡлашыу"; +"purchase.header.feature.openSource" = "Open-source эшләү"; +"purchase.header.feature.writeAccess" = "Һаҡлағыстарға яҙыу рөхсәте"; +"purchase.product.donateAndUpgrade" = "Иғәнә һәм яңыртыу"; +"purchase.product.freeUpgrade" = "Түләүһеҙ яңыртыу"; +"purchase.product.lifetimeLicense" = "Ғүмерлек рөхсәтнамә"; +"purchase.product.lifetimeLicense.duration" = "бер тапҡыр"; +"purchase.product.pricing.free" = "Түләүһеҙ"; +"purchase.product.trial" = "30 көнлөк һынау"; +"purchase.product.trial.expirationDate" = "Ҡулланыу ваҡыты: %@"; +"purchase.product.trial.duration" = "30 көн"; +"purchase.product.yearlySubscription" = "Йыллыҡ яҙылыу"; +"purchase.product.yearlySubscription.duration" = "йыл һайын"; +"purchase.readOnlyMode.alert.title" = "Уҡыу ғына режимы"; +"purchase.readOnlyMode.alert.message" = "Cryptomator ҡушымтаһының тулы версияһын һуңыраҡ көйләүҙәрҙә аса һәм уны хәҙергә тик уҡыу режимында ғына ҡуллана алаһығыҙ."; +"purchase.restorePurchase.button" = "Һатып алыуҙы тергеҙеү"; +"purchase.restorePurchase.validTrialFound.alert.title" = "Һынау дауам ителде"; +"purchase.restorePurchase.validTrialFound.alert.message" = "Cryptomator-ҙың тулы версияһын хәҙер сикләүле ваҡытта ҡулланырға мөмкин. Һеҙҙең һынау ваҡыты %@- тамамлана. Шунан һуң да һаҡлағыстарығыҙға бары тик уҡыу режимында ғына инергә мөмкин буласаҡ."; +"purchase.restorePurchase.fullVersionFound.alert.title" = "Тергеҙеү уңышлы булды"; +"purchase.restorePurchase.fullVersionNotFound.alert.title" = "Тулы версия табылманы"; +"purchase.restorePurchase.fullVersionNotFound.alert.message" = "Элек һатып алынған, тергеҙелә алырлыҡ тулы версияны таба алманыҡ. Зинһар, икенсе вариантты һынап ҡарағыҙ."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Яңыртыу хоҡуғына эйә"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Cryptomator-ҙың иҫкерәк версияһынан яңыртырға тырышаһығыҙ шикелле. Был осраҡта уның урынына «Яңыртыу тәҡдиме» вариантын һайлағыҙ."; +"purchase.retry.button" = "Ҡабатла"; +"purchase.retry.footer" = "Булған продукттарҙы тейәп булмай."; +"purchase.title" = "Тулы версияны асыу"; +"purchase.unlockedFullVersion.message" = "Хәҙер һеҙ Cryptomator-ҙың тулы версияһын ҡуллана алаһығыҙ. Хәйерле шифрлауҙар!"; +"purchase.unlockedFullVersion.title" = "Рәхмәт"; +"purchase.error.unknown" = "Был ҡушымта билдәһеҙ сәбәптәр арҡаһында App Store-ҙа юҡ. Зинһар, һуңыраҡ ҡабатлап ҡарағыҙ.\n\nӘгәр был хата ҡабатланһа, йыһазды яңынан эшләтеп ҡарағыҙ йәки iOS көйләүҙәрендә Apple ID-ғыҙҙан сығығыҙ һәм кире керегеҙ."; + +"settings.title" = "Көйләүҙәр"; +"settings.aboutCryptomator" = "Cryptomator тураһында"; +"settings.aboutCryptomator.title" = "%@ версияһы (%@)"; +"settings.cacheSize" = "Кэш күләме"; +"settings.clearCache" = "Кэшты таҙартыу"; +"settings.cloudServices" = "Болот хеҙмәттәре"; +"settings.contact" = "Бәйләнеш"; +"settings.debugMode" = "Төҙөкләндереү режимы"; +"settings.debugMode.alert.message" = "Был режимда нескә мәғлүмәттәрҙең (мәҫәлән, файл исемдәре, юлдар) йыһаздың журнал файлына яҙылыуы ихтимал. Серһүҙҙәр, cookie-файлдар һ.б. тура алып ташлана.\n\nЯйлау (debug) режимын тиҙ арала ябырға онотмағыҙ."; +"settings.manageSubscriptions" = "Яҙылыуҙар менән идара итеү"; +"settings.rateApp" = "Ҡушымтаны баһала"; +"settings.sendLogFile" = "Журнал файлын ебәр"; +"settings.shortcutsGuide" = "Ҡыҫҡа юлдар гиды"; +"settings.unlockFullVersion" = "Тулы версияны асыу"; + +"snapshots.fileprovider.file1" = "/Иҫәп-хисап.numbers"; +"snapshots.fileprovider.file2" = "/Һуңғы презентация.key"; +"snapshots.fileprovider.file3" = "/Продукт трейлеры.mov"; +"snapshots.fileprovider.file4" = "/Тәҡдим.docx"; +"snapshots.fileprovider.file5" = "/Хисаплама.pdf"; +"snapshots.fileprovider.folder3" = "/Серле проект"; +"snapshots.fileprovider.folder2" = "Фактуралар"; +"snapshots.fileprovider.folder1" = "/Сертификаттар"; +"snapshots.main.vault1" = "/Эш"; +"snapshots.main.vault2" = "/Ғаилә"; +"snapshots.main.vault3" = "/Документтар"; +"snapshots.main.vault4" = "/Калифорния сәйәхәте"; + +"s3Authentication.displayName" = "Күренәсәк исем"; +"s3Authentication.accessKey" = "Инеү асҡысы (Access Key)"; +"s3Authentication.secretKey" = "Серле асҡыс (Secret Key)"; +"s3Authentication.existingBucket" = "Булған кәрзин (Bucket)"; +"s3Authentication.endpoint" = "Ос нөктә (Endpoint)"; +"s3Authentication.region" = "Регион"; +"s3Authentication.error.invalidCredentials" = "Хаталы иҫәп яҙмаһы мәғлүмәттәре."; +"s3Authentication.error.invalidEndpoint" = "Бирелгән ос нөктә URL-адресы форматына тап килмәй."; + +"trialStatus.active" = "Ғәмәлдә"; +"trialStatus.expired" = "Ваҡыты үткән"; + +"unlockVault.button.unlock" = "Биген ас"; +"unlockVault.button.unlockVia" = "%@ аша биген ас"; +"unlockVault.password.footer" = "\"%@\" серһүҙен яҙығыҙ."; +"unlockVault.enableBiometricalUnlock.switch" = "%@ ғәмәлдә"; +"unlockVault.enableBiometricalUnlock.footer" = "Һаҡлағысты серһүҙ менән асыу урынына уны %@ аша асырға мөмкин."; +"unlockVault.evaluatePolicy.reason" = "Һаҡлағысығыҙҙы асыу"; +"unlockVault.progress" = "Бикте асыу…"; + +"untrustedTLSCertificate.title" = "Ғәмәлдә булмаған TLS сертификаты"; +"untrustedTLSCertificate.message" = "\"%@\" TLS сертификаты ғәмәлдә түгел. Нисек булһа ла, уға ышанырға теләйһегеҙме?\n\nSHA-256: %@"; +"untrustedTLSCertificate.add" = "Ышан"; +"untrustedTLSCertificate.dismiss" = "Ышанмаҫҡа"; + +"upgrade.title" = "Яңыртыу тәҡдиме"; +"upgrade.notEligible.alert.title" = "Яңыртыу уңышһыҙ булды"; +"upgrade.notEligible.alert.message" = "Cryptomator йыһазығыҙға ҡуйылған элекке версияны таба алманы. Әгәр уны һатып алған булһағыҙ, App Store-ҙан яңынан күсереп алып һәм яңынан ҡабатлағыҙ."; +"upgrade.info" = "Беренсе версиянан Cryptomator-ға ышаныуығыҙ өсөн рәхмәт. Тоғро ҡулланыусы булараҡ, һеҙ бушлай яңыртыу хоҡуғына эйә."; + +"urlSession.error.httpError.401" = "Хаталы ҡулланыусы исеме һәм/йәки серһүҙ."; +"urlSession.error.httpError.403" = "Һоралған ресурсҡа хоҡуҡтар етмәй."; +"urlSession.error.httpError.404" = "Һоралған ресурс табылманы."; +"urlSession.error.httpError.405" = "Һоратыу методы маҡсат ресурс тарафынан терәкләнмәй."; +"urlSession.error.httpError.409" = "Һоратыу маҡсат ресурстың ағымдағы торошона тап килмәй."; +"urlSession.error.httpError.412" = "Маҡсатлы ресурсҡа инеү тыйылды."; +"urlSession.error.httpError.default" = "Селтәр бәйләнеше %ld статус коды менән уышһыҙ булды."; +"urlSession.error.unexpectedResponse" = "Көтөлмәгән селтәр яуабы килде."; + +"vaultAccountManager.error.vaultAccountAlreadyExists" = "Был һаҡлағыс өҫтәлгән инде."; + +"vaultDetail.button.changeVaultPassword" = "Серһүҙҙе үҙгәрт"; +"vaultDetail.button.lock" = "Биклә"; +"vaultDetail.button.moveVault" = "Күсер"; +"vaultDetail.button.removeVault" = "Һаҡлағыс исемлегенән алып ташла"; +"vaultDetail.button.renameVault" = "Исемен үҙгәрт"; +"vaultDetail.changePassword.footer" = "Һаҡлағысығыҙ өсөн үҙегеҙ генә белгән көслө серһүҙ һайлағыҙ һәм уны хәүефһеҙ урында тотоғоҙ."; +"vaultDetail.disabledBiometricalUnlock.footer" = "Әгәр һеҙ %@ ғәмәлдә булһа, һеҙҙең һаҡлағыс серһүҙегеҙ iOS-тың асҡыс сылбырында һаҡланасаҡ."; +"vaultDetail.enabledBiometricalUnlock.footer" = "Һаҡлағыс пароле, %@ аутентиклауы уңышһыҙ булғанда ғына кәрәк буласаҡ."; +"vaultDetail.info.footer.accessVault" = "Файлдар ҡушымтаһы аша һаҡлағысҡа инеү."; +"vaultDetail.info.footer.accountInfo" = "%@ исеме менән %@ аша инелгән."; +"vaultDetail.keepUnlocked.title" = "Асыҡ булыу ваҡыты"; +"vaultDetail.keepUnlocked.footer.off" = "Бикте асыу, Cryptomator Файлдар ҡушымтаһы тарафынан туҡтатылған ваҡытта кәрәк буласаҡ."; +"vaultDetail.keepUnlocked.footer.limitedDuration" = "Бикте асыу, һаҡлағыс %@ буш торған ваҡытта кәрәк буласаҡ."; +"vaultDetail.keepUnlocked.footer.unlimitedDuration" = "Ҡул менән бикләнмәһә, асыу талап ителмәйәсәк."; +"vaultDetail.locked.footer" = "Һаҡлағысығыҙ хәҙерге ваҡытта бикле."; +"vaultDetail.moveVault.detectedMasterkey.text" = "Cryptomator был урында булған һаҡлағыс тапты.\nҺаҡлағысты күсереү өсөн кире ҡайтып икенсе каталог һайлағыҙ."; +"vaultDetail.moveVault.progress" = "Күсереү…"; +"vaultDetail.removeVault.footer" = "Был һаҡлағысты исемлектән генә алып ташлай һәм шифрланған файлдарҙы юймай."; +"vaultDetail.renameVault.progress" = "Исемен үҙгәртеү…"; +"vaultDetail.unlocked.footer" = "Әлеге ваҡытта һаҡлағыс Файлдар ҡушымтаһында асылған."; +"vaultDetail.unlockVault.footer" = "\"%@\" серһүҙен iOS асҡыс сылбырында һаҡлау һәм %@ ҡулланыу өсөн серһүҙҙе яҙығыҙ."; + +"vaultList.header.title" = "Һаҡлағыстар"; +"vaultList.emptyList.message" = "Һаҡлағыс өҫтәү өсөн бында баҫығыҙ"; +"vaultList.remove.alert.title" = "Һаҡлағысты алып ташларғамы?"; +"vaultList.remove.alert.message" = "Был һаҡлағысты исемлектән генә алып ташлай. Шифрланған мәғлүмәттәр юйылмай. Һаҡлағысты һуңыраҡ яңынан өҫтәргә мөмкин."; + +"vaultProviderFactory.error.unsupportedVaultConfig" = "Һаҡлағыс конфигурацияһы терәкһеҙ. Cryptomator-ҙың һуңғы версияһын эшләтеүегеҙҙе тикшерегеҙ."; +"vaultProviderFactory.error.unsupportedVaultVersion" = "Һаҡлағыстың терәкһеҙ версияһы: %ld. Был һаҡлағыс Cryptomator-ҙың башҡа версияһы менән яһалған."; + +"webDAVAuthentication.httpConnection.alert.title" = "HTTPS ҡулланырғамы?"; +"webDAVAuthentication.httpConnection.alert.message" = "HTTP ҡулланыу хәүефһеҙ түгел. Уның урынына HTTPS ҡулланырға кәңәш итәбеҙ. Хәүефтәрҙе белһәгеҙ, HTTP менән дауам итә алаһығыҙ."; +"webDAVAuthentication.httpConnection.change" = "HTTPS ҡулланыу"; +"webDAVAuthentication.httpConnection.continue" = "HTTP ҡалһын"; + +"webDAVAuthenticator.error.unsupportedProtocol" = "Сервер WebDAV яраҡлы түгел кеүек. Дөрөҫ URL ҡулланғанығыҙҙы тикшерегеҙ."; +"webDAVAuthenticator.error.untrustedCertificate" = "Был серверҙың сертификаты ышанысһыҙ. Был WebDAV бәйләнешен яңынан өҫтәү кәрәк булыр."; + +"Retry Upload" = "Тейәүҙе ҡабатла"; +"Clear from Cache" = "Кэштан таҙала"; diff --git a/SharedResources/be.lproj/Localizable.strings b/SharedResources/be.lproj/Localizable.strings index 498e79b74..75256fb31 100644 --- a/SharedResources/be.lproj/Localizable.strings +++ b/SharedResources/be.lproj/Localizable.strings @@ -1,12 +1,15 @@ "common.button.cancel" = "Скасаваць"; "common.button.change" = "Змяніць"; +"common.button.choose" = "Абраць"; "common.button.close" = "Зачыніць"; "common.button.create" = "Стварыць"; "common.button.done" = "Файна"; "common.button.edit" = "Рэдагаваць"; "common.button.next" = "Далей"; "common.button.ok" = "Добра"; +"common.button.refresh" = "Абнавiць"; "common.button.remove" = "Выдаліць"; +"common.button.retry" = "Паспрабаваць ізноў"; "common.cells.password" = "Пароль"; "common.cells.url" = "URL"; "common.cells.username" = "Імя карыстальніка"; @@ -17,11 +20,16 @@ "addVault.createNewVault.chooseCloud.header" = "Дзе мусіць Cryptomator захоўваць зашыфраваныя файлы з тваёй скарбніцы?"; "addVault.openExistingVault.title" = "Адчыніць існуючую скарбніцу"; "fileProvider.error.unlockButton" = "Адамкнуць"; +"hubAuthentication.accessNotGranted" = "Тваёй прыладзе ў дадзены момант не дазволена мець доступ да гэтай скрабніцы. Запытайся ўладальніка скрабніцы за дазволам."; +"hubAuthentication.licenseExceeded" = "Твая інстанцыя Cryptomator Hub мае некарэктную ліцэнзію. Калі ласка, паведамі адміністратару Hub пра гэта, каб абнавіць альбо аднавіць ліцэнзію."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Назва прылады"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Каб атрымаць доступ да скарбніцы, твая прылада мусіць быць спраўджанай уладальнікам скарбніцы."; "keepUnlocked.alert.title" = "Ці замкнуць скарбніцу?"; "keepUnlocked.alert.confirm" = "Пацвердзіць і адразу замкнуць"; "keepUnlockedDuration.auto" = "Дазволіць iOS прымаць рашэнне самастойна"; "keepUnlockedDuration.auto.shortDisplayName" = "Аўтаматычна"; +"purchase.retry.button" = "Паспрабаваць ізноў"; "purchase.unlockedFullVersion.title" = "Шчыры дзякуй"; "settings.title" = "Налады"; @@ -34,5 +42,6 @@ "unlockVault.button.unlock" = "Адамкнуць"; "vaultDetail.button.changeVaultPassword" = "Змяніць пароль"; +"vaultDetail.button.moveVault" = "Перамясціць"; "vaultDetail.button.renameVault" = "Пераназваць"; "vaultDetail.keepUnlocked.title" = "Працягласць размыкання"; diff --git a/SharedResources/bg.lproj/Localizable.strings b/SharedResources/bg.lproj/Localizable.strings new file mode 100644 index 000000000..5dce12828 --- /dev/null +++ b/SharedResources/bg.lproj/Localizable.strings @@ -0,0 +1,52 @@ +"common.button.cancel" = "Отказ"; +"common.button.change" = "Променяне"; +"common.button.choose" = "Избор"; +"common.button.close" = "Затваряне"; +"common.button.create" = "Създаване"; +"common.button.done" = "Готово"; +"common.button.edit" = "Редактиране"; +"common.button.next" = "Напред"; +"common.button.refresh" = "Презареждане"; +"common.button.register" = "Регистриране"; +"common.button.remove" = "Премахване"; +"common.button.retry" = "Повторен опит"; +"common.cells.password" = "Парола"; +"common.cells.url" = "URL"; +"common.cells.username" = "Потребител"; + +"addVault.title" = "Добавяне на хранилище"; +"addVault.createNewVault.title" = "Ново хранилище"; +"addVault.createNewVault.setVaultName.cells.name" = "Наименование"; +"addVault.createNewVault.chooseCloud.header" = "Къде Криптоматор ще държи шифрованите файлове на хранилището?"; +"addVault.createNewVault.password.confirmPassword.alert.message" = "ВАЖНО: Ако забравите паролата няма начин да възстановите достъпа до данните."; +"addVault.openExistingVault.title" = "Отваряне на хранилище"; +"addVault.openExistingVault.downloadVault.progress" = "Изтегля се хранилище…"; +"fileProvider.error.unlockButton" = "Отключване"; + +"hubAuthentication.title" = "Хранилище на Hub"; +"hubAuthentication.accessNotGranted" = "Устройството не е упълномощено за достъп до това хранилище. Поискайте достъп от собственика."; +"hubAuthentication.licenseExceeded" = "Лиценза на екземпляра на Концентратора на Криптоматор който вие използвате е лиценз. Информирайте администратора на Концентратора, за да поднови или надгради лиценза."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Име на устройството"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Изглежда, че това е първи достъп до Hub от това устройство. За да го разпознаете при разрешаване на достъпа, трябва да му дадете име."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Вашият ключ за профила е необходим при вход от нови приложения или мрежови четци. Може да бъде намерен в профила."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Устройството е регистрирано"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "За да получи достъп до хранилището, устройството трябва да бъде упълномощено от собственика на хранилището."; +"hubAuthentication.requireAccountInit.alert.title" = "Необходимо е действие"; +"hubAuthentication.requireAccountInit.alert.message" = "За да продължите завършете необходимите стъпки в потребителския профил в Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Към профила"; +"purchase.retry.button" = "Повторен опит"; + +"settings.title" = "Настройки"; + +"s3Authentication.displayName" = "Показвано име"; +"s3Authentication.accessKey" = "Ключ за достъп"; +"s3Authentication.secretKey" = "Таен ключ"; +"s3Authentication.existingBucket" = "Bucket"; +"s3Authentication.endpoint" = "Крайна точка"; +"s3Authentication.region" = "Регион"; + +"unlockVault.button.unlock" = "Отключване"; + +"vaultDetail.button.changeVaultPassword" = "Промяна на парола"; +"vaultDetail.button.moveVault" = "Преместване"; +"vaultDetail.button.renameVault" = "Преименуване"; diff --git a/SharedResources/bn.lproj/Localizable.strings b/SharedResources/bn.lproj/Localizable.strings index cca673947..59ff54afb 100644 --- a/SharedResources/bn.lproj/Localizable.strings +++ b/SharedResources/bn.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "সক্রিয় করুন"; "common.button.next" = "পরবর্তী"; "common.button.ok" = "আচ্ছা"; +"common.button.refresh" = "রিফ্রেশ"; "common.button.remove" = "বাতিল"; "common.button.retry" = "পুনরায় চেষ্টা করুন"; "common.button.signOut" = "সাইন আউট"; diff --git a/SharedResources/ca.lproj/Localizable.strings b/SharedResources/ca.lproj/Localizable.strings index 4d581a11b..e1653121c 100644 --- a/SharedResources/ca.lproj/Localizable.strings +++ b/SharedResources/ca.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Activar"; "common.button.next" = "Següent"; "common.button.ok" = "D'acord"; +"common.button.refresh" = "Refresca"; "common.button.remove" = "Elimina"; "common.button.retry" = "Reintenta"; "common.button.signOut" = "Tanca la sessió"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "No s'ha proporcionat cap ruta. Proporcioneu una ruta vàlida que retorni una carpeta."; "getFolderIntent.error.noVaultSelected" = "No s'ha seleccionat cap caixa forta."; +"hubAuthentication.accessNotGranted" = "El vostre dispositiu no ha estat encara autoritzat a accedir a aquesta caixa forta. Demaneu autorització al propietari."; +"hubAuthentication.licenseExceeded" = "Aquest Cryptomator Hub no té una llicència vàlida. Informa si us plau a l'administrador perquè actualitzi o renovi la llicència."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nom del dispositiu"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Per a accedir a la caixa forta, el vostre dispositiu ha de ser autoritzat pel propietari de la caixa."; + "intents.saveFile.missingFile" = "El fitxer proporcionat no és vàlid."; "intents.saveFile.invalidFolder" = "La carpeta proporcionada no és vàlida."; "intents.saveFile.missingTemporaryFolder" = "No s'ha pogut crear la carpeta temporal."; diff --git a/SharedResources/cs.lproj/Localizable.strings b/SharedResources/cs.lproj/Localizable.strings index 8faca291c..896b5ac67 100644 --- a/SharedResources/cs.lproj/Localizable.strings +++ b/SharedResources/cs.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Povolit"; "common.button.next" = "Další"; "common.button.ok" = "OK"; +"common.button.refresh" = "Načíst znovu"; "common.button.remove" = "Odstranit"; "common.button.retry" = "Opakovat"; "common.button.signOut" = "Odhlásit se"; @@ -101,15 +102,23 @@ "fileProvider.error.unlockButton" = "Odemknout"; "fileProvider.clearFileFromCache.title" = "Smazat soubor z mezipaměti"; "fileProvider.clearFileFromCache.message" = "Pouze smaže soubor z Vašeho zařízení a ponechá soubor v cloudu."; +"fileProvider.fileImporting.error.missingPremium" = "Odemkněte plnou verzi aplikace Cryptomator a získejte přístup k zápisu do vašeho trezoru."; "fileProvider.uploadProgress.connecting" = "Připojování…"; "fileProvider.uploadProgress.message" = "Aktuální průběh: %@\n\nPokud se domníváte, že proces nahrávání je zaseknutý, můžete zkusit nahrávání znovu."; "fileProvider.uploadProgress.missing" = "Průběh nebylo možné zjistit. Může nadále pokračovat na pozadí."; "fileProvider.uploadProgress.title" = "Nahrávání…"; "fileProvider.uploadProgress.missingDomainError" = "Doména nebyla nalezena."; + +"getFolderIntent.error.missingPath" = "Nebyla zadána žádná cesta. Uveďte prosím platnou cestu, pro kterou má být složka vrácena."; "getFolderIntent.error.noVaultSelected" = "Nebyl vybrán žádný trezor."; +"hubAuthentication.accessNotGranted" = "Vaše zařízení dosud nebylo oprávněno k přístupu k tomuto trezoru. Požádejte vlastníka trezoru, aby jej autorizoval."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Název zařízení"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Pro přístup k trezoru musí být vaše zařízení autorizováno majitelem trezoru."; + "intents.saveFile.missingFile" = "Zadaný soubor není platný."; "intents.saveFile.invalidFolder" = "Zadaná složka není platná."; "intents.saveFile.missingTemporaryFolder" = "Vytvoření dočasné složky se nezdařilo."; +"intents.saveFile.lockedVault" = "Musíte odemknout váš trezor, abyste mohli používat tuto zkratku."; "intents.saveFile.selectedVaultNotFound" = "Vybraný trezor nebyl nalezen."; "keepUnlocked.alert.title" = "Zamknout trezor?"; @@ -209,6 +218,7 @@ "s3Authentication.endpoint" = "Koncový bod"; "s3Authentication.region" = "Oblast"; "s3Authentication.error.invalidCredentials" = "Neplatné přihlašovací údaje."; +"s3Authentication.error.invalidEndpoint" = "Zadaný endpoint neodpovídá formátu URL."; "trialStatus.active" = "Aktivní"; "trialStatus.expired" = "Expirováno"; diff --git a/SharedResources/da.lproj/Localizable.strings b/SharedResources/da.lproj/Localizable.strings index b16a04c8d..8d789792e 100644 --- a/SharedResources/da.lproj/Localizable.strings +++ b/SharedResources/da.lproj/Localizable.strings @@ -15,18 +15,19 @@ "common.button.confirm" = "Bekræft"; "common.button.create" = "Opret"; "common.button.createFolder" = "Opret mappe"; -"common.button.done" = "Ferdig"; +"common.button.done" = "Færdig"; "common.button.download" = "Download"; "common.button.edit" = "Redigér"; "common.button.enable" = "Aktivér"; "common.button.next" = "Næste"; "common.button.ok" = "OK"; +"common.button.refresh" = "Opdatér"; "common.button.remove" = "Fjern"; "common.button.retry" = "Forsøg igen"; "common.button.signOut" = "Log ud"; "common.button.verify" = "Bekræft"; "common.cells.openInFilesApp" = "Åbn i Filer appen"; -"common.cells.password" = "Passord"; +"common.cells.password" = "Adgangskode"; "common.cells.url" = "URL"; "common.cells.username" = "Brugernavn"; "common.footer.learnMore" = "Læs mere."; @@ -37,13 +38,13 @@ "accountList.signOut.alert.title" = "Fjern associerede bokse?"; "accountList.signOut.alert.message" = "Ved at logge ud, vil alle tilknyttede bokse blive fjernet fra listen. Ingen krypterede data vil blive slettet. Du kan logge ind igen og tilføje boksene igen senere."; -"addVault.title" = "Legg til kvelv"; -"addVault.createNewVault.title" = "Opprett ein ny kvelv"; +"addVault.title" = "Tilføj boks"; +"addVault.createNewVault.title" = "Opret ny boks"; "addVault.createNewVault.purchase" = "Oprettelse af en ny boks kræver den fulde version af Cryptomator."; "addVault.createNewVault.setVaultName.header.title" = "Vælg et navn til boksen."; -"addVault.createNewVault.setVaultName.cells.name" = "Namn på kvelven"; +"addVault.createNewVault.setVaultName.cells.name" = "Boks-navn"; "addVault.createNewVault.setVaultName.error.emptyVaultName" = "Boks navn må ikke være tomt."; -"addVault.createNewVault.chooseCloud.header" = "Kvar skal Cryptomator lagra dei krypterte filene i kvelven din?"; +"addVault.createNewVault.chooseCloud.header" = "Hvor skal Cryptomator gemme de krypterede filer af din boks?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" findes allerede på dette sted. Vælg et andet navn eller en anden placering."; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator detekterede en eksisterende boks på denne placering.\nFor at oprette en ny boks, skal du gå tilbage og vælge en anden mappe."; "addVault.createNewVault.password.enterPassword.header" = "Indtast en ny adgangskode."; @@ -54,7 +55,7 @@ "addVault.createNewVault.password.error.nonMatchingPasswords" = "Adgangskoderne er ikke ens."; "addVault.createNewVault.password.error.tooShortPassword" = "Adgangskoden skal indeholde mindst 8 tegn."; "addVault.createNewVault.progress" = "Opretter boks…"; -"addVault.openExistingVault.title" = "Opn ein eksisterande kvelv"; +"addVault.openExistingVault.title" = "Open eksisterende boks"; "addVault.openExistingVault.chooseCloud.header" = "Hvor er boksen placeret?"; "addVault.openExistingVault.detectedMasterkey.text" = "Kryptomator fandt boksen \"%@\".\nVil du tilføje denne boks?"; "addVault.openExistingVault.detectedMasterkey.add" = "Tilføj denne boks"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "Ingen sti blev angivet. Angiv venligst en gyldig sti til en mappe."; "getFolderIntent.error.noVaultSelected" = "Ingen boks er valgt."; +"hubAuthentication.accessNotGranted" = "Din enhed er endnu ikke blevet godkendt til at få adgang til denne boks. Spørg boks-ejeren om godkendelse."; +"hubAuthentication.licenseExceeded" = "Din Cryptomator Hub har en ugyldig licens. Få venligst en Hub administrator til at opgradere eller forny licensen."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Enheds-navn"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "For at tilgå boksen, skal din enhed godkendes af boks-ejeren."; + "intents.saveFile.missingFile" = "Den angivne fil er ikke gyldig."; "intents.saveFile.invalidFolder" = "Den angivne mappe er ikke gyldig."; "intents.saveFile.missingTemporaryFolder" = "Kunne ikke oprette midlertidig mappe."; @@ -247,7 +253,7 @@ "vaultAccountManager.error.vaultAccountAlreadyExists" = "Du har allerede tilføjet denne boks."; -"vaultDetail.button.changeVaultPassword" = "Byt passord"; +"vaultDetail.button.changeVaultPassword" = "Skift adgangskode"; "vaultDetail.button.lock" = "Lås nu"; "vaultDetail.button.moveVault" = "Flyt"; "vaultDetail.button.removeVault" = "Fjern fra listen over bokse"; diff --git a/SharedResources/de.lproj/Localizable.strings b/SharedResources/de.lproj/Localizable.strings index 8af61043c..221a89aec 100644 --- a/SharedResources/de.lproj/Localizable.strings +++ b/SharedResources/de.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Aktivieren"; "common.button.next" = "Weiter"; "common.button.ok" = "OK"; +"common.button.refresh" = "Aktualisieren"; +"common.button.register" = "Registrieren"; "common.button.remove" = "Entfernen"; "common.button.retry" = "Wiederholen"; "common.button.signOut" = "Ausloggen"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Wo befindet sich der Tresor?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator hat den Tresor „%@“ erkannt.\nMöchtest du diesen Tresor hinzufügen?"; "addVault.openExistingVault.detectedMasterkey.add" = "Diesen Tresor hinzufügen"; +"addVault.openExistingVault.downloadVault.progress" = "Tresor wird heruntergeladen…"; "addVault.openExistingVault.password.footer" = "Gib das Passwort für „%@“ ein."; "addVault.openExistingVault.progress" = "Tresor wird hinzugefügt …"; "addVault.success.info" = "Tresor „%@“ erfolgreich hinzugefügt.\nGreife auf diesen Tresor über die App „Dateien“ zu."; @@ -80,7 +83,7 @@ "cloudProvider.error.itemNotFound" = "„%@“ konnte nicht gefunden werden."; "cloudProvider.error.itemAlreadyExists" = "„%@“ existiert bereits."; -"cloudProvider.error.itemTypeMismatch" = "\"%@\" hat einen unerwarteten Elementtyp."; +"cloudProvider.error.itemTypeMismatch" = "„%@“ hat einen unerwarteten Dateityp."; "cloudProvider.error.parentFolderDoesNotExist" = "Übergeordneter Ordner „%@“ existiert nicht."; "cloudProvider.error.pageTokenInvalid" = "Abrufen von Verzeichnisinhalten konnte nicht fortgesetzt werden."; "cloudProvider.error.quotaInsufficient" = "Dein Speicher hat nicht genügend Platz."; @@ -108,9 +111,22 @@ "fileProvider.uploadProgress.title" = "Wird hochgeladen …"; "fileProvider.uploadProgress.missingDomainError" = "Domain konnte nicht gefunden werden."; -"getFolderIntent.error.missingPath" = "Es wurde kein Pfad angegeben. Bitte gib einen gültigen Pfad zu einem Ordner an."; +"getFolderIntent.error.missingPath" = "Es wurde kein Pfad angegeben. Bitte geben Sie einen gültigen Pfad an, für den ein Ordner angegeben werden soll."; "getFolderIntent.error.noVaultSelected" = "Es wurde kein Tresor ausgewählt."; -"intents.saveFile.missingFile" = "Die angegebene Datei ist ungültig."; + +"hubAuthentication.title" = "Hubtresor"; +"hubAuthentication.accessNotGranted" = "Dein Gerät wurde noch nicht für den Zugriff auf diesen Tresor autorisiert. Bitte den Tresorbesitzer, dein Gerät zu autorisieren."; +"hubAuthentication.licenseExceeded" = "Die Lizenz deiner Cryptomator-Hub-Instanz ist ungültig. Bitte informiere deinen Hub-Administrator, um die Lizenz zu erweitern oder zu erneuern."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Gerätename"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Dies scheint der erste Hub-Zugriff von diesem Gerät zu sein. Um es für die Zugriffsberechtigung zu identifizieren, musst du diesem Gerät einen Namen geben."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Ihr Account Key ist erforderlich, um sich von neuen Anwendungen oder Browsern aus anzumelden. Sie können ihn in Ihrem Profil finden."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Gerät erfolgreich registriert"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Für Zugriff auf den Tresor muss dein Gerät vom Tresorbesitzer autorisiert werden."; +"hubAuthentication.requireAccountInit.alert.title" = "Handlung erforderlich"; +"hubAuthentication.requireAccountInit.alert.message" = "Um fortzufahren, führen Sie bitte die erforderlichen Schritte in Ihrem Hub Benutzerprofil aus."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Zum Profil"; + +"intents.saveFile.missingFile" = "Die bereitgestellte Datei ist nicht gültig."; "intents.saveFile.invalidFolder" = "Der angegebene Ordner ist ungültig."; "intents.saveFile.missingTemporaryFolder" = "Erstellung eines temporären Ordners fehlgeschlagen."; "intents.saveFile.lockedVault" = "Du musst deinen Tresor entsperren, um diesen Kurzbefehl nutzen zu können."; diff --git a/SharedResources/el.lproj/Localizable.strings b/SharedResources/el.lproj/Localizable.strings index 69900ae7d..2c1eeef59 100644 --- a/SharedResources/el.lproj/Localizable.strings +++ b/SharedResources/el.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Ενεργοποίηση"; "common.button.next" = "Επόμενο"; "common.button.ok" = "ΟΚ"; +"common.button.refresh" = "Ανανέωση"; +"common.button.register" = "Εγγραφή"; "common.button.remove" = "Αφαίρεση"; "common.button.retry" = "Επανάληψη"; "common.button.signOut" = "Αποσύνδεση"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Πού βρίσκεται η κρυπτή;"; "addVault.openExistingVault.detectedMasterkey.text" = "Το Cryptomator εντόπισε την κρύπτη \"%@\".\nΘα θέλατε να προσθέσετε αυτή την κρύπτη;"; "addVault.openExistingVault.detectedMasterkey.add" = "Προσθήκη αυτής της κρύπτης"; +"addVault.openExistingVault.downloadVault.progress" = "Λήψη Κρύπτης…"; "addVault.openExistingVault.password.footer" = "Εισάγετε τον κωδικό για \"%@\"."; "addVault.openExistingVault.progress" = "Προσθήκη Κρύπτης…"; "addVault.success.info" = "Προστέθηκε με επιτυχία η κρύπτη \"%@\".\nΠρόσβαση σε αυτή την κρύπτη μέσω της εφαρμογής Αρχείων."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Δε δόθηκε καμία διαδρομή. Παρακαλώ δώστε μια έγκυρη διαδρομή για την οποία θα πρέπει να επιστραφεί ένας φάκελος."; "getFolderIntent.error.noVaultSelected" = "Δεν έχει επιλεγεί κρύπτη."; + +"hubAuthentication.title" = "Κρύπτη Hub"; +"hubAuthentication.accessNotGranted" = "Η συσκευή σας δεν έχει ακόμη εξουσιοδοτηθεί να έχει πρόσβαση σε αυτή την κρύπτη. Ζητήστε από τον κάτοχο της κρύπτης να την εξουσιοδοτήσει."; +"hubAuthentication.licenseExceeded" = "Η συνεδρία σας στο Cryptomator Hub έχει μη έγκυρη άδεια χρήσης. Ενημερώστε έναν διαχειριστή του Hub για να αναβαθμίσει ή να ανανεώσει την άδεια χρήσης."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Όνομα Συσκευής"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Αυτή φαίνεται να είναι η πρώτη πρόσβαση στο Hub από αυτήν τη συσκευή. Για να την αναγνωρίσετε για εξουσιοδότηση πρόσβασης, πρέπει να ονομάσετε αυτήν τη συσκευή."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Το Κλειδί Λογαριασμού σας απαιτείται για να συνδεθείτε από νέες εφαρμογές ή προγράμματα περιήγησης. Μπορείτε να το βρείτε στο προφίλ σας."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Επιτυχής Εγγραφή Συσκευής"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Για να αποκτήσετε πρόσβαση στην κρύπτη, η συσκευή σας πρέπει να είναι εξουσιοδοτημένη από τον κάτοχο της κρύπτης."; +"hubAuthentication.requireAccountInit.alert.title" = "Απαιτείται Ενέργεια"; +"hubAuthentication.requireAccountInit.alert.message" = "Για να συνεχίσετε, παρακαλούμε συμπληρώστε τα βήματα που απαιτούνται στο προφίλ χρήστη Hub σας."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Πηγαίνετε στο Προφίλ"; + "intents.saveFile.missingFile" = "Το παρεχόμενο αρχείο δεν είναι έγκυρο."; "intents.saveFile.invalidFolder" = "Ο παρεχόμενος φάκελος δεν είναι έγκυρος."; "intents.saveFile.missingTemporaryFolder" = "Αποτυχία δημιουργίας προσωρινού φακέλου."; diff --git a/SharedResources/es.lproj/Localizable.strings b/SharedResources/es.lproj/Localizable.strings index 95f5e754b..d94a1104a 100644 --- a/SharedResources/es.lproj/Localizable.strings +++ b/SharedResources/es.lproj/Localizable.strings @@ -19,8 +19,10 @@ "common.button.download" = "Descargar"; "common.button.edit" = "Editar"; "common.button.enable" = "Activar"; -"common.button.next" = "Continuar"; +"common.button.next" = "Siguiente"; "common.button.ok" = "Aceptar"; +"common.button.refresh" = "Recargar"; +"common.button.register" = "Registrarse"; "common.button.remove" = "Eliminar"; "common.button.retry" = "Reintentar"; "common.button.signOut" = "Cerrar sesión"; @@ -40,14 +42,14 @@ "addVault.title" = "Añadir bóveda"; "addVault.createNewVault.title" = "Crear bóveda nueva"; "addVault.createNewVault.purchase" = "Crear una bóveda nueva requiere la versión completa de Cryptomator."; -"addVault.createNewVault.setVaultName.header.title" = "Elegir un nombre para la bóveda."; +"addVault.createNewVault.setVaultName.header.title" = "Elija un nombre para la bóveda."; "addVault.createNewVault.setVaultName.cells.name" = "Nombre de la bóveda"; "addVault.createNewVault.setVaultName.error.emptyVaultName" = "El nombre de la bóveda no puede estar vacío."; "addVault.createNewVault.chooseCloud.header" = "¿Dónde se deben almacenar los archivos cifrados de la bóveda?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" ya existe en esta ubicación. Elija un nombre o ubicación diferente."; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator detectó una bóveda existente en esta ubicación.\nPara crear una bóveda nueva, por favor vuelva atrás y elija una carpeta diferente."; "addVault.createNewVault.password.enterPassword.header" = "Ingrese una contraseña nueva."; -"addVault.createNewVault.password.confirmPassword.header" = "Confirmar la contraseña nueva."; +"addVault.createNewVault.password.confirmPassword.header" = "Confirme la contraseña nueva."; "addVault.createNewVault.password.confirmPassword.alert.title" = "¿Confirmar contraseña?"; "addVault.createNewVault.password.confirmPassword.alert.message" = "IMPORTANTE: si olvida su contraseña no habrá manera de recuperar los datos."; "addVault.createNewVault.password.error.emptyPassword" = "La contraseña no puede estar vacía."; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "¿Dónde se ubica la bóveda?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator detectó la bóveda \"%@\".\n¿Desea añadir esta bóveda?"; "addVault.openExistingVault.detectedMasterkey.add" = "Añadir esta bóveda"; +"addVault.openExistingVault.downloadVault.progress" = "Descargando bóveda…"; "addVault.openExistingVault.password.footer" = "Ingresar contraseña para \"%@\"."; "addVault.openExistingVault.progress" = "Añadiendo bóveda…"; "addVault.success.info" = "Se ha añadido correctamente la bóveda \"%@\".\nAcceda a esta bóveda desde la aplicación de archivos."; @@ -69,7 +72,7 @@ "changePassword.error.invalidOldPassword" = "La contraseña actual es incorrecta. Intente de nuevo."; "changePassword.header.currentPassword.title" = "Ingrese la contraseña actual."; "changePassword.header.newPassword.title" = "Ingrese una contraseña nueva."; -"changePassword.header.newPasswordConfirmation.title" = "Confirmar la contraseña nueva."; +"changePassword.header.newPasswordConfirmation.title" = "Confirme la contraseña nueva."; "changePassword.progress" = "Cambiando contraseña…"; "chooseFolder.emptyFolder.footer" = "La carpeta está vacía"; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "No se ha proporcionado ninguna ruta. Proporcione una ruta válida para la que se debe devolver una carpeta."; "getFolderIntent.error.noVaultSelected" = "No se ha seleccionado una bóveda."; + +"hubAuthentication.title" = "Bóveda de Hub"; +"hubAuthentication.accessNotGranted" = "Su dispositivo aún no ha sido autorizado para acceder a esta bóveda. Pídale al propietario de la bóveda que lo autorice."; +"hubAuthentication.licenseExceeded" = "Su instancia del Hub de Cryptomator tiene una licencia inválida. Informe a un administrador del Hub para actualizar o renovar la licencia."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nombre del dispositivo"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Este parece ser el primer acceso al Hub desde este dispositivo. Para identificarlo y autorizar el acceso, necesita nombrar este dispositivo."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Se requiere su clave de cuenta para iniciar sesión desde nuevas aplicaciones o navegadores. Puede encontrarse en su perfil."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Registro del dispositivo exitoso"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Para acceder a la bóveda, su dispositivo debe ser autorizado por el propietario de la bóveda."; +"hubAuthentication.requireAccountInit.alert.title" = "Acción requerida"; +"hubAuthentication.requireAccountInit.alert.message" = "Para continuar, complete los pasos necesarios en su perfil de usuario de Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Ir al Perfil"; + "intents.saveFile.missingFile" = "El archivo proporcionado es inválido."; "intents.saveFile.invalidFolder" = "La carpeta proporcionada es inválida."; "intents.saveFile.missingTemporaryFolder" = "Error al crear la carpeta temporal."; @@ -209,8 +225,8 @@ "s3Authentication.displayName" = "Nombre para mostrar"; "s3Authentication.accessKey" = "Clave de acceso"; "s3Authentication.secretKey" = "Clave secreta"; -"s3Authentication.existingBucket" = "Cubeta existente"; -"s3Authentication.endpoint" = "Punto final"; +"s3Authentication.existingBucket" = "Bucket existente"; +"s3Authentication.endpoint" = "Punto de enlace"; "s3Authentication.region" = "Región"; "s3Authentication.error.invalidCredentials" = "Credenciales inválidas."; "s3Authentication.error.invalidEndpoint" = "El punto final proporcionado no coincide con el formato de una URL."; diff --git a/SharedResources/fa.lproj/Localizable.strings b/SharedResources/fa.lproj/Localizable.strings index 81009d812..71b04e241 100644 --- a/SharedResources/fa.lproj/Localizable.strings +++ b/SharedResources/fa.lproj/Localizable.strings @@ -6,6 +6,7 @@ "common.button.done" = "انجام شده"; "common.button.edit" = "ویرایش"; "common.button.next" = "بعدی"; +"common.button.refresh" = "نوسازی"; "common.button.remove" = "حذف"; "common.button.retry" = "تلاش مجدد"; "common.cells.url" = "آدرس اینترنتی"; @@ -20,8 +21,12 @@ "purchase.retry.button" = "تلاش مجدد"; "settings.title" = "تنظیمات"; + +"s3Authentication.displayName" = "نام"; "s3Authentication.accessKey" = "کلید دسترسی"; "s3Authentication.secretKey" = "کلید مخفی"; +"s3Authentication.existingBucket" = "صندوقچه فعلی"; +"s3Authentication.endpoint" = "نقطه انتهایی"; "s3Authentication.region" = "منطقه"; "unlockVault.button.unlock" = "بازکردن قفل"; diff --git a/SharedResources/fi.lproj/Localizable.strings b/SharedResources/fi.lproj/Localizable.strings new file mode 100644 index 000000000..b483434b9 --- /dev/null +++ b/SharedResources/fi.lproj/Localizable.strings @@ -0,0 +1,20 @@ +"common.button.cancel" = "Peruuta"; +"common.button.change" = "Muuta"; +"common.button.close" = "Sulje"; +"common.button.done" = "Valmis"; +"common.button.next" = "Seuraava"; + +"addVault.title" = "Lisää Vault"; +"addVault.createNewVault.title" = "Luo Uusi Vault"; +"addVault.createNewVault.setVaultName.cells.name" = "Vault Nimi"; +"addVault.createNewVault.chooseCloud.header" = "Missä pitäisi Cryptomator tallentaa salattuja tiedostoja Vault?"; +"addVault.openExistingVault.title" = "Avaa Olemassaoleva Vault"; +"fileProvider.error.unlockButton" = "Avaa"; +"hubAuthentication.accessNotGranted" = "Laitteellasi ei ole pääsyvaltuutusta tähän holviin. Pyydä holvin omistajaa lisäämän valtuutus laitteellesi."; +"hubAuthentication.licenseExceeded" = "Cryptomator Hub:illasi ei ole voimassa olevaa lisenssiä. Ole hyvä ja ilmoita Hubin järjestelmänvalvojalle lisenssin päivittämiseksi tai sen uusimiseksi."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Laitteen Nimi"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Käyttääksesi holvia, holvin omistajan on valtuutettava laitteesi."; + +"unlockVault.button.unlock" = "Avaa"; + +"vaultDetail.button.changeVaultPassword" = "Vaihda salasana"; diff --git a/SharedResources/fil.lproj/Localizable.strings b/SharedResources/fil.lproj/Localizable.strings index bafcdc5c3..98a7ecc96 100644 --- a/SharedResources/fil.lproj/Localizable.strings +++ b/SharedResources/fil.lproj/Localizable.strings @@ -1,37 +1,295 @@ +/* + Localizable.strings + Cryptomator + + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"common.alert.error.title" = "Error"; "common.alert.attention.title" = "Atensyon"; "common.button.cancel" = "Kanselahin"; "common.button.change" = "Baguhin"; "common.button.choose" = "Pumili"; +"common.button.clear" = "Maaliwalas"; "common.button.close" = "Isara"; +"common.button.confirm" = "Kumpirmahin"; "common.button.create" = "Gumawa"; +"common.button.createFolder" = "Gumawa ng Folder"; "common.button.done" = "Tapos na"; +"common.button.download" = "I-download"; "common.button.edit" = "I-edit"; "common.button.enable" = "I-enable"; "common.button.next" = "Sunod"; "common.button.ok" = "OK"; +"common.button.refresh" = "I-refresh"; "common.button.remove" = "Tanggalin"; "common.button.retry" = "Subukan muli"; +"common.button.signOut" = "Mag-sign Out"; +"common.button.verify" = "I-verify"; +"common.cells.openInFilesApp" = "Ibuksan sa Files App"; +"common.cells.password" = "Password"; "common.cells.url" = "URL"; "common.cells.username" = "Username"; +"common.footer.learnMore" = "Matuto pa."; +"common.hud.authenticating" = "Pinapatunayan…"; + +"accountList.header.title" = "Mga pagpapatunay"; +"accountList.emptyList.message" = "Pumindot dito para madagdag ang account"; +"accountList.signOut.alert.title" = "Itangal ang mga associated na vaults?"; +"accountList.signOut.alert.message" = "Sa pamamagitan ng pag-sign out, ang lahat ng nauugnay na vault ay aalisin sa listahan ng vault. Walang matatanggal na naka-encrypt na data. Maaari kang mag-sign in muli at muling idagdag ang mga vault sa ibang pagkakataon."; "addVault.title" = "Magdagdag ng Vault"; "addVault.createNewVault.title" = "Gumawa ng Bagong Vault"; +"addVault.createNewVault.purchase" = "Ang paggawa ng bagong vault ay nangangailangan ng buong bersyon ng Cryptomator."; +"addVault.createNewVault.setVaultName.header.title" = "Pumili ng pangalan para sa vault."; "addVault.createNewVault.setVaultName.cells.name" = "Pangalan ng Vault"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "Hindi pwedeng wala ang pangalan ng vault."; "addVault.createNewVault.chooseCloud.header" = "Saan maaaring ilagay ng Cryptomator ang mga encrypted files ng iyong vault?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "Umiiral na ang \"%@\" sa lokasyong ito. Pumili ng ibang pangalan o lokasyon ng vault."; +"addVault.createNewVault.detectedMasterkey.text" = "Nakakita ang Cryptomator ng umiiral nang vault sa lokasyong ito.\nUpang gumawa ng bagong vault, mangyaring bumalik at pumili ng ibang folder."; +"addVault.createNewVault.password.enterPassword.header" = "Maglagay ng bagong password."; +"addVault.createNewVault.password.confirmPassword.header" = "Kumpirmahin ang bagong password."; +"addVault.createNewVault.password.confirmPassword.alert.title" = "Kumpirmahin ang Password?"; "addVault.createNewVault.password.confirmPassword.alert.message" = "IMPORTANTE: Walang paraan para maisalba ang iyong datos kapag nakalimutan ang iyong password."; +"addVault.createNewVault.password.error.emptyPassword" = "Hindi maaaring walang laman ang password."; +"addVault.createNewVault.password.error.nonMatchingPasswords" = "Hindi tugma ang mga password."; +"addVault.createNewVault.password.error.tooShortPassword" = "8 o higit pang karakter ang kailangan sa password."; +"addVault.createNewVault.progress" = "Gumagawa ng Vault…"; "addVault.openExistingVault.title" = "Magbukas ng Umiiral na Vault"; +"addVault.openExistingVault.chooseCloud.header" = "Saan matatagpuan ang vault?"; +"addVault.openExistingVault.detectedMasterkey.text" = "Natukoy ng Cryptomator ang vault na \"%@\".\nGusto mo bang idagdag ang vault na ito?"; +"addVault.openExistingVault.detectedMasterkey.add" = "Idagdag ang Vault na ito"; +"addVault.openExistingVault.password.footer" = "Ipasok ang password para sa \"%@\"."; +"addVault.openExistingVault.progress" = "Idinaragdag ang Vault…"; +"addVault.success.info" = "Matagumpay na naidagdag ang vault na \"%@\".\nI-access ang vault na ito sa pamamagitan ng Files app."; +"addVault.success.footer" = "Kung hindi mo pa nagagawa, paganahin ang Cryptomator sa Files app."; + +"biometryType.faceID" = "Face ID"; +"biometryType.touchID" = "Pindutin ang ID"; + +"changePassword.error.invalidOldPassword" = "Mali ang kasalukuyang password. Pakisubukang muli."; +"changePassword.header.currentPassword.title" = "Ipasok ang kasalukuyang password."; +"changePassword.header.newPassword.title" = "Maglagay ng bagong password."; +"changePassword.header.newPasswordConfirmation.title" = "Kumpirmahin ang bagong password."; +"changePassword.progress" = "Pinapalitan ang Password…"; + +"chooseFolder.emptyFolder.footer" = "Walang laman ang folder"; +"chooseFolder.createNewFolder.header.title" = "Pumili ng pangalan para sa folder."; +"chooseFolder.createNewFolder.cells.name" = "Pangalan ng Folder"; +"chooseFolder.createNewFolder.error.emptyFolderName" = "Hindi maaaring walang laman ang pangalan ng folder."; +"chooseFolder.createNewFolder.progress" = "Gumagawa ng Folder…"; + +"cloudProvider.error.itemNotFound" = "Hindi mahanap ang \"%@\"."; +"cloudProvider.error.itemAlreadyExists" = "\"%@\" mayroon na."; +"cloudProvider.error.itemTypeMismatch" = "Ang \"%@\" ay may hindi inaasahang uri ng item."; +"cloudProvider.error.parentFolderDoesNotExist" = "Ang folder ng magulang na \"%@\" ay hindi umiiral."; +"cloudProvider.error.pageTokenInvalid" = "Hindi maipagpatuloy ang pagkuha ng mga nilalaman ng direktoryo."; +"cloudProvider.error.quotaInsufficient" = "Walang sapat na espasyo ang iyong storage."; +"cloudProvider.error.unauthorized" = "Hindi magawa ang hindi awtorisadong operasyon."; +"cloudProvider.error.noInternetConnection" = "Kailangan ng koneksyon sa internet para sa operasyong ito."; + +"cloudProviderType.localFileSystem" = "Iba pang File Provider"; + +"fileProvider.onboarding.title" = "Maligayang pagdating"; +"fileProvider.onboarding.info" = "Salamat sa pagpili sa Cryptomator para protektahan ang iyong mga file. Upang makapagsimula, pumunta sa pangunahing app at magdagdag ng vault."; +"fileProvider.onboarding.button.openCryptomator" = "Buksan ang Cryptomator"; +"fileProvider.error.biometricalAuthCanceled.title" = "Nakansela ang Unlock"; +"fileProvider.error.biometricalAuthCanceled.message" = "Hindi matagumpay ang pag-unlock sa pamamagitan ng %@. Pakisubukang muli."; +"fileProvider.error.biometricalAuthWrongPassword.title" = "Mali ang Password"; +"fileProvider.error.biometricalAuthWrongPassword.message" = "Mali ang password na na-save para sa %@. Pakisubukang muli at ilagay ang iyong password upang muling paganahin ang %@."; +"fileProvider.error.defaultLock.title" = "Kinakailangan ang I-unlock"; +"fileProvider.error.defaultLock.message" = "Upang ma-access at maipakita ang mga nilalaman ng iyong vault, kailangan itong i-unlock."; "fileProvider.error.unlockButton" = "I-unlock"; +"fileProvider.clearFileFromCache.title" = "I-clear ang File mula sa Cache"; +"fileProvider.clearFileFromCache.message" = "Inaalis lang nito ang lokal na file sa iyong device at hindi tinatanggal ang file sa cloud."; +"fileProvider.fileImporting.error.missingPremium" = "I-unlock ang buong bersyon sa Cryptomator app para magkaroon ng access sa pagsulat sa iyong mga vault."; +"fileProvider.uploadProgress.connecting" = "Kumokonekta…"; +"fileProvider.uploadProgress.message" = "Kasalukuyang Pag-unlad: %@\n\nKung napapansin mong natigil ang pag-usad ng pag-upload, maaari mong subukang muli ang pag-upload."; +"fileProvider.uploadProgress.missing" = "Hindi matukoy ang pag-unlad. Maaaring tumatakbo pa rin ito sa background."; +"fileProvider.uploadProgress.title" = "Ina-upload…"; +"fileProvider.uploadProgress.missingDomainError" = "Hindi mahanap ang domain."; + +"getFolderIntent.error.missingPath" = "Walang ibinigay na landas. Mangyaring magbigay ng wastong landas kung saan dapat ibalik ang isang folder."; +"getFolderIntent.error.noVaultSelected" = "Walang vault na napili."; +"hubAuthentication.accessNotGranted" = "Hindi pa pinahihintulutan ang iyong device na i-access ang vault na ito. Hilingin sa may-ari ng vault na pahintulutan ito."; +"hubAuthentication.licenseExceeded" = "Ang iyong Cryptomator Hub instance ay may di-wastong lisensya. Mangyaring ipagbigay-alam sa administrator ng Hub na mag-upgrade o mag-renew ng lisensya."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Pangalan ng device"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Para ma-access ang vault, kailangang pahintulutan ng may-ari ng vault ang iyong device."; + +"intents.saveFile.missingFile" = "Ang ibinigay na file ay hindi wasto."; +"intents.saveFile.invalidFolder" = "Ang ibinigay na folder ay hindi wasto."; +"intents.saveFile.missingTemporaryFolder" = "Nabigong gumawa ng pansamantalang folder."; +"intents.saveFile.lockedVault" = "Kailangan mong i-unlock ang iyong vault para magamit ang shortcut na ito."; +"intents.saveFile.selectedVaultNotFound" = "Ang naselect na vault ay hindi makita."; + +"keepUnlocked.alert.title" = "Lock Vault?"; +"keepUnlocked.alert.message" = "Kinakailangan ng pagbabagong ito na i-lock ang iyong vault upang magkabisa."; +"keepUnlocked.alert.confirm" = "Kumpirmahin at I-lock Ngayon"; +"keepUnlocked.header" = "Tukuyin kung gaano katagal mo gustong manatiling naka-unlock ang vault na ito kapag idle."; +"keepUnlocked.footer.auto" = "Ang pagpapasya sa iOS ay nangangahulugan na ang Cryptomator ay maaaring wakasan anumang oras upang magbakante ng memorya, na awtomatikong nagla-lock sa vault."; +"keepUnlocked.footer.on" = "Gamit ang napiling opsyon, kailangang maimbak ang isang kopya ng iyong key sa iOS keychain, hangga't naka-unlock ang vault."; +"keepUnlockedDuration.auto" = "Hayaang Awtomatikong Magpasya ang iOS"; +"keepUnlockedDuration.auto.shortDisplayName" = "Auto"; +"keepUnlockedDuration.indefinite" = "Walang katiyakan"; + +"localFileSystemAuthentication.createNewVault.header" = "Sa susunod na screen, piliin ang lokasyon ng storage para sa iyong bagong vault."; +"localFileSystemAuthentication.createNewVault.button" = "Piliin ang Lokasyon ng Storage"; +"localFileSystemAuthentication.createNewVault.error.detectedExistingVault" = "Mayroon nang vault sa lokasyong ito. Pakisubukang muli gamit ang ibang lokasyon ng storage."; +"localFileSystemAuthentication.openExistingVault.header" = "Sa susunod na screen, piliin ang folder ng iyong kasalukuyang vault."; +"localFileSystemAuthentication.openExistingVault.button" = "Piliin ang Vault Folder"; +"localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "Ang napiling folder ay hindi isang vault. Pakisubukang muli gamit ang ibang folder."; +"localFileSystemAuthentication.info.footer" = "Hindi sinusuportahan ng mga file provider na naka-gray out ang \"pagpili ng mga folder.\" Ito ay hindi isang limitasyon ng Cryptomator."; + +"maintenanceModeError.runningCloudTask" = "Hindi maisagawa ang operasyon dahil ang ibang mga pagpapatakbo sa background para sa vault na ito ay kailangang tapusin muna. Subukang muli mamaya."; + +"nameValidation.error.endsWithPeriod" = "Hindi ka maaaring gumamit ng pangalan na nagtatapos sa tuldok. Mangyaring pumili ng ibang pangalan."; +"nameValidation.error.endsWithSpace" = "Hindi ka maaaring gumamit ng pangalan na nagtatapos sa isang puwang. Mangyaring pumili ng ibang pangalan."; +"nameValidation.error.containsIllegalCharacter" = "Hindi ka maaaring gumamit ng pangalan na naglalaman ng \"%@\". Mangyaring pumili ng ibang pangalan."; + +"onboarding.title" = "Maligayang pagdating"; +"onboarding.info" = "Salamat sa pagpili sa Cryptomator para protektahan ang iyong mga file.\n\nSa Cryptomator, nasa iyong mga kamay ang susi sa iyong data. Ine-encrypt ng Cryptomator ang iyong data nang mabilis at madali.\n\nAng app na ito ay ganap na isinama sa Files app. Tiyaking paganahin ang Cryptomator sa Files app sa ibang pagkakataon upang ma-access ang iyong mga vault."; +"onboarding.button.continue" = "Magpatuloy"; + +"purchase.beginFreeTrial.alert.title" = "Na-unlock ang Pagsubok"; +"purchase.expiredTrial" = "Ang iyong pagsubok ay nag-expire na."; +"purchase.footer.privacyPolicy" = "Patakaran sa Privacy"; +"purchase.footer.termsOfUse" = "Mga Tuntunin ng Paggamit"; +"purchase.header.feature.familySharing" = "Pagbabahaginan ng pamilya"; +"purchase.header.feature.openSource" = "Pag-unlad ng open-source"; +"purchase.header.feature.writeAccess" = "Magsulat ng access sa iyong mga vault"; +"purchase.product.donateAndUpgrade" = "Mag-donate at Mag-upgrade"; +"purchase.product.freeUpgrade" = "Libreng Upgrade"; +"purchase.product.lifetimeLicense" = "Panghabambuhay na Lisensya"; +"purchase.product.lifetimeLicense.duration" = "isang beses"; +"purchase.product.pricing.free" = "Libre"; +"purchase.product.trial" = "30-Araw na Pagsubok"; +"purchase.product.trial.expirationDate" = "Petsa ng pagkawalang bisa: %@"; +"purchase.product.trial.duration" = "sa loob ng 30 araw"; +"purchase.product.yearlySubscription" = "Taunang Subskripsyon"; +"purchase.product.yearlySubscription.duration" = "taun-taon"; +"purchase.readOnlyMode.alert.title" = "Read-Only Mode"; +"purchase.readOnlyMode.alert.message" = "Maaari mong i-unlock ang buong bersyon ng Cryptomator sa ibang pagkakataon sa mga setting at gamitin ito sa read-only na mode sa ngayon."; +"purchase.restorePurchase.button" = "Ibalik ang Pagbili"; +"purchase.restorePurchase.validTrialFound.alert.title" = "Ipinagpatuloy ang Pagsubok"; +"purchase.restorePurchase.validTrialFound.alert.message" = "Magagamit mo na ngayon ang buong bersyon ng Cryptomator sa limitadong panahon. Mag-e-expire ang iyong trial sa %@. Pagkatapos nito, maa-access pa rin ang iyong mga vault sa read-only mode."; +"purchase.restorePurchase.fullVersionFound.alert.title" = "Ibalik ang Matagumpay"; +"purchase.restorePurchase.fullVersionNotFound.alert.title" = "Walang Buong Bersyon"; +"purchase.restorePurchase.fullVersionNotFound.alert.message" = "Hindi namin mahanap ang isang dating binili na buong bersyon na maaaring ibalik. Mangyaring subukan ang isa pang pagpipilian."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Kwalipikado para sa Pag-upgrade"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Mukhang sinusubukan mong mag-upgrade mula sa isang mas lumang bersyon ng Cryptomator. Kung ganoon, mangyaring piliin ang opsyon na \"I-upgrade ang Alok\" sa halip."; "purchase.retry.button" = "Subukan muli"; +"purchase.retry.footer" = "Hindi ma-load ang mga available na produkto."; +"purchase.title" = "I-unlock ang Buong Bersyon"; +"purchase.unlockedFullVersion.message" = "Magagamit mo na ngayon ang buong bersyon ng Cryptomator. Maligayang pag-crypting!"; +"purchase.unlockedFullVersion.title" = "Salamat"; +"purchase.error.unknown" = "Hindi available ang pagbiling ito sa App Store sa hindi malamang dahilan. Pakisubukang muli sa ibang pagkakataon.\n\nKung magpapatuloy ang error na ito, subukang i-restart ang iyong device o mag-sign out at bumalik sa iyong Apple ID sa mga setting ng iOS."; "settings.title" = "Settings"; +"settings.aboutCryptomator" = "Tungkol sa Cryptomator"; +"settings.aboutCryptomator.title" = "Bersyon %@ (%@)"; +"settings.cacheSize" = "Laki ng Cache"; "settings.clearCache" = "Linisin ang Cache"; +"settings.cloudServices" = "Mga Serbisyo sa Cloud"; +"settings.contact" = "Makipag-ugnayan"; +"settings.debugMode" = "Debug Mode"; +"settings.debugMode.alert.message" = "Sa mode na ito, maaaring isulat ang sensitibong data sa isang log file sa iyong device (hal., mga filename at path). Ang mga password, cookies, atbp. ay tahasang hindi kasama.\n\nTandaang i-disable ang debug mode sa lalong madaling panahon."; +"settings.manageSubscriptions" = "Pamahalaan ang Subscription"; +"settings.rateApp" = "I-rate ang App"; +"settings.sendLogFile" = "Magpadala ng Log File"; +"settings.shortcutsGuide" = "Gabay sa Mga Shortcut"; +"settings.unlockFullVersion" = "I-unlock ang Buong Bersyon"; + +"snapshots.fileprovider.file1" = "/Accounting.numbers"; +"snapshots.fileprovider.file2" = "/Pangwakas na Presentasyon.key"; +"snapshots.fileprovider.file3" = "/Product Trailer.mov"; +"snapshots.fileprovider.file4" = "/Proposal.docx"; +"snapshots.fileprovider.file5" = "/ulat.pdf"; +"snapshots.fileprovider.folder3" = "/Lihim na Proyekto"; +"snapshots.fileprovider.folder2" = "/Lihim na Proyekto"; +"snapshots.fileprovider.folder1" = "/Mga Sertipiko"; +"snapshots.main.vault1" = "/Trabaho"; +"snapshots.main.vault2" = "/Pamilya"; +"snapshots.main.vault3" = "/Mga dokumento"; +"snapshots.main.vault4" = "/Paglalakbay sa California"; + +"s3Authentication.displayName" = "Display Name"; "s3Authentication.accessKey" = "Access Key"; +"s3Authentication.secretKey" = "Lihim na Susi"; +"s3Authentication.existingBucket" = "Umiiral na Balde"; +"s3Authentication.endpoint" = "Endpoint"; "s3Authentication.region" = "Rehiyon"; +"s3Authentication.error.invalidCredentials" = "Di-wastong mga kredensyal."; +"s3Authentication.error.invalidEndpoint" = "Ang ibinigay na endpoint ay hindi tumutugma sa format ng isang URL."; + +"trialStatus.active" = "Aktibo"; +"trialStatus.expired" = "Nag-expire na"; "unlockVault.button.unlock" = "I-unlock"; +"unlockVault.button.unlockVia" = "I-unlock sa pamamagitan ng %@"; +"unlockVault.password.footer" = "Ipasok ang password para sa \"%@\"."; +"unlockVault.enableBiometricalUnlock.switch" = "Paganahin ang %@"; +"unlockVault.enableBiometricalUnlock.footer" = "Sa halip na i-unlock ang iyong vault gamit ang iyong password, maaari mo itong i-unlock sa pamamagitan ng %@."; +"unlockVault.evaluatePolicy.reason" = "I-unlock ang iyong vault"; +"unlockVault.progress" = "Ina-unlock…"; + +"untrustedTLSCertificate.title" = "Di-wastong TLS Certificate"; +"untrustedTLSCertificate.message" = "Ang TLS Certificate ng \"%@\" ay hindi wasto. Gusto mo pa rin bang magtiwala dito?\n\n SHA-256: %@"; +"untrustedTLSCertificate.add" = "Magtiwala"; +"untrustedTLSCertificate.dismiss" = "Huwag Magtiwala"; + +"upgrade.title" = "I-upgrade ang Alok"; +"upgrade.notEligible.alert.title" = "Nabigo ang pag-upgrade"; +"upgrade.notEligible.alert.message" = "Hindi matukoy ng Cryptomator ang isang mas lumang bersyon na naka-install sa iyong device. Kung binili mo ito, mangyaring muling i-download ito mula sa App Store at subukang muli."; +"upgrade.info" = "Salamat sa pagtitiwala sa Cryptomator mula noong unang bersyon. Bilang isang tapat na user, karapat-dapat ka para sa isang libreng pag-upgrade."; + +"urlSession.error.httpError.401" = "Maling username at/o password."; +"urlSession.error.httpError.403" = "Hindi sapat na mga karapatan sa hiniling na mapagkukunan."; +"urlSession.error.httpError.404" = "Hindi nakita ang hiniling na mapagkukunan."; +"urlSession.error.httpError.405" = "Ang paraan ng kahilingan ay hindi sinusuportahan ng target na mapagkukunan."; +"urlSession.error.httpError.409" = "Humiling ng salungat sa kasalukuyang estado ng target na mapagkukunan."; +"urlSession.error.httpError.412" = "Tinanggihan ang pag-access sa target na mapagkukunan."; +"urlSession.error.httpError.default" = "Nabigo ang koneksyon sa network gamit ang status code na %ld."; +"urlSession.error.unexpectedResponse" = "Nagkaroon ng hindi inaasahang tugon sa network."; + +"vaultAccountManager.error.vaultAccountAlreadyExists" = "Naidagdag mo na ang vault na ito."; + +"vaultDetail.button.changeVaultPassword" = "Palitan ANG password"; +"vaultDetail.button.lock" = "I-lock ngayon"; "vaultDetail.button.moveVault" = "Ilipat"; +"vaultDetail.button.removeVault" = "Alisin sa Listahan ng Vault"; "vaultDetail.button.renameVault" = "Baguhin ang pangalan"; +"vaultDetail.changePassword.footer" = "Pumili ng malakas na password para sa iyong vault na ikaw lang ang nakakaalam at itago ito sa isang ligtas na lugar."; +"vaultDetail.disabledBiometricalUnlock.footer" = "Kung pinagana mo ang %@, maiimbak ang iyong password sa vault sa iOS keychain."; +"vaultDetail.enabledBiometricalUnlock.footer" = "Kakailanganin lang ang iyong password sa vault kung nabigo ang %@ authentication."; +"vaultDetail.info.footer.accessVault" = "I-access ang vault sa pamamagitan ng Files app."; +"vaultDetail.info.footer.accountInfo" = "Naka-log in bilang %@ sa pamamagitan ng %@."; +"vaultDetail.keepUnlocked.title" = "I-unlock ang Tagal"; +"vaultDetail.keepUnlocked.footer.off" = "Kakailanganin ang pag-unlock kapag ang Cryptomator ay winakasan ng Files app."; +"vaultDetail.keepUnlocked.footer.limitedDuration" = "Kakailanganin ang pag-unlock kapag ang iyong vault ay naging idle para sa %@."; +"vaultDetail.keepUnlocked.footer.unlimitedDuration" = "Walang pag-unlock ang kakailanganin maliban kung manu-manong naka-lock."; +"vaultDetail.locked.footer" = "Kasalukuyang naka-lock ang iyong vault."; +"vaultDetail.moveVault.detectedMasterkey.text" = "Nakakita ang Cryptomator ng umiiral nang vault sa lokasyong ito.\nUpang ilipat ang iyong vault, mangyaring bumalik at pumili ng ibang folder."; +"vaultDetail.moveVault.progress" = "Gumagalaw…"; +"vaultDetail.removeVault.footer" = "Aalisin lang nito ang vault sa listahan ng vault at hindi tatanggalin ang anumang naka-encrypt na file."; +"vaultDetail.renameVault.progress" = "Pinapalitan ang pangalan…"; +"vaultDetail.unlocked.footer" = "Kasalukuyang naka-unlock ang iyong vault sa Files app."; +"vaultDetail.unlockVault.footer" = "Ilagay ang password para sa \"%@\" para iimbak ito sa iOS keychain at para paganahin ang %@."; + +"vaultList.header.title" = "Mga Vault"; +"vaultList.emptyList.message" = "Mag-tap dito para magdagdag ng vault"; +"vaultList.remove.alert.title" = "Alisin ang Vault?"; +"vaultList.remove.alert.message" = "Aalisin lang nito ang vault sa listahan ng vault. Walang matatanggal na naka-encrypt na data. Maaari mong muling idagdag ang vault sa ibang pagkakataon."; + +"vaultProviderFactory.error.unsupportedVaultConfig" = "Hindi sinusuportahan ang configuration ng Vault. Pakitiyak na pinapatakbo mo ang pinakabagong bersyon ng Cryptomator."; +"vaultProviderFactory.error.unsupportedVaultVersion" = "Hindi sinusuportahan ang bersyon ng Vault na %ld. Ang vault na ito ay ginawa gamit ang mas luma o mas bagong bersyon ng Cryptomator."; "webDAVAuthentication.httpConnection.alert.title" = "Gamiting ang HTTPS?"; "webDAVAuthentication.httpConnection.alert.message" = "Ang pag-gamit ng HTTP ay hindi ligtas. Kung alam mo ang mga panganib, maaari kang tumuloy gamit ang HTTP."; "webDAVAuthentication.httpConnection.change" = "Gawing HTTPS"; +"webDAVAuthentication.httpConnection.continue" = "Panatilihin ang HTTP"; + +"webDAVAuthenticator.error.unsupportedProtocol" = "Mukhang hindi compatible sa WebDAV ang server. Pakisuri kung ginamit mo ang tamang URL."; +"webDAVAuthenticator.error.untrustedCertificate" = "Ang sertipiko ng server na ito ay hindi pinagkakatiwalaan. Maaaring kailanganin mong muling idagdag ang koneksyon sa WebDAV na ito."; + +"Retry Upload" = "Subukan muli ang Pag-upload"; +"Clear from Cache" = "I-clear mula sa Cache"; diff --git a/SharedResources/fr.lproj/Localizable.strings b/SharedResources/fr.lproj/Localizable.strings index dfd834b8b..c7bbe5b3c 100644 --- a/SharedResources/fr.lproj/Localizable.strings +++ b/SharedResources/fr.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Activer"; "common.button.next" = "Suivant"; "common.button.ok" = "OK"; +"common.button.refresh" = "Actualiser"; +"common.button.register" = "Inscription"; "common.button.remove" = "Retirer"; "common.button.retry" = "Réessayer"; "common.button.signOut" = "Se déconnecter"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Où se situe le coffre ?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator a détecté le coffre \"%@\".\nVoulez-vous ajouter ce coffre ?"; "addVault.openExistingVault.detectedMasterkey.add" = "Ajouter ce coffre"; +"addVault.openExistingVault.downloadVault.progress" = "Téléchargement du coffre…"; "addVault.openExistingVault.password.footer" = "Entrez le mot de passe pour “%@”."; "addVault.openExistingVault.progress" = "Ajout du Coffre-fort…"; "addVault.success.info" = "Le coffre \"%@\" a été ajouté avec succès.\nAccédez à ce coffre via l'application Fichiers."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Aucun chemin n'a été fourni. Veuillez fournir un chemin d'accès valide pour lequel un dossier doit être renvoyé."; "getFolderIntent.error.noVaultSelected" = "Aucun coffre n'a été sélectionné."; + +"hubAuthentication.title" = "Coffre dans Hub"; +"hubAuthentication.accessNotGranted" = "Votre appareil n'a pas encore été autorisé à accéder à ce coffre-fort. Demandez au propriétaire du coffre-fort de l'autoriser."; +"hubAuthentication.licenseExceeded" = "Votre instance Cryptomator Hub a une licence invalide. Veuillez informer un administrateur Hub pour la mettre à niveau ou la renouveler."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nom de l'appareil"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Il semble que ce soit le premier accès au Hub à partir de cet appareil. Afin de l'identifier pour l'autorisation d'accès, vous devez nommer cet appareil."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Votre clé de compte est requise pour vous connecter depuis de nouvelles applications ou de nouveaux navigateurs. Elle se trouve dans votre profil."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Enregistrement de l'appareil réussi"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Pour accéder au coffre-fort, votre appareil doit être autorisé par le propriétaire du coffre-fort."; +"hubAuthentication.requireAccountInit.alert.title" = "Action requise"; +"hubAuthentication.requireAccountInit.alert.message" = "Pour continuer, veuillez compléter les étapes requises dans votre profil d'utilisateur Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Aller au profil"; + "intents.saveFile.missingFile" = "Le fichier fourni n'est pas valide."; "intents.saveFile.invalidFolder" = "Le dossier fourni n'est pas valide."; "intents.saveFile.missingTemporaryFolder" = "Impossible de créer un dossier temporaire."; diff --git a/SharedResources/gl.lproj/Localizable.strings b/SharedResources/gl.lproj/Localizable.strings index ffc086db2..ad4546046 100644 --- a/SharedResources/gl.lproj/Localizable.strings +++ b/SharedResources/gl.lproj/Localizable.strings @@ -6,6 +6,7 @@ "common.button.done" = "Feito"; "common.button.edit" = "Editar"; "common.button.next" = "Seguinte"; +"common.button.refresh" = "Actualizar"; "common.button.remove" = "Eliminar"; "common.button.retry" = "Tentar de novo"; "common.cells.url" = "URL"; diff --git a/SharedResources/he.lproj/Localizable.strings b/SharedResources/he.lproj/Localizable.strings index f131f6aea..2528c08cf 100644 --- a/SharedResources/he.lproj/Localizable.strings +++ b/SharedResources/he.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "הפעל"; "common.button.next" = "הבא"; "common.button.ok" = "אישור"; +"common.button.refresh" = "רענן"; "common.button.remove" = "הסר"; "common.button.retry" = "לנסות שוב"; "common.button.signOut" = "התנתק"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "לא סופק נתיב. נא ספק נתיב תקין שממנו יש להחזיר תיקיה."; "getFolderIntent.error.noVaultSelected" = "לא נבחר קובץ vault."; +"hubAuthentication.accessNotGranted" = "המכשיר שלך טרם אושר לגשת לכספת הזאת. יש לבקש אישור גישה מבעל הכספת."; +"hubAuthentication.licenseExceeded" = "הרישיון שמותקן במופע ה- Cryptomator האב שלך אינו תקף. אנא ידע את מנהל ההאב שלך לשדרג או לחדש את הרישיון."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "שם מכשיר"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "כדי לגשת לכספת, המכשיר שלך צריך לקבל הרשאה על ידי בעלי הכספת."; + "intents.saveFile.missingFile" = "הקובץ שסופק אינו תקין."; "intents.saveFile.invalidFolder" = "התיקייה שנבחרה איננה תקינה."; "intents.saveFile.missingTemporaryFolder" = "כשלון ביצירת תיקיה זמנית."; diff --git a/SharedResources/hi.lproj/Localizable.strings b/SharedResources/hi.lproj/Localizable.strings index 720614402..4ca6c79d6 100644 --- a/SharedResources/hi.lproj/Localizable.strings +++ b/SharedResources/hi.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "सक्षम करें"; "common.button.next" = "अगला"; "common.button.ok" = "ठीक है"; +"common.button.refresh" = "रीफ्रेश करें"; "common.button.remove" = "हटाएँ"; "common.button.retry" = "पुन: प्रयास करें"; "common.button.signOut" = "साइन आउट करें"; @@ -87,6 +88,7 @@ "fileProvider.error.unlockButton" = "अनलॉक करें"; "fileProvider.clearFileFromCache.title" = "कैशे से फ़ाइल साफ़ करें"; "fileProvider.clearFileFromCache.message" = "यह केवल आपके डिवाइस से स्थानीय फ़ाइल को हटाता है और क्लाउड में फ़ाइल को नहीं हटाता"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "डिवाइस का नाम"; "localFileSystemAuthentication.createNewVault.header" = "अगले स्क्रीन पर अपने कक्ष के लिए भंडार स्थान का चयन करें।"; "localFileSystemAuthentication.createNewVault.button" = "संग्रहण का स्थान चयन करें"; diff --git a/SharedResources/hr.lproj/Localizable.strings b/SharedResources/hr.lproj/Localizable.strings index 972ac0d67..ef69f4e98 100644 --- a/SharedResources/hr.lproj/Localizable.strings +++ b/SharedResources/hr.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Omogući"; "common.button.next" = "Sljedeći"; "common.button.ok" = "U redu"; +"common.button.refresh" = "Osvježi"; "common.button.remove" = "Ukloni"; "common.button.retry" = "Pokušaj ponovno"; "common.button.signOut" = "Odjavi se"; @@ -109,6 +110,7 @@ "getFolderIntent.error.missingPath" = "Putanja nije navedena. Navedite valjanu putanju za koju treba vratiti mapu."; "getFolderIntent.error.noVaultSelected" = "Niti jedan trezor nije odabran."; + "intents.saveFile.missingFile" = "Datoteka nije ispravna."; "intents.saveFile.invalidFolder" = "Mapa nije ispravna."; "intents.saveFile.missingTemporaryFolder" = "Izrada privremene mape nije uspjela."; diff --git a/SharedResources/hu.lproj/Localizable.strings b/SharedResources/hu.lproj/Localizable.strings index 69c919ab6..dc87abdcc 100644 --- a/SharedResources/hu.lproj/Localizable.strings +++ b/SharedResources/hu.lproj/Localizable.strings @@ -1,32 +1,193 @@ +/* + Localizable.strings + Cryptomator + + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"common.alert.error.title" = "Hiba"; "common.alert.attention.title" = "Figyelem"; "common.button.cancel" = "Mégse"; "common.button.change" = "Változtat"; "common.button.choose" = "Választás"; +"common.button.clear" = "Törlés"; "common.button.close" = "Bezár"; +"common.button.confirm" = "Megerősítés"; "common.button.create" = "Létrehozás"; +"common.button.createFolder" = "Mappa létrehozása"; "common.button.done" = "Kész"; +"common.button.download" = "Letöltés"; "common.button.edit" = "Szerkeszt"; "common.button.enable" = "Engedélyezés"; "common.button.next" = "Következő"; "common.button.ok" = "OK"; +"common.button.refresh" = "Frissítés"; +"common.button.register" = "Regisztráció"; "common.button.remove" = "Eltávolítás"; "common.button.retry" = "Újra"; +"common.button.signOut" = "Kijelentkezés"; +"common.button.verify" = "Hitelesítés"; +"common.cells.openInFilesApp" = "Megnyitás ezzel: Fájlok"; "common.cells.password" = "Jelszó"; "common.cells.url" = "ULR"; "common.cells.username" = "Felhasználónév"; +"common.footer.learnMore" = "További információk."; +"common.hud.authenticating" = "Hitelesítés…"; + +"accountList.header.title" = "Hitelesítések"; +"accountList.emptyList.message" = "Koppintson ide egy fiók hozzáadásához"; +"accountList.signOut.alert.title" = "Hozzátartozó széfek eltávolítása?"; +"accountList.signOut.alert.message" = "A kijelentkezéssel az összes hozzátartozó széf eltávolításra kerül a listából. Semmilyen titkosított adat nem törlődik. Később újra bejelentkezhet és újra felveheti a széfeket."; "addVault.title" = "Széf hozzáadása"; "addVault.createNewVault.title" = "Új széf létrehozása"; +"addVault.createNewVault.purchase" = "Új széf létrehozásához meg kell vennie a Cryptomator teljes verzióját."; +"addVault.createNewVault.setVaultName.header.title" = "Válasszon egy nevet az új széf számára."; "addVault.createNewVault.setVaultName.cells.name" = "A széf neve"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "A széf neve nem lehet üres."; "addVault.createNewVault.chooseCloud.header" = "Hova mentse a Cryptomator a széf titkosított fájljait?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" nevű széf már létezik ezen a helyen. Válasszon másik nevet vagy helyet a széfnek."; +"addVault.createNewVault.detectedMasterkey.text" = "A Cryptomator talált egy már létező széfet ezen a helyen.\nÚj széf létrehozásához lépjen vissza és válasszon egy másik mappát."; +"addVault.createNewVault.password.enterPassword.header" = "Adjon meg egy új jelszót."; +"addVault.createNewVault.password.confirmPassword.header" = "Erősítse meg az új jelszót."; +"addVault.createNewVault.password.confirmPassword.alert.title" = "Jelszó megerősítése?"; "addVault.createNewVault.password.confirmPassword.alert.message" = "FONTOS: Ha elfelejti a jelszavát, nincs mód az adatok helyreállítására."; +"addVault.createNewVault.password.error.emptyPassword" = "A jelszó nem lehet üres."; +"addVault.createNewVault.password.error.nonMatchingPasswords" = "A jelszavak nem egyeznek."; +"addVault.createNewVault.password.error.tooShortPassword" = "A jelszónak legalább 8 karaktert kell tartalmaznia."; +"addVault.createNewVault.progress" = "Széf létrehozása…"; "addVault.openExistingVault.title" = "Meglévő széf megnyitása"; +"addVault.openExistingVault.chooseCloud.header" = "Hol található a széf?"; +"addVault.openExistingVault.detectedMasterkey.text" = "A Cryptomator felismerte a következő széfet: \"%@\".\nSzeretné ezt a széfet hozzáadni?"; +"addVault.openExistingVault.detectedMasterkey.add" = "Széf hozzáadása"; +"addVault.openExistingVault.downloadVault.progress" = "Széf letöltése…"; +"addVault.openExistingVault.password.footer" = "Írja be a jelszót a következőhöz: \"%@\"."; +"addVault.openExistingVault.progress" = "Széf hozzáadása…"; +"addVault.success.info" = "Sikeresen megtörtént a következő széf hozzáadása: \"%@\".\nElérheti ezt a széfet a Fájlok appban."; +"addVault.success.footer" = "Ha még nem tette volna, engedélyezze a Cryptomatort a Fájlok appban."; + +"biometryType.faceID" = "Face ID"; +"biometryType.touchID" = "Touch ID"; + +"changePassword.error.invalidOldPassword" = "A jelenlegi jelszó helytelen. Kérjük, próbálja újra."; +"changePassword.header.currentPassword.title" = "Írja be a jelenlegi jelszót."; +"changePassword.header.newPassword.title" = "Adjon meg egy új jelszót."; +"changePassword.header.newPasswordConfirmation.title" = "Erősítse meg az új jelszót."; +"changePassword.progress" = "Jelszó módosítása…"; + +"chooseFolder.emptyFolder.footer" = "A mappa üres"; +"chooseFolder.createNewFolder.header.title" = "Válasszon egy nevet a mappa számára."; +"chooseFolder.createNewFolder.cells.name" = "Mappa neve"; +"chooseFolder.createNewFolder.error.emptyFolderName" = "A mappa neve nem lehet üres."; +"chooseFolder.createNewFolder.progress" = "Mappa létrehozása…"; + +"cloudProvider.error.itemNotFound" = "A(z) \"%@\" nem található."; +"cloudProvider.error.itemAlreadyExists" = "A(z) \"%@\" már létezik."; +"cloudProvider.error.itemTypeMismatch" = "A következőben egy nem várt elemtípus található: \"%@\"."; +"cloudProvider.error.parentFolderDoesNotExist" = "A szülőmappa \"%@\" nem létezik."; +"cloudProvider.error.pageTokenInvalid" = "A könyvtár elemeinek lekérését nem tudtuk folytatni."; +"cloudProvider.error.quotaInsufficient" = "Kevés a tárhely."; +"cloudProvider.error.unauthorized" = "Nem lehet jogosulatlan műveletet végrehajtani."; +"cloudProvider.error.noInternetConnection" = "Internet hozzáférés szükséges ehhez a művelethez."; + +"cloudProviderType.localFileSystem" = "Más fájlszolgáltató"; + +"fileProvider.onboarding.title" = "Üdvözöljük"; +"fileProvider.onboarding.info" = "Köszönjük, hogy a Cryptomatort válaszotta fájlainak megvédéséhez. Kezdéshez menjen a fő appba és adjon hozzá egy széfet."; +"fileProvider.onboarding.button.openCryptomator" = "Cryptomator megnyitása"; +"fileProvider.error.biometricalAuthCanceled.title" = "Feloldás megszakítva"; +"fileProvider.error.biometricalAuthWrongPassword.title" = "Helytelen jelszó"; +"fileProvider.error.defaultLock.title" = "Feloldás szükséges"; "fileProvider.error.unlockButton" = "Feloldás"; +"fileProvider.clearFileFromCache.title" = "Fájl törlése az átmeneti tárból"; +"fileProvider.clearFileFromCache.message" = "Ez csak a helyi fájlt távolítja el az eszközről, de nem törli a fájlt a felhőben."; +"fileProvider.fileImporting.error.missingPremium" = "Oldja fel a teljes verziót a Cryptomator alkalmazásban, hogy írási hozzáférést kapjon a széfjeihez."; +"fileProvider.uploadProgress.connecting" = "Kapcsolódás…"; +"fileProvider.uploadProgress.title" = "Feltöltés…"; +"fileProvider.uploadProgress.missingDomainError" = "A domain nem található."; +"getFolderIntent.error.noVaultSelected" = "Nincs széf kiválasztva."; +"hubAuthentication.accessNotGranted" = "Eszköze még nem kapott engedélyt ehhez a széfhez. Kérje a széf tulajdonosát, hogy engedélyezze a hozzáférést."; +"hubAuthentication.licenseExceeded" = "Az Ön Cryptomator Hub példánya érvénytelen licenccel rendelkezik. Kérem, értesítsen egy Hub rendszergazdát hogy frissítse vagy újítsa meg a licencet."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Készülék neve"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Az eszköz regisztrációja sikeres"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "A széf hozzáféréséhez a széf tulajdonosának hitelesítenie kell az eszközét."; + +"intents.saveFile.missingFile" = "A megadott fájl érvénytelen."; +"intents.saveFile.invalidFolder" = "A megadott mappa érvénytelen."; +"intents.saveFile.missingTemporaryFolder" = "Nem sikerült létrehozni az ideiglenes mappát."; + +"keepUnlocked.alert.title" = "Széf zárolása?"; +"keepUnlocked.alert.message" = "Ezen módosítások végbemenéséhez szükséges a széfet zárolni."; +"keepUnlocked.alert.confirm" = "Megerősítés és zárolás most"; +"keepUnlocked.header" = "Adja meg, hogy mennyi ideig legyen a széf feloldva amikor tétlen."; +"keepUnlockedDuration.auto" = "Döntsön az iOS automatikusan"; +"keepUnlockedDuration.auto.shortDisplayName" = "Automatikus"; +"keepUnlockedDuration.indefinite" = "Korlátlan"; + +"onboarding.title" = "Üdvözöljük"; +"onboarding.info" = "Köszönjük, hogy a Cryptomatort választotta fájlai védelméhez.\n\nA Cryptomatorral az adatainak kulcsa az ön kezében van. A Cryptomator titkosítja az adatait gyorsan és könnyedén.\n\nEz az alkalmazás teljesen integrálva van a Fájlok appba. Engedélyezze a Cryptomatort a Fájlok appban később, hogy hozzáférhessen a széfjeihez."; +"onboarding.button.continue" = "Tovább"; + +"purchase.beginFreeTrial.alert.title" = "Próbaverzió feloldva"; +"purchase.expiredTrial" = "A próbaidőszak lejárt."; +"purchase.footer.privacyPolicy" = "Adatvédelmi irányelvek"; +"purchase.footer.termsOfUse" = "Használati feltételek"; +"purchase.header.feature.familySharing" = "Családon belüli megosztás"; +"purchase.header.feature.openSource" = "Nyílt forráskódú fejlesztés"; +"purchase.product.donateAndUpgrade" = "Adományozás és frissítés"; +"purchase.product.freeUpgrade" = "Ingyenes frissítés"; +"purchase.product.lifetimeLicense" = "Örök licensz"; +"purchase.product.lifetimeLicense.duration" = "egyszeri"; +"purchase.product.pricing.free" = "Ingyenes"; +"purchase.product.trial" = "30 napos próbaverzió"; +"purchase.product.trial.expirationDate" = "Lejárati dátum: %@"; +"purchase.product.trial.duration" = "30 napig"; +"purchase.product.yearlySubscription" = "Éves előfizetés"; +"purchase.product.yearlySubscription.duration" = "évente"; +"purchase.readOnlyMode.alert.title" = "Csak olvasható mód"; +"purchase.readOnlyMode.alert.message" = "Feloldhatja a Cryptomator teljes verzióját a beállításokban később és addig használhatja csak olvasható módban."; +"purchase.restorePurchase.button" = "Vásárlás visszaállítása"; +"purchase.restorePurchase.validTrialFound.alert.title" = "Próbaidő folytatódott"; +"purchase.restorePurchase.validTrialFound.alert.message" = "Mostmár használhatja a Cryptomator teljes verzióját korlátozott időre. A próbaidője lejár ekkor: %@. Azután a széfeit továbbra is elérheti csak olvasható módban."; +"purchase.restorePurchase.fullVersionFound.alert.title" = "Sikeres visszaállítás"; +"purchase.restorePurchase.fullVersionNotFound.alert.title" = "Nincs teljes verzió"; +"purchase.restorePurchase.fullVersionNotFound.alert.message" = "Nem találtunk egy előzőleg megvásárolt teljes verziót, melyet visszaállíthattunk volna. Kérjük, próbáljon meg egy másik opciót."; +"purchase.restorePurchase.eligibleForUpgrade.alert.title" = "Frissítésre jogosult"; +"purchase.restorePurchase.eligibleForUpgrade.alert.message" = "Úgy tűnik, hogy a Cryptomator egy régebbi verziójáról próbál meg frissíteni. Ebben az esetben, kérjük válassza a \"Frissítési ajánlat\" opciót."; "purchase.retry.button" = "Újra"; +"purchase.retry.footer" = "Az elérhető termékek betöltése sikertelen."; +"purchase.title" = "Teljes verzió feloldása"; +"purchase.unlockedFullVersion.message" = "Mostmár használhatja a Cryptomator teljes verzióját. Örömteli titkosítást!"; +"purchase.unlockedFullVersion.title" = "Köszönjük"; +"purchase.error.unknown" = "A vásárlás ismeretlen ok miatt nem elérhető az App Storeban. Kérjük, próbálja újra később.\n\nHa ez a probléma fennállna, próbálja meg újraindítani a készülékét vagy ki- és bejelentkezni az Apple ID-ból az iOS beállításokban."; "settings.title" = "Beállítások"; +"settings.aboutCryptomator" = "A Cryptomator névjegye"; +"settings.aboutCryptomator.title" = "Verzió %@ (%@)"; +"settings.cacheSize" = "Gyorsítótár mérete"; "settings.clearCache" = "Gyorsítótár törlése"; +"settings.cloudServices" = "Felhőszolgáltatások"; +"settings.contact" = "Kapcsolat"; +"settings.debugMode" = "Hibakeresési mód"; "settings.debugMode.alert.message" = "Ebben a módban érzékeny adatok kerülhetnek az eszközön lévő naplófájlba (pl. fájlnevek és -útvonalak). Ez jelszavakra és sütikre nem vonatkozik.\n\n Ne felejtse el minél hamarabb kikapcsolni a fejlesztői módot."; +"settings.manageSubscriptions" = "Előfizetés kezelése"; +"settings.rateApp" = "Alkalmazás értékelése"; +"settings.sendLogFile" = "Naplófájl küldése"; +"settings.shortcutsGuide" = "Parancsok útmutató"; +"settings.unlockFullVersion" = "Teljes verzió feloldása"; + +"snapshots.fileprovider.file1" = "/Könyevlés.numbers"; +"snapshots.fileprovider.file2" = "/Kész Előadás.key"; +"snapshots.fileprovider.file3" = "/Termék Filmelőzetes.mov"; +"snapshots.fileprovider.file4" = "/Javaslat.docx"; +"snapshots.fileprovider.file5" = "/Jelentés.pdf"; +"snapshots.fileprovider.folder3" = "/Titkos Projekt"; +"snapshots.fileprovider.folder2" = "/Számlák"; +"snapshots.fileprovider.folder1" = "/Tanúsítványok"; +"snapshots.main.vault1" = "/Munka"; +"snapshots.main.vault2" = "/Család"; +"snapshots.main.vault3" = "/Dokumentumok"; +"snapshots.main.vault4" = "/Utazás Kaliforniába"; "s3Authentication.displayName" = "Megjelenítendő név"; "s3Authentication.accessKey" = "Hozzáférési kulcs"; @@ -34,16 +195,43 @@ "s3Authentication.existingBucket" = "Meglévő tartály"; "s3Authentication.endpoint" = "Végpont"; "s3Authentication.region" = "Régió"; +"s3Authentication.error.invalidCredentials" = "Érvénytelen hitelesítő adatok."; +"s3Authentication.error.invalidEndpoint" = "A megadott végpont nem egyezik az URL formátumával."; + +"trialStatus.active" = "Aktív"; +"trialStatus.expired" = "Lejárt"; "unlockVault.button.unlock" = "Kinyitás"; "unlockVault.button.unlockVia" = "Kinyitva %@ által"; +"unlockVault.password.footer" = "Írja be a jelszót a következőhöz: \"%@\"."; "unlockVault.enableBiometricalUnlock.switch" = "%@ engedélyezése"; "unlockVault.enableBiometricalUnlock.footer" = "Ahelyett, hogy jelszavával nyitná fel a széfet, a %@ segítségével is feloldhatja azt."; "unlockVault.evaluatePolicy.reason" = "Széf kinyitása"; +"unlockVault.progress" = "Feloldás…"; + +"untrustedTLSCertificate.title" = "Érvénytelen TLS-tanúsítvány"; +"untrustedTLSCertificate.message" = "A \"%@\" TLS-tanúsítványa érvénytelen. Megjelöli mégis megbízhatóként?\n\n SHA-256: %@"; +"untrustedTLSCertificate.add" = "Megbízható"; +"untrustedTLSCertificate.dismiss" = "Nem megbízható"; + +"upgrade.title" = "Frissítési ajánlat"; +"upgrade.notEligible.alert.title" = "A frissítés nem sikerült"; +"urlSession.error.httpError.404" = "A keresett oldal nem található."; "vaultDetail.button.changeVaultPassword" = "Jelszó megváltoztatása"; +"vaultDetail.button.lock" = "Zárolás most"; "vaultDetail.button.moveVault" = "Áthelyezés"; "vaultDetail.button.renameVault" = "Átnevezés"; +"vaultDetail.info.footer.accessVault" = "Elérheti a széfet a Fájlok appban."; +"vaultDetail.info.footer.accountInfo" = "Bejelentkezve, mint %@ itt: %@."; +"vaultDetail.keepUnlocked.title" = "Feloldás hossza"; +"vaultDetail.moveVault.progress" = "Áthelyezés…"; +"vaultDetail.renameVault.progress" = "Átnevezés…"; +"vaultDetail.unlocked.footer" = "A széfe jelenleg fel van oldva a Fájlok appban."; + +"vaultList.header.title" = "Széfek"; +"vaultList.emptyList.message" = "Koppintson ide egy széf hozzáadásához"; +"vaultList.remove.alert.title" = "Széf eltávolitása?"; "webDAVAuthentication.httpConnection.alert.title" = "HTTPS használata"; "webDAVAuthentication.httpConnection.alert.message" = "A HTTP használata nem biztonságos, helyette a HTTPS használata ajánlott. Ha ismeri a kockázatot, folytathatja HTTP-vel is."; diff --git a/SharedResources/id.lproj/Localizable.strings b/SharedResources/id.lproj/Localizable.strings index 0018f1ca7..d915210e4 100644 --- a/SharedResources/id.lproj/Localizable.strings +++ b/SharedResources/id.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Aktifkan"; "common.button.next" = "Lanjut"; "common.button.ok" = "OK"; +"common.button.refresh" = "Segarkan"; "common.button.remove" = "Hapus"; "common.button.retry" = "Coba lagi"; "common.button.signOut" = "Keluar"; @@ -109,6 +110,7 @@ "getFolderIntent.error.missingPath" = "Tidak ada path yang disediakan. Harap cantumkan path yang valid untuk folder yang harus dikembalikan."; "getFolderIntent.error.noVaultSelected" = "Tidak ada vault yang dipilih."; + "intents.saveFile.missingFile" = "File yang disediakan tidak valid."; "intents.saveFile.invalidFolder" = "Folder yang disediakan tidak valid."; "intents.saveFile.missingTemporaryFolder" = "Gagal membuat folder sementara."; diff --git a/SharedResources/it.lproj/Localizable.strings b/SharedResources/it.lproj/Localizable.strings index 8b013eafa..1518e7c9f 100644 --- a/SharedResources/it.lproj/Localizable.strings +++ b/SharedResources/it.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Abilita"; "common.button.next" = "Avanti"; "common.button.ok" = "OK"; +"common.button.refresh" = "Aggiorna"; +"common.button.register" = "Registrati"; "common.button.remove" = "Rimuovi"; "common.button.retry" = "Riprova"; "common.button.signOut" = "Disconnettiti"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Dove si trova la cassaforte?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator ha rilevato la cassaforte \"%@\".\nVorresti aggiungerla?"; "addVault.openExistingVault.detectedMasterkey.add" = "Aggiungi Questa Cassaforte"; +"addVault.openExistingVault.downloadVault.progress" = "Scaricamento Cassaforte…"; "addVault.openExistingVault.password.footer" = "Inserisci la password per \"%@\"."; "addVault.openExistingVault.progress" = "Aggiungendo la Cassaforte…"; "addVault.success.info" = "Cassaforte \"%@\" aggiunta correttamente.\nAccedi a questa cassaforte tramite l'app File."; @@ -110,6 +113,15 @@ "getFolderIntent.error.missingPath" = "Non è stato fornito alcun percorso. Fornire un percorso valido per il quale deve essere restituita una cartella."; "getFolderIntent.error.noVaultSelected" = "Nessuna cassaforte è stata selezionata."; + +"hubAuthentication.title" = "Centrale delle Casseforti"; +"hubAuthentication.accessNotGranted" = "Il tuo dispositivo non è ancora stato autorizzato ad accedere a questa cassaforte. Chiedi al proprietario della cassaforte di autorizzarlo."; +"hubAuthentication.licenseExceeded" = "La tua istanza Cryptomator Hub ha una licenza non valida. Si prega di informare un amministratore Hub per aggiornare o rinnovare la licenza."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nome Del Dispositivo"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Sembra che questo sia il primo accesso alle Centrali delle Casseforti da questo dispositivo. Per poterlo identificare ai fini dell'autorizzazione all'accesso è necessario dare un nome a questo dispositivo."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Registrazione del dispositivo Riuscita"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Per accedere al vault, il tuo dispositivo deve essere autorizzato dal proprietario del vault."; + "intents.saveFile.missingFile" = "Il file fornito non è valido."; "intents.saveFile.invalidFolder" = "La cartella fornita non è valida."; "intents.saveFile.missingTemporaryFolder" = "Non è stato possibile creare la cartella temporanea."; diff --git a/SharedResources/ja.lproj/Localizable.strings b/SharedResources/ja.lproj/Localizable.strings index a4724c7fb..8f4ecf5eb 100644 --- a/SharedResources/ja.lproj/Localizable.strings +++ b/SharedResources/ja.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "有効にする"; "common.button.next" = "次へ"; "common.button.ok" = "OK"; +"common.button.refresh" = "更新"; +"common.button.register" = "登録"; "common.button.remove" = "削除"; "common.button.retry" = "再試行"; "common.button.signOut" = "サインアウト"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "金庫の場所"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomatorが金庫 \"%@\" を検出しました。\nこの金庫を追加しますか?"; "addVault.openExistingVault.detectedMasterkey.add" = "この金庫を追加"; +"addVault.openExistingVault.downloadVault.progress" = "金庫をダウンロード…"; "addVault.openExistingVault.password.footer" = "\"%@\" のパスワードを入力してください。"; "addVault.openExistingVault.progress" = "金庫を追加しています…"; "addVault.success.info" = "金庫 \"%@\" を正常に追加しました。\nファイル アプリからこの金庫にアクセスしてください。"; @@ -110,6 +113,15 @@ "getFolderIntent.error.missingPath" = "パスがありません。妥当なフォルダーのパスを指定してください。"; "getFolderIntent.error.noVaultSelected" = "金庫は選択されていません。"; +"hubAuthentication.accessNotGranted" = "お使いのデバイスはまだこの金庫にアクセスする権限がありません。金庫のオーナーに権限を与えてもらってください。"; +"hubAuthentication.licenseExceeded" = "Cryptomator Hub インスタンスのライセンスが無効です。ライセンスをアップグレードまたは更新するには、Hub の管理者にご連絡ください。"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "デバイス名"; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "アカウントキーは新しいアプリやブラウザからログインするために必要です。プロフィール中に記載されています。"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "金庫にアクセスするためには,金庫のオーナーが端末を認証する必要があります。"; +"hubAuthentication.requireAccountInit.alert.title" = "要対応"; +"hubAuthentication.requireAccountInit.alert.message" = "続行するにはHubユーザープロフィールで必要な手順を完了してください。"; +"hubAuthentication.requireAccountInit.alert.actionButton" = "プロフィールへ移動"; + "intents.saveFile.missingFile" = "指定されたファイルはだめです。"; "intents.saveFile.invalidFolder" = "指定されたフォルダーはだめです。"; "intents.saveFile.missingTemporaryFolder" = "臨時フォルダーの作成ができませんでした。"; diff --git a/SharedResources/ko.lproj/Localizable.strings b/SharedResources/ko.lproj/Localizable.strings index 0014858fa..380f92695 100644 --- a/SharedResources/ko.lproj/Localizable.strings +++ b/SharedResources/ko.lproj/Localizable.strings @@ -15,10 +15,12 @@ "common.button.create" = "생성"; "common.button.createFolder" = "폴더 생성"; "common.button.done" = "완료"; +"common.button.download" = "다운로드"; "common.button.edit" = "수정"; "common.button.enable" = "활성화"; "common.button.next" = "다음"; "common.button.ok" = "OK"; +"common.button.refresh" = "새로고침"; "common.button.remove" = "제거"; "common.button.retry" = "재시도"; "common.button.signOut" = "로그아웃"; @@ -28,6 +30,9 @@ "common.cells.url" = "URL"; "common.cells.username" = "사용자명"; "common.footer.learnMore" = "자세히 알아보기"; +"common.hud.authenticating" = "로그인중…"; + +"accountList.header.title" = "계정"; "accountList.emptyList.message" = "여기를 눌러 계정 추가"; "accountList.signOut.alert.title" = "연결된 Vault를 제거하시겠습니까?"; "accountList.signOut.alert.message" = "로그아웃 시, 연결된 Vault가 목록에서 모두 제거됩니다. 암호화된 데이터가 삭제되는 것은 아닙니다. 다시 로그인 시 Vault를 다시 추가 할 수 있습니다."; @@ -37,7 +42,9 @@ "addVault.createNewVault.purchase" = "새로운 vault를 만들려면 정식 버전을 구입해야 합니다."; "addVault.createNewVault.setVaultName.header.title" = "Vault의 이름을 선택해주십시요."; "addVault.createNewVault.setVaultName.cells.name" = "Vault 이름"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "Vault 이름은 비어있을 수 없습니다."; "addVault.createNewVault.chooseCloud.header" = "Cryptomator Vault의 암호화 파일을 어디에 저장하시겠습니까?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "해당 위치에 \"%@\"가 이미 존재합니다. 다른 vault명을 정하거나 다른 위치를 선택해주세요."; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator가 이 위치에 있는 Vault를 감지하였습니다.\n 새 Vault를 생성하기 위해, 다른 경로를 선택하시기 바랍니다."; "addVault.createNewVault.password.enterPassword.header" = "새 비밀번호를 입력하세요."; "addVault.createNewVault.password.confirmPassword.header" = "새 비밀번호 확인."; @@ -60,6 +67,7 @@ "biometryType.touchID" = "Touch ID"; "changePassword.error.invalidOldPassword" = "기존 비밀번호가 올바르지 않습니다. 다시 시도하십시오."; +"changePassword.header.currentPassword.title" = "기존 비밀번호를 입력해주세요."; "changePassword.header.newPassword.title" = "새 비밀번호를 입력하세요."; "changePassword.header.newPasswordConfirmation.title" = "새 비밀번호 확인."; "changePassword.progress" = "비밀번호 변경 중..."; @@ -70,6 +78,12 @@ "chooseFolder.createNewFolder.error.emptyFolderName" = "폴더 이름이 비어 있어서는 안 됩니다."; "chooseFolder.createNewFolder.progress" = "폴더 생성 중..."; +"cloudProvider.error.itemNotFound" = "\"%@\"를 찾을 수 없습니다."; +"cloudProvider.error.itemAlreadyExists" = "\"%@\"가 이미 존재합니다."; +"cloudProvider.error.itemTypeMismatch" = "\"%@\"의 파일 타입이 올바르지 않습니다."; +"cloudProvider.error.parentFolderDoesNotExist" = "상위 폴더 \"%@\"가 존재하지 않습니다."; +"cloudProvider.error.quotaInsufficient" = "저장공간이 부족합니다."; + "cloudProviderType.localFileSystem" = "다른 파일 앱"; "fileProvider.onboarding.title" = "환영합니다"; @@ -79,6 +93,10 @@ "fileProvider.fileImporting.error.missingPremium" = "Vault에 쓰기 권한을 얻기 위해 Cryptomator앱에서 정식 버전을 구입하십시오."; "fileProvider.uploadProgress.connecting" = "연결 중..."; "fileProvider.uploadProgress.title" = "업로드 중…"; +"hubAuthentication.accessNotGranted" = "귀하의 기기는 아직 이 저장소에 액세스할 수 있는 권한이 없습니다. Vault 소유자에게 승인을 요청하세요."; +"hubAuthentication.licenseExceeded" = "Cryptomator Hub 인스턴스에 잘못된 라이선스가 있습니다. 라이센스를 업그레이드하거나 갱신하려면 허브 관리자에게 알리십시오."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "기기 이름"; + "intents.saveFile.missingFile" = "파일이 올바르지 않습니다."; "intents.saveFile.invalidFolder" = "폴더가 올바르지 않습니다."; @@ -121,6 +139,7 @@ "s3Authentication.displayName" = "표시 이름"; "s3Authentication.accessKey" = "액세스 키"; "s3Authentication.secretKey" = "비밀키"; +"s3Authentication.existingBucket" = "이미 존재하는 버킷"; "s3Authentication.endpoint" = "엔드포인트"; "s3Authentication.region" = "지역"; @@ -138,6 +157,7 @@ "vaultDetail.button.changeVaultPassword" = "비밀번호 변경"; "vaultDetail.button.moveVault" = "이동"; "vaultDetail.button.renameVault" = "이름 변경"; +"vaultDetail.renameVault.progress" = "이름변경중…"; "vaultList.header.title" = "Vault"; "vaultList.emptyList.message" = "여기를 눌러 Vault 추가"; @@ -147,3 +167,5 @@ "webDAVAuthentication.httpConnection.alert.title" = "HTTPS를 사용하겠습니가?"; "webDAVAuthentication.httpConnection.alert.message" = "HTTP 사용은 안전하지 않습니다. HTTPS 사용을 권장합니다. HTTP 사용으로 야기될 문제를 숙지하고 있다면, HTTP를 사용할 수 있습니다."; "webDAVAuthentication.httpConnection.change" = "HTTPS로 변경"; + +"Retry Upload" = "업로드 재시도"; diff --git a/SharedResources/lv.lproj/Localizable.strings b/SharedResources/lv.lproj/Localizable.strings index da3698ff5..0b1101d65 100644 --- a/SharedResources/lv.lproj/Localizable.strings +++ b/SharedResources/lv.lproj/Localizable.strings @@ -11,6 +11,7 @@ "addVault.createNewVault.chooseCloud.header" = "Kur Cryptomator vajadzētu glabāt jūsu glabātuves šifrētos failus?"; "addVault.openExistingVault.title" = "Atvērt esošu glabātuvi"; "fileProvider.error.unlockButton" = "Atslēgt"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Ierīces nosaukums"; "unlockVault.button.unlock" = "Atslēgt"; diff --git a/SharedResources/mk.lproj/Localizable.strings b/SharedResources/mk.lproj/Localizable.strings index 60f7ef444..cdd508edd 100644 --- a/SharedResources/mk.lproj/Localizable.strings +++ b/SharedResources/mk.lproj/Localizable.strings @@ -7,7 +7,9 @@ "common.alert.error.title" = "Грешка"; "common.button.cancel" = "Излез"; +"common.button.change" = "Промени"; "common.button.choose" = "Избор"; +"common.button.close" = "Затвори"; "common.button.confirm" = "Потврди"; "common.button.create" = "Создади"; "common.button.createFolder" = "Креирај папка"; @@ -37,3 +39,7 @@ "addVault.createNewVault.setVaultName.error.emptyVaultName" = "Името не може да биде празно."; "addVault.createNewVault.chooseCloud.header" = "Каде би сакале Cryptomator да ги зачува шифрираните фајлови на Вашиот сеф?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" веќе постои на оваа локација. Изберете друго име за сефот или нова локација."; +"addVault.openExistingVault.title" = "Отвори постоечки сеф"; +"fileProvider.error.unlockButton" = "Отклучи"; + +"unlockVault.button.unlock" = "Отклучи"; diff --git a/SharedResources/nb.lproj/Localizable.strings b/SharedResources/nb.lproj/Localizable.strings index bbf46383a..97b5dbe8e 100644 --- a/SharedResources/nb.lproj/Localizable.strings +++ b/SharedResources/nb.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Aktiver"; "common.button.next" = "Neste"; "common.button.ok" = "Ok"; +"common.button.refresh" = "Oppdater"; +"common.button.register" = "Registrer"; "common.button.remove" = "Fjern"; "common.button.retry" = "Prøv igjen"; "common.button.signOut" = "Logg ut"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Hvor er hvelvet plassert?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator oppdaget hvelvet \"%@\".\nØnsker du å legge til dette hvelvet?"; "addVault.openExistingVault.detectedMasterkey.add" = "Legg til dette hvelvet"; +"addVault.openExistingVault.downloadVault.progress" = "Laster ned hvelv…"; "addVault.openExistingVault.password.footer" = "Skriv inn passordet for \"%@\"."; "addVault.openExistingVault.progress" = "Legger til hvelv…"; "addVault.success.info" = "Hvelvet \"%@\" ble lagt til.\nFå tilgang til dette hvelvet via Filer-appen."; @@ -110,6 +113,15 @@ "getFolderIntent.error.missingPath" = "Ingen målmappe ble oppgitt. Vennligst oppgi en gyldig bane hvor en mappe skal returneres til."; "getFolderIntent.error.noVaultSelected" = "Ingen hvelv har blitt valgt."; + +"hubAuthentication.title" = "Hub hvelv"; +"hubAuthentication.accessNotGranted" = "Enheten din har ikke blitt autorisert til å få tilgang til dette hvelvet ennå. Spør hvelveieren om å tillate det."; +"hubAuthentication.licenseExceeded" = "Cryptomator Hub instansen din har en ugyldig lisens. Vennligst informer en Hub-administrator om å oppgradere eller fornye lisensen."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Enhetsnavn"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Dette ser ut til å være den første Hub-tilgangen fra denne enheten. For å kunne identifisere den for tilgangsautorisasjon, må du å navngi denne enheten."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Enhetsregistrering vellykket"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "For å få tilgang til hvelvet, så må enheten din bli autorisert av hvelvets eier."; + "intents.saveFile.missingFile" = "Den oppgitte filen er ugyldig."; "intents.saveFile.invalidFolder" = "Den oppgitte mappen er ugyldig."; "intents.saveFile.missingTemporaryFolder" = "Kunne ikke opprette temporær mappe."; diff --git a/SharedResources/nl.lproj/Localizable.strings b/SharedResources/nl.lproj/Localizable.strings index be59af1b6..ea47789dd 100644 --- a/SharedResources/nl.lproj/Localizable.strings +++ b/SharedResources/nl.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Inschakelen"; "common.button.next" = "Volgende"; "common.button.ok" = "Ok"; +"common.button.refresh" = "Vernieuwen"; +"common.button.register" = "Registreren"; "common.button.remove" = "Verwijderen"; "common.button.retry" = "Opnieuw proberen"; "common.button.signOut" = "Uitloggen"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Wat is de locatie van de kluis?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator detecteerde de kluis \"%@\".\nWilt u deze kluis toevoegen?"; "addVault.openExistingVault.detectedMasterkey.add" = "Voeg deze kluis toe"; +"addVault.openExistingVault.downloadVault.progress" = "Kluis downloaden…"; "addVault.openExistingVault.password.footer" = "Voer wachtwoord voor \"%@\" in."; "addVault.openExistingVault.progress" = "Kluis toevoegen…"; "addVault.success.info" = "Kluis succesvol toegevoegd \"%@\".\nToegang tot deze kluis via de bestandsapp."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Er is geen pad opgegeven. Geef een geldig pad om de folder weer te geven."; "getFolderIntent.error.noVaultSelected" = "Er is geen kluis geselecteerd."; + +"hubAuthentication.title" = "Hub Kluis"; +"hubAuthentication.accessNotGranted" = "Uw toestel is nog niet gemachtigd om toegang te krijgen tot deze kluis. Vraag de eigenaar van de kluis om toestemming te geven."; +"hubAuthentication.licenseExceeded" = "Uw Cryptomator Hub installatie heeft een ongeldige licentie. Contacteer een Hub administrator om de licentie te upgraden of te verlengen."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Naam van toestel"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Dit lijkt de eerste Hub toegang te zijn vanaf dit toestel. Om dit toestel te autoriseren voor toegang, moet u dit toestel benoemen."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Uw accountsleutel is vereist om in te loggen vanuit nieuwe apps of browsers. Deze kan worden gevonden in uw profiel."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Registratie van toestel gelukt"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Om toegang te krijgen tot de kluis, moet je toestel worden gemachtigd door de eigenaar van de kluis."; +"hubAuthentication.requireAccountInit.alert.title" = "Actie vereist"; +"hubAuthentication.requireAccountInit.alert.message" = "Om verder te gaan, gelieve de stappen te voltooien in uw Hub-gebruikersprofiel."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Ga naar Profiel"; + "intents.saveFile.missingFile" = "Het opgegeven bestand is niet geldig."; "intents.saveFile.invalidFolder" = "De opgegeven map is niet geldig."; "intents.saveFile.missingTemporaryFolder" = "Aanmaken van tijdelijke map mislukt."; diff --git a/SharedResources/pa.lproj/Localizable.strings b/SharedResources/pa.lproj/Localizable.strings index 52494d230..093d84b5d 100644 --- a/SharedResources/pa.lproj/Localizable.strings +++ b/SharedResources/pa.lproj/Localizable.strings @@ -1,18 +1,196 @@ +/* + Localizable.strings + Cryptomator + + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"common.alert.error.title" = "ਗਲਤੀ"; +"common.alert.attention.title" = "ਧਿਆਨ ਦਿਓ"; "common.button.cancel" = "ਰੱਦ ਕਰੋ"; "common.button.change" = "ਬਦਲੋ"; "common.button.choose" = "ਚੁਣੋ"; +"common.button.clear" = "ਮਿਟਾਓ"; "common.button.close" = "ਬੰਦ ਕਰੋ"; +"common.button.confirm" = "ਤਸਦੀਕ"; +"common.button.create" = "ਬਣਾਓ"; +"common.button.createFolder" = "ਫੋਲਡਰ ਬਣਾਓ"; "common.button.done" = "ਮੁਕੰਮਲ"; +"common.button.download" = "ਡਾਊਨਲੋਡ"; +"common.button.edit" = "ਸੋਧੋ"; +"common.button.enable" = "ਸਮਰੱਥ"; "common.button.next" = "ਅੱਗੇ"; +"common.button.ok" = "ਠੀਕ ਹੈ"; +"common.button.remove" = "ਹਟਾਓ"; +"common.button.retry" = "ਮੁੜ-ਕੋਸ਼ਿਸ਼"; +"common.button.signOut" = "ਸਾਈਨ ਆਉਟ"; +"common.button.verify" = "ਤਸਦੀਕ"; +"common.cells.openInFilesApp" = "Files ਐਪ ਵਿੱਚ ਖੋਲ੍ਹੋ"; "common.cells.password" = "ਪਾਸਵਰਡ"; +"common.cells.url" = "URL"; +"common.cells.username" = "ਵਰਤੋਂਕਾਰ ਨਾਂ"; +"common.footer.learnMore" = "ਹੋਰ ਜਾਣੋ।"; +"common.hud.authenticating" = "ਪਰਮਾਣਿਤ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ…"; + +"accountList.header.title" = "ਪਰਮਾਣਕਿਤਾ"; +"accountList.emptyList.message" = "ਖਾਤਾ ਜੋੜਣ ਲਈ ਇੱਥੇ ਛੋਹੋ"; +"accountList.signOut.alert.title" = "ਸੰਬੰਧਿਤ ਖਾਤੇ ਹਟਾਉਣੇ ਹਨ?"; +"accountList.signOut.alert.message" = "ਸਾਈਨ ਆਉਟ ਕਰਨ ਨਾਲ ਵਾਲਟ ਸੂਚੀ ਵਿੱਚੋਂ ਸਾਰੇ ਸੰਬੰਧਿਤ ਵਾਲਟ ਹਟਾਏ ਜਾਣਗੇ। ਕੋਈ ਵੀ ਇੰਕ੍ਰਿਪਟ ਕੀਤਾ ਡਾਟੇ ਨਹੀਂ ਹਟਾਇਆ ਜਾਵੇਗਾ। ਤੁਸੀਂ ਮੁੜ ਸਾਈਨ ਕਰ ਸਕਦੇ ਹੋ ਅਤੇ ਵਾਲਟਾਂ ਨੂੰ ਫੇਰ ਜੋੜ ਸਕਦੇ ਹੋ।"; "addVault.title" = "ਵਾਲਟ ਜੋੜੋ"; "addVault.createNewVault.title" = "ਨਵਾਂ ਵਾਲਟ ਬਣਾਓ"; +"addVault.createNewVault.purchase" = "ਨਵਾਂ ਵਾਲਟ ਬਣਾਉਣ ਥਈ Cryptomator ਦਾ ਪੂਰਾ ਵਰਜ਼ਨ ਚਾਹੀਦਾ ਹੈ।"; +"addVault.createNewVault.setVaultName.header.title" = "ਵਾਲਟ ਲਈ ਨਵਾਂ ਚੁਣੋ।"; "addVault.createNewVault.setVaultName.cells.name" = "ਵਾਲਟ ਦਾ ਨਾਂ"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "ਵਾਲਟ ਨਾਂ ਖਾਲੀ ਨਹੀਂ ਹੋ ਸਕਦਾ ਹੈ।"; "addVault.createNewVault.chooseCloud.header" = "ਤੁਹਾਡੇ ਵਾਲੇਟ ਲਈ ਇੰਕ੍ਰਿਪਟ ਕੀਤੀਆਂ ਫ਼ਾਇਲਾਂ Cryptomator ਕਿੱਥੇ ਸਟੋਰ ਕਰੇ?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "ਇਸ ਟਿਕਾਣੇ ਉੱਤੇ \"%@\" ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ। ਵੱਖਰਾ ਵਾਲਟ ਨਾਂ ਜਾਂ ਟਿਕਾਣਾ ਚੁਣੋ।"; +"addVault.createNewVault.detectedMasterkey.text" = "Cryptomator ਨੂੰ ਇਸ ਟਿਕਾਣੇ ਉੱਤੇ ਇੱਕ ਮੌਜੂਦਾ ਵਾਲਟ ਮਿਲਿਆ ਹੈ।\nਨਵਾਂ ਵਾਲਟ ਬਣਾਉਣ ਲਈ ਪਿੱਛੇ ਜਾ ਕੇ ਵੱਖਰਾ ਫੋਲਡਰ ਚੁਣੋ।"; +"addVault.createNewVault.password.enterPassword.header" = "ਨਵਾਂ ਪਾਸਵਰਡ ਦਿਓ।"; +"addVault.createNewVault.password.confirmPassword.header" = "ਨਵੇਂ ਪਾਸਵਰਡ ਨੂੰ ਤਸਦੀਕ ਕਰੋ।"; +"addVault.createNewVault.password.confirmPassword.alert.title" = "ਪਾਸਵਰਡ ਤਸਦੀਕ ਕਰਨਾ ਹੈ?"; +"addVault.createNewVault.password.confirmPassword.alert.message" = "ਜ਼ਰੂਰੀ: ਜੇ ਤੁਸੀਂ ਆਪਣਾ ਪਾਸਵਰਡ ਭੁੱਲ ਗਏ ਤਾਂ ਤੁਹਾਡੇ ਡਾਟੇ ਨੂੰ ਬਹਾਲ ਕਰਨ ਦਾ ਕੋਈ ਵੀ ਢੰਗ ਨਹੀਂ ਹੈ।"; +"addVault.createNewVault.password.error.emptyPassword" = "ਪਾਸਵਰਡ ਖਾਲੀ ਨਹੀਂ ਹੋ ਸਕਦਾ ਹੈ।"; +"addVault.createNewVault.password.error.nonMatchingPasswords" = "ਪਾਸਵਰਡ ਮਿਲਦਾ ਨਹੀਂ ਹੈ।"; +"addVault.createNewVault.password.error.tooShortPassword" = "ਪਾਸਵਰਡ ਘੱਟੋ-ਘੱਟ 8 ਅੱਖਰਾਂ ਦਾ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ।"; +"addVault.createNewVault.progress" = "ਵਾਲਟ ਬਣਾਇਆ ਜਾ ਰਿਹਾ ਹੈ…"; "addVault.openExistingVault.title" = "ਮੌਜੂਦਾ ਵਾਲਟ ਖੋਲ੍ਹੋ"; +"addVault.openExistingVault.chooseCloud.header" = "ਵਾਲਟ ਕਿੱਥੇ ਮੌਜੂਦ ਹੈ?"; +"addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator ਨੂੰ \"%@\" ਵਾਲਟ ਖੋਜਿਆ ਗਿਆ ਹੈ।\nਕੀ ਤੁਸੀ ਇਹ ਵਾਲਟ ਜੋੜਨਾ ਚਾਹੁੰਦੇ ਹੋ?"; +"addVault.openExistingVault.detectedMasterkey.add" = "ਇਹ ਵਾਲਟ ਜੋੜੋ"; +"addVault.openExistingVault.password.footer" = "\"%@\" ਲਈ ਪਾਸਵਰਡ ਦਿਓ।"; +"addVault.openExistingVault.progress" = "…ਵਾਲਟ ਜੋੜਿਆ ਜਾ ਰਿਹਾ ਹੈ"; +"addVault.success.info" = "\"%@\" ਵਾਲਟ ਨੂੰ ਕਾਮਯਾਬੀ ਨਾਲ ਜੋੜਿਆ ਗਿਆ ਹੈ।\nਇਸ ਵਾਲਟ ਨੂੰ Files ਐਪ ਰਾਹੀਂ ਵਰਤੋਂ।"; +"addVault.success.footer" = "ਜੇ ਤੁਸੀਂ ਹਾਲੇ ਨਹੀਂ ਕੀਤਾ ਤਾਂ Files ਐਪ ਵਿੱਚ Cryptomator ਨੂੰ ਸਮਰੱਥ ਕਰੋ।"; + +"biometryType.faceID" = "ਫੇਸ ID"; +"biometryType.touchID" = "ਟੱਚ ID"; + +"changePassword.error.invalidOldPassword" = "ਮੌਜੂਦਾ ਪਾਸਵਰਡ ਗਲਤ ਹੈ। ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ।"; +"changePassword.header.currentPassword.title" = "ਮੌਜੂਦਾ ਪਾਸਵਰਡ ਦਿਓ।"; +"changePassword.header.newPassword.title" = "ਨਵਾਂ ਪਾਸਵਰਡ ਦਿਓ।"; +"changePassword.header.newPasswordConfirmation.title" = "ਨਵੇਂ ਪਾਸਵਰਡ ਨੂੰ ਤਸਦੀਕ ਕਰੋ।"; +"changePassword.progress" = "…ਪਾਸਵਰਡ ਬਦਲਿਆ ਜਾ ਰਿਹਾ ਹੈ"; + +"chooseFolder.emptyFolder.footer" = "ਫੋਲਡਰ ਖਾਲੀ ਹੈ"; +"chooseFolder.createNewFolder.header.title" = "ਫੋਲਡਰ ਲਈ ਨਾਂ ਚੁਣੋ।"; +"chooseFolder.createNewFolder.cells.name" = "ਫੋਲਡਰ ਦਾ ਨਾਂ"; +"chooseFolder.createNewFolder.error.emptyFolderName" = "ਫ਼ੋਲਡਰ ਨਾਂ ਖਾਲੀ ਨਹੀਂ ਹੋ ਸਕਦਾ ਹੈ।"; +"chooseFolder.createNewFolder.progress" = "…ਫੋਲਡਰ ਬਣਾਇਆ ਜਾ ਰਿਹਾ ਹੈ"; + +"cloudProvider.error.itemNotFound" = "\"%@\" ਲੱਭਿਆ ਨਹੀਂ ਜਾ ਸਕਿਆ।"; +"cloudProvider.error.itemAlreadyExists" = "\"%@\" ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ।"; +"cloudProvider.error.itemTypeMismatch" = "\"%@\" ਅਣਪਛਾਤੀ ਆਈਟਮ ਕਿਸਮ ਹੈ। "; +"cloudProvider.error.parentFolderDoesNotExist" = "ਮੁੱਢਲਾ ਫੋਲਡਰ \"%@\" ਮੌਜੂਦ ਨਹੀਂ ਹੈ।"; +"cloudProvider.error.quotaInsufficient" = "ਤੁਹਾਡੇ ਕੋਲ ਨਾ-ਕਾਫ਼ੀ ਖਾਲੀ ਥਾਂ ਹੈ।"; +"cloudProvider.error.noInternetConnection" = "ਇਸ ਕਾਰਵਾਈ ਲਈ ਇੰਟਰਨੈੱਟ ਕਨੈਕਸ਼ਨ ਦੀ ਲੋੜ ਹੈ।"; + +"cloudProviderType.localFileSystem" = "ਹੋਰ ਫਾਇਲ ਪੂਰਕ"; + +"fileProvider.onboarding.title" = "ਜੀ ਆਇਆਂ ਨੂੰ"; +"fileProvider.onboarding.info" = "ਆਪਣੀਆਂ ਫਾਇਲਾਂ ਨੂੰ ਸੁਰੱਖਿਅਤ ਬਣਾਉਣ ਲਈ Cryptomator ਚੁਣਨ ਵਾਸਤੇ ਧੰਨਵਾਦ ਹੈ। ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਮੁੱਖ ਐਪ ਉੱਤੇ ਜਾਓ ਅਤੇ ਵਾਲਟ ਜੋੜੋ।"; +"fileProvider.onboarding.button.openCryptomator" = "Cryptomator ਖੋਲ੍ਹੋ"; +"fileProvider.error.biometricalAuthCanceled.title" = "ਅਣ-ਲਾਕ ਕਰਨ ਰੱਦ ਕੀਤਾ"; +"fileProvider.error.biometricalAuthCanceled.message" = "%@ ਰਾਹੀਂ ਅਣ-ਲਾਕ ਕਰਨਾ ਕਾਮਯਾਬ ਨਹੀਂ ਹੈ। ਬਾਅਦ ਵਿੱਚ ਫੇਰ ਕੋਸ਼ਿਸ ਕਰੋ।"; +"fileProvider.error.biometricalAuthWrongPassword.title" = "ਗ਼ਲਤ ਪਾਸਵਰਡ"; +"fileProvider.error.biometricalAuthWrongPassword.message" = "%@ ਲਈ ਸੰਭਾਲਿਆ ਗਿਆ ਪਾਸਵਰਡ ਗਲਤ ਹੈ। ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਅਤੇ %@ ਨੂੰ ਮੁੜ-ਸਮਰੱਥ ਕਰਨ ਵਾਸਤੇ ਆਪਣਾ ਪਾਸਵਰਡ ਦਿਓ।"; +"fileProvider.error.defaultLock.title" = "ਅਣ-ਲਾਕ ਕਰਨ ਦੀ ਲੋੜ ਹੈ"; +"fileProvider.error.defaultLock.message" = "ਤੁਹਾਡੇ ਵਾਲਟ ਦੀ ਸਮੱਗਰੀ ਨੂੰ ਵਰਤਣ ਅਤੇ ਵੇਖਣ ਲਈ ਇਸ ਨੂੰ ਅਣ-ਲਾਕ ਕਰਨ ਦੀ ਲੋੜ ਹੈ।"; "fileProvider.error.unlockButton" = "ਅਣ-ਲਾਕ ਕਰੋ"; +"fileProvider.clearFileFromCache.title" = "ਕੈਸ਼ ਤੋਂ ਫਾਇਲ ਮਿਟਾਓ"; +"fileProvider.clearFileFromCache.message" = "ਇਹ ਸਿਰਫ਼ ਤੁਹਾਡੇ ਡਿਵਾਈਸ ਤੋਂ ਲੋਕਲ ਫਾਇਲ ਹੀ ਹਟਾਉਂਦਾ ਹੈ ਅਤੇ ਕਲਾਉਡ ਤੋਂ ਫਾਇਲ ਨੂੰ ਨਹੀਂ ਹਟਾਉਂਦਾ ਹੈ।"; +"fileProvider.fileImporting.error.missingPremium" = "ਤੁਹਾਡੇ ਵਾਲਟਾਂ ਲਈ ਲਿਖਣ ਪਹੁੰਚਣ ਲਈ ਵਾਸਤੇ Cryptomator ਐਪ ਦਾ ਪੂਰਾ ਵਰਜ਼ਨ ਅਣ-ਲਾਕ ਕਰੋ।"; +"fileProvider.uploadProgress.connecting" = "…ਕਨੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"; +"fileProvider.uploadProgress.title" = "…ਅੱਪਲੋਡ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"; +"getFolderIntent.error.noVaultSelected" = "ਕੋਈ ਵਾਲਟ ਨਹੀਂ ਚੁਣਿਆ ਹੈ।"; + +"keepUnlocked.alert.title" = "ਵਾਲਟ ਲਾਕ ਕਰਨਾ ਹੈ?"; +"keepUnlocked.alert.confirm" = "ਤਸਦੀਕ ਅਤੇ ਹੁਣੇ ਲਾਕ ਕਰੋ"; +"keepUnlockedDuration.auto.shortDisplayName" = "ਆਟੋ"; +"localFileSystemAuthentication.createNewVault.button" = "ਸਟੋਰੇਜ਼ ਦਾ ਟਿਕਾਣਾ ਚੁਣੋ"; +"localFileSystemAuthentication.createNewVault.error.detectedExistingVault" = "ਇਸ ਟਿਕਾਣੇ ਉੱਤੇ ਵਾਲਟ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹੈ। ਵੱਖਰੇ ਸਟੋਰੇਜ਼ ਟਿਕਾਣੇ ਨੂੰ ਚੁਣ ਕੇ ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ।"; +"localFileSystemAuthentication.openExistingVault.header" = "ਅਗਲੀ ਸਕਰੀਨ ਉੱਤੇ ਆਪਣੇ ਮੌਜੂਦਾ ਵਾਲਟ ਦਾ ਫੋਲਡਰ ਚੁਣੋ।"; +"localFileSystemAuthentication.openExistingVault.button" = "ਵਾਲਟ ਫੋਲਡਰ ਚੁਣੋ"; +"localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "ਚੁਣਿਆ ਫੋਲਡਰ ਵਾਲਟ ਨਹੀਂ ਹੈ। ਵੱਖਰੇ ਫੋਲਡਰ ਨਾਲ ਫੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ।"; + +"onboarding.title" = "ਜੀ ਆਇਆਂ ਨੂੰ"; +"onboarding.info" = "ਆਪਣੀਆਂ ਫਾਇਲਾਂ ਨੂੰ ਸੁਰੱਖਿਅਤ ਕਰਨ ਵਾਸਤੇ Cryptomator ਚੁਣਨ ਲਈ ਧੰਨਵਾਦ ਹੈ।\n\nCryptomator ਨਾਲ ਤੁਹਾਡੇ ਡਾਟੇ ਦੀ ਕੁੰਜੀ ਤੁਹਾਡੇ ਹੱਥ ਵਿੱਚ ਹੈ। Cryptomator ਤੁਹਾਡੇ ਡਾਟੇ ਨੂੰ ਫ਼ੌਰਨ ਅਤੇ ਸੌਖੀ ਤਰ੍ਹਾਂ ਇੰਕ੍ਰਿਪਟ ਕਰਦਾ ਹੈ।\n\nਇਹ ਐਪ ਪੂਰੀ ਤਰ੍ਹਾਂ Files ਐਪ ਨਾਲ ਜੁੜੀ ਹੋਈ ਹੈ। ਬਾਅਦ ਆਪਣੇ ਵਾਲਟਾਂ ਨੂੰ ਵਰਤਣ Files ਐਪ ਵਿੱਚ Cryptomator ਨੂੰ ਸਮਰੱਥ ਕਰਨਾ ਨਾ ਭੁੱਲੋ।"; +"onboarding.button.continue" = "ਜਾਰੀ ਰੱਖੋ"; + +"purchase.beginFreeTrial.alert.title" = "ਅਜ਼ਮਾਇਸ਼ ਅਣ-ਲਾਕ ਹੈ"; +"purchase.expiredTrial" = "ਤੁਹਾਡੀ ਅਜ਼ਮਾਇਜ਼ ਦੀ ਮਿਆਦ ਪੁੱਗੀ ਹੈ।"; +"purchase.footer.privacyPolicy" = "ਪਰਦੇਦਾਰੀ ਨੀਤੀ"; +"purchase.footer.termsOfUse" = "ਵਰਤਣ ਦੀਆਂ ਸ਼ਰਤਾਂ"; +"purchase.header.feature.familySharing" = "ਪਰਿਵਾਰ ਲਈ ਸਾਂਝਾ"; +"purchase.header.feature.openSource" = "ਓਪਨ-ਸਰੋਤ ਵਿਕਾਸ"; +"purchase.header.feature.writeAccess" = "ਤੁਹਾਡੇ ਵਾਲਟਾਂ ਲਈ ਲਿਖਣ ਪਹੁੰਚ"; +"purchase.product.donateAndUpgrade" = "ਦਾਨ ਦਿਓ ਤੇ ਅੱਪਗਰੇਡ ਕਰੋ"; +"purchase.product.freeUpgrade" = "ਮੁਫ਼ਤ ਅੱਪਗਰੇਡ"; +"purchase.product.lifetimeLicense" = "ਜ਼ਿੰਗਦੀ ਭਰ ਲਈ ਲਸੰਸ"; +"purchase.product.lifetimeLicense.duration" = "ਇੱਕ-ਵਾਰ"; +"purchase.product.pricing.free" = "ਮੁਫ਼ਤ"; +"purchase.product.trial" = "30-ਦਿਨਾਂ ਲਈ ਅਜ਼ਮਾਇਸ਼"; +"purchase.product.trial.expirationDate" = "ਮਿਆਦ: %@"; +"purchase.product.trial.duration" = "30 ਦਿਨਾਂ ਲਈ"; +"purchase.product.yearlySubscription" = "ਸਾਲਨਾ ਮੈਂਬਰੀ"; +"purchase.product.yearlySubscription.duration" = "ਸਾਲਨਾ"; +"purchase.readOnlyMode.alert.title" = "ਕੇਵਲ ਪੜ੍ਹਨ ਲਈ ਢੰਗ"; +"purchase.retry.button" = "ਮੁੜ-ਕੋਸ਼ਿਸ਼"; +"purchase.title" = "ਪੂਰਾ ਵਰਜ਼ਨ ਅਣ-ਲਾਕ ਕਰੋ"; +"purchase.unlockedFullVersion.title" = "ਤੁਹਾਡਾ ਧੰਨਵਾਦ"; + +"settings.title" = "ਸੈਟਿੰਗਾਂ"; +"settings.aboutCryptomator" = "Cryptomator ਖੋਲ੍ਹੋ"; +"settings.aboutCryptomator.title" = "ਵਰਜ਼ਨ %@ (%@)"; +"settings.cacheSize" = "ਕੈਸ਼ ਆਕਾਰ"; +"settings.clearCache" = "ਕੈਸ਼ ਮਿਟਾਓ"; +"settings.cloudServices" = "ਕਲਾਉਡ ਸੇਵਾਵਾਂ"; +"settings.contact" = "ਸੰਪਰਕ ਕਰੋ"; +"settings.debugMode" = "ਡੀਬੱਗ ਢੰਗ"; +"settings.manageSubscriptions" = "ਮੈਂਬਰੀ ਦਾ ਇੰਤਜ਼ਾਮ ਕਰੋ"; +"settings.rateApp" = "ਐਪ ਨੂੰ ਦਰਜਾ ਦਿਓ"; +"settings.sendLogFile" = "ਲਾਗ ਫਾਇਲ ਭੇਝੋ"; +"settings.shortcutsGuide" = "ਸ਼ਾਰਟਕੱਟ ਗਾਈਡ"; +"settings.unlockFullVersion" = "ਪੂਰਾ ਵਰਜ਼ਨ ਅਣ-ਲਾਕ ਕਰੋ"; + +"s3Authentication.displayName" = "ਦਿਖਾਉਣ ਲਈ ਨਾਂ"; +"s3Authentication.accessKey" = "ਪਹੁੰਚ ਕੁੰਜੀ"; +"s3Authentication.secretKey" = "ਗੁਪਤ ਕੁੰਜੀ"; +"s3Authentication.existingBucket" = "ਮੌਜੂਦਾ ਡੱਬਾ"; +"s3Authentication.endpoint" = "ਐਂਡ-ਪੁਆਇੰਟ"; +"s3Authentication.region" = "ਖੇਤਰ"; +"s3Authentication.error.invalidCredentials" = "ਗਲਤ ਸਨਦਾਂ।"; + +"trialStatus.active" = "ਸਰਗਰਮ"; +"trialStatus.expired" = "ਮਿਆਦ ਪੁੱਗ ਗਈ"; "unlockVault.button.unlock" = "ਅਣ-ਲਾਕ ਕਰੋ"; +"unlockVault.button.unlockVia" = "%@ ਰਾਹੀਂ ਅਣ-ਲਾਕ"; +"unlockVault.password.footer" = "\"%@\" ਲਈ ਪਾਸਵਰਡ ਦਿਓ।"; +"unlockVault.enableBiometricalUnlock.switch" = "%@ ਸਮਰੱਥ ਕਰੋ"; +"unlockVault.enableBiometricalUnlock.footer" = "ਆਪਣੇ ਪਾਸਵਰਡ ਨਾਲ ਆਪਣੇ ਵਾਲਟ ਨੂੰ ਅਣ-ਲਾਕ ਕਰਨ ਦੀ ਬਜਾਏ ਇਸ ਨੂੰ %@ ਰਾਹੀਂ ਅਣ-ਲਾਕ ਕਰ ਸਕਦੇ ਹੋ।"; +"unlockVault.evaluatePolicy.reason" = "ਆਪਣੇ ਵਾਲਟ ਨੂੰ ਅਣ-ਲਾਕ ਕਰੋ"; +"unlockVault.progress" = "…ਅਣ-ਲਾਕ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"; +"untrustedTLSCertificate.add" = "ਭਰੋਸਾ ਕਰੋ"; +"untrustedTLSCertificate.dismiss" = "ਭਰੋਸਾ ਨਾ ਕਰੋ"; + +"upgrade.title" = "ਅੱਪਗਰੇਡ ਪੇਸ਼ਕਸ਼"; +"upgrade.notEligible.alert.title" = "ਅੱਪਗਰੇਡ ਫੇਲ੍ਹ ਹੈ"; +"upgrade.info" = "ਵਰਜ਼ਨ ਇੱਕ ਤੋਂ ਲੈ ਕੇ Cryptomator ਉੱਤੇ ਭਰੋਸਾ ਕਰਨ ਲਈ ਧੰਨਵਾਦ ਹੈ। ਤੁਹਾਡੇ ਵਰਗੇ ਸਮਰਪਿਤ ਵਰਤੋਂਕਾਰ ਮੁਫ਼ਤ ਅੱਪਗਰੇਡ ਲਈ ਪਾਤਰ ਹਨ।"; + +"urlSession.error.httpError.401" = "ਗਲਤ ਵਰਤੋਂਕਾਰ ਨਾਂ ਅਤੇ/ਜਾਂ ਪਾਸਵਰਡ ਹੈ।"; "vaultDetail.button.changeVaultPassword" = "ਪਾਸਵਰਡ ਬਦਲੋ"; +"vaultDetail.button.lock" = "ਹੁਣੇ ਲਾਕ ਕਰੋ"; +"vaultDetail.button.moveVault" = "ਭੇਜੋ"; +"vaultDetail.button.removeVault" = "ਵਾਲਟ ਸੂਚੀ ਵਿੱਚੋਂ ਹਟਾਓ"; +"vaultDetail.button.renameVault" = "ਨਾਂ ਬਦਲੋ"; +"vaultDetail.moveVault.progress" = "…ਭੇਜਿਆ ਜਾ ਰਿਹਾ ਹੈ"; +"vaultDetail.renameVault.progress" = "…ਨਾਂ ਬਦਲਿਆ ਜਾ ਰਿਹਾ ਹੈ"; + +"vaultList.header.title" = "ਵਾਲਟ"; +"vaultList.emptyList.message" = "ਵਾਲਟ ਜੋੜਨ ਲਈ ਇੱਥੇ ਛੂਹੋ"; +"vaultList.remove.alert.title" = "ਵਾਲਟ ਹਟਾਉਣਾ ਹੈ?"; + +"Retry Upload" = "ਅੱਪਲੋਡ ਦੀ ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ"; +"Clear from Cache" = "ਕੈਸ਼ ਤੋਂ ਮਿਟਾਓ"; diff --git a/SharedResources/pl.lproj/Localizable.strings b/SharedResources/pl.lproj/Localizable.strings index f071d69f6..a99e191e2 100644 --- a/SharedResources/pl.lproj/Localizable.strings +++ b/SharedResources/pl.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Włącz"; "common.button.next" = "Dalej"; "common.button.ok" = "OK"; +"common.button.refresh" = "Odśwież"; +"common.button.register" = "Zarejestruj"; "common.button.remove" = "Usuń"; "common.button.retry" = "Ponów"; "common.button.signOut" = "Wyloguj"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Gdzie znajduje się sejf?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator wykrył sejf \"%@\".\nCzy chcesz dodać ten sejf?"; "addVault.openExistingVault.detectedMasterkey.add" = "Dodaj ten sejf"; +"addVault.openExistingVault.downloadVault.progress" = "Pobieranie sejfu…"; "addVault.openExistingVault.password.footer" = "Wprowadź hasło dla \"%@\"."; "addVault.openExistingVault.progress" = "Dodawanie sejfu…"; "addVault.success.info" = "Dodano sejf \"%@\".\nUzyskaj dostęp do tego sejfu poprzez aplikację Pliki."; @@ -110,6 +113,18 @@ "getFolderIntent.error.missingPath" = "Nie podano ścieżki. Proszę podać prawidłową ścieżkę do folderu."; "getFolderIntent.error.noVaultSelected" = "Nie wybrano sejfu."; + +"hubAuthentication.title" = "Hub sejfów"; +"hubAuthentication.accessNotGranted" = "Twoje urządzenie nie zostało jeszcze upoważnione do dostępu do tego sejfu. Poproś właściciela sejfu o autoryzację."; +"hubAuthentication.licenseExceeded" = "Twoja instancja Hub ma nieprawidłową licencję. Poproś administratora Hub o uaktualnienie lub odnowienie licencji."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nazwa urządzenia"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Wygląda że jest to pierwszy dostęp do Huba z tego urządzenia. Aby zidentyfikować go w celu autoryzacji dostępu, musisz nazwać to urządzenie."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Twój Klucz Konta jest wymagany do zalogowania się z nowych aplikacji lub przeglądarek. Można go znaleźć w twoim profilu."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Rejestracja urządzenia powiodła się"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Aby dostać się do sejfu, Twoje urządzenie musi być autoryzowane przez właściciela sejfu."; +"hubAuthentication.requireAccountInit.alert.title" = "Wymagane działanie"; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Przejdź do profilu"; + "intents.saveFile.missingFile" = "Wskazany plik nie jest prawidłowy."; "intents.saveFile.invalidFolder" = "Wskazany folder jest niewłaściwy."; "intents.saveFile.missingTemporaryFolder" = "Nie udało się utworzyć katalogu tymczasowego."; diff --git a/SharedResources/pt-BR.lproj/Localizable.strings b/SharedResources/pt-BR.lproj/Localizable.strings index c2800f8ae..34160fc48 100644 --- a/SharedResources/pt-BR.lproj/Localizable.strings +++ b/SharedResources/pt-BR.lproj/Localizable.strings @@ -15,12 +15,14 @@ "common.button.confirm" = "Confirmar"; "common.button.create" = "Criar"; "common.button.createFolder" = "Criar pasta"; -"common.button.done" = "Pronto"; +"common.button.done" = "Concluído"; "common.button.download" = "Baixar"; "common.button.edit" = "Editar"; "common.button.enable" = "Habilitar"; "common.button.next" = "Próximo"; "common.button.ok" = "Ok"; +"common.button.refresh" = "Atualizar"; +"common.button.register" = "Registrar"; "common.button.remove" = "Remover"; "common.button.retry" = "Tentar Novamente"; "common.button.signOut" = "Finalizar sessão"; @@ -43,7 +45,7 @@ "addVault.createNewVault.setVaultName.header.title" = "Escolha um nome para o cofre."; "addVault.createNewVault.setVaultName.cells.name" = "Nome do Cofre"; "addVault.createNewVault.setVaultName.error.emptyVaultName" = "O nome do cofre não pode estar vazio."; -"addVault.createNewVault.chooseCloud.header" = "Onde o Cryptomator deve armazenar os arquivos encriptados do seu cofre?"; +"addVault.createNewVault.chooseCloud.header" = "Onde o Cryptomator deve armazenar os arquivos criptografados do seu cofre?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@já existe neste local. Escolha um nome ou local de um cofre diferente."; "addVault.createNewVault.detectedMasterkey.text" = "O Cryptomator detectou um cofre existente neste local.\nPara criar um novo cofre, por favor, volte e escolha uma pasta diferente."; "addVault.createNewVault.password.enterPassword.header" = "Digite uma nova senha."; @@ -54,17 +56,18 @@ "addVault.createNewVault.password.error.nonMatchingPasswords" = "As senhas não coincidem."; "addVault.createNewVault.password.error.tooShortPassword" = "A senha deve conter pelo menos 8 caracteres."; "addVault.createNewVault.progress" = "Criando Cofre…"; -"addVault.openExistingVault.title" = "Abrir Cofre"; +"addVault.openExistingVault.title" = "Abrir Cofre Existente"; "addVault.openExistingVault.chooseCloud.header" = "Onde o cofre está localizado?"; "addVault.openExistingVault.detectedMasterkey.text" = "O Cryptomator detectou o cofre \"%@\".\nVocê gostaria de adicionar este cofre?"; "addVault.openExistingVault.detectedMasterkey.add" = "Adicionar Este Cofre"; +"addVault.openExistingVault.downloadVault.progress" = "Baixando o cofre…"; "addVault.openExistingVault.password.footer" = "Digite a senha para \"%@\"."; "addVault.openExistingVault.progress" = "Adicionando Cofre…"; -"addVault.success.info" = "Cofre \"%@\" adicionado com sucesso.\nAcesse-o através do aplicativo Arquivos."; +"addVault.success.info" = "Cofre \"%@\" adicionado com sucesso.\nAcesse-o pelo aplicativo Arquivos."; "addVault.success.footer" = "Se você ainda não o fez, ative o Cryptomator no aplicativo Arquivos."; "biometryType.faceID" = "Face ID"; -"biometryType.touchID" = "ID do toque"; +"biometryType.touchID" = "Touch ID"; "changePassword.error.invalidOldPassword" = "Senha atual incorreta. Favor tentar novamente."; "changePassword.header.currentPassword.title" = "Digite a senha atual."; @@ -110,6 +113,15 @@ "getFolderIntent.error.missingPath" = "Nenhum caminho foi fornecido. Por favor, forneça um caminho válido para o qual a pasta deve ser retornada."; "getFolderIntent.error.noVaultSelected" = "Nenhum cofre foi selecionado."; + +"hubAuthentication.title" = "Cofre do Hub"; +"hubAuthentication.accessNotGranted" = "Seu dispositivo ainda não foi autorizado a acessar este cofre. Peça ao proprietário do cofre para autorizá-lo."; +"hubAuthentication.licenseExceeded" = "Sua instância do Cryptomator Hub possui uma licença inválida. Por favor, informe um administrador do Hub para atualizar ou renovar a licença."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nome do dispositivo"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Parece ser o primeiro acesso do Hub a partir deste dispositivo. Para identificá-lo para autorização de acesso, você precisa nomear este dispositivo."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Dispositivo registrado com sucesso"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Para acessar o cofre, seu dispositivo precisa ser autorizado pelo proprietário do cofre."; + "intents.saveFile.missingFile" = "O arquivo fornecido não é válido."; "intents.saveFile.invalidFolder" = "A pasta fornecida não é válida."; "intents.saveFile.missingTemporaryFolder" = "Falha ao criar uma pasta temporária."; @@ -148,7 +160,7 @@ "purchase.expiredTrial" = "Seu período de teste expirou."; "purchase.footer.privacyPolicy" = "Política de Privacidade"; "purchase.footer.termsOfUse" = "Termos de Uso"; -"purchase.header.feature.familySharing" = "Compartilhamento de família"; +"purchase.header.feature.familySharing" = "Compartilhamento familiar"; "purchase.header.feature.openSource" = "Desenvolvimento de código aberto"; "purchase.header.feature.writeAccess" = "Acesso de escrita aos seus cofres"; "purchase.product.donateAndUpgrade" = "Doe e faça um “upgrade”"; @@ -207,13 +219,13 @@ "snapshots.main.vault4" = "/Viagem pra Califórnia"; "s3Authentication.displayName" = "Nome de Exibição"; -"s3Authentication.accessKey" = "Chave de acesso"; +"s3Authentication.accessKey" = "Chave de Acesso"; "s3Authentication.secretKey" = "Chave Secreta"; -"s3Authentication.existingBucket" = "Bucket do nome de domínio na URL existente"; -"s3Authentication.endpoint" = "URL para autenticação no Amazon S3 — “Endpoint”"; +"s3Authentication.existingBucket" = "Bucket existente"; +"s3Authentication.endpoint" = "Endpoint"; "s3Authentication.region" = "Região"; "s3Authentication.error.invalidCredentials" = "Credenciais inválidas."; -"s3Authentication.error.invalidEndpoint" = "O Endpoint — “link” — fornecido não coincide com o formato de uma URL válida no Amazon S3."; +"s3Authentication.error.invalidEndpoint" = "O endpoint fornecido não coincide com o formato de uma URL."; "trialStatus.active" = "Ativo"; "trialStatus.expired" = "Expirado"; @@ -279,7 +291,7 @@ Para mover o seu cofre, por favor, volte e escolha uma pasta diferente."; "vaultProviderFactory.error.unsupportedVaultVersion" = "A versão do Cofre %ld não é suportada. Este cofre foi criado com uma versão mais antiga ou mais recente do Cryptomator."; "webDAVAuthentication.httpConnection.alert.title" = "Usar HTTPS?"; -"webDAVAuthentication.httpConnection.alert.message" = "O uso de HTTP é inseguro. Recomendamos a utilização de HTTPS. Se você souber os riscos, você pode continuar com o HTTP."; +"webDAVAuthentication.httpConnection.alert.message" = "O uso de HTTP é inseguro. Recomendamos utilizar HTTPS. Se você conhecer os riscos, pode continuar com HTTP."; "webDAVAuthentication.httpConnection.change" = "Mudar para HTTPS"; "webDAVAuthentication.httpConnection.continue" = "Manter HTTP"; diff --git a/SharedResources/pt.lproj/Localizable.strings b/SharedResources/pt.lproj/Localizable.strings index acb66d590..998d175f9 100644 --- a/SharedResources/pt.lproj/Localizable.strings +++ b/SharedResources/pt.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Ativar"; "common.button.next" = "Seguinte"; "common.button.ok" = "OK"; +"common.button.refresh" = "Atualizar"; +"common.button.register" = "Registo"; "common.button.remove" = "Remover"; "common.button.retry" = "Tentar de novo"; "common.button.signOut" = "Terminar Sessão"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Onde está o cofre localizado?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator detetou o cofre \"%@\".\nGostaria de adicionar este cofre?"; "addVault.openExistingVault.detectedMasterkey.add" = "Adicionar Este Cofre"; +"addVault.openExistingVault.downloadVault.progress" = "Descarregando o cofre…"; "addVault.openExistingVault.password.footer" = "Insira a palavra-passe para \"%@\"."; "addVault.openExistingVault.progress" = "A Adicionar Cofre…"; "addVault.success.info" = "Cofre \"%@\" adicionado com sucesso.\nAccesse este cofre através da aplicação Ficherios."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Trajeto não foi fornecido. Por favor, forneça um trajeto válido para o qual a pasta deve ser retornada."; "getFolderIntent.error.noVaultSelected" = "Nenhum cofre selecionado."; + +"hubAuthentication.title" = "Cofre do Hub"; +"hubAuthentication.accessNotGranted" = "O seu dispositivo ainda não foi autorizado a aceder a este cofre. Peça ao proprietário do cofre para o autorizar."; +"hubAuthentication.licenseExceeded" = "A entidade do seu Cryptomator Hub tem uma licença inválida. Por favor, informe o administrador do Hub para atualizar ou renovar a licença."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nome do dispositivo"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Parece ser o primeiro acesso ao Hub a partir deste dispositivo. Para identificá-lo para autorização de acesso, é preciso dar um nome a este dispositivo."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "A sua chave de conta é necessária para iniciar sessão em novos aplicativos ou navegadores. Ela pode ser encontrada no seu perfil."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Dispositivo registado com sucesso"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Para aceder ao cofre, o seu dispositivo precisa de ser autorizado pelo proprietário do cofre."; +"hubAuthentication.requireAccountInit.alert.title" = "Ação necessária"; +"hubAuthentication.requireAccountInit.alert.message" = "Para continuar, por favor conclua as etapas exigidas no seu perfil de utilizador no Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Vá para o Perfil"; + "intents.saveFile.missingFile" = "O ficheiro especificado não é válido."; "intents.saveFile.invalidFolder" = "A pasta especificada não é válida."; "intents.saveFile.missingTemporaryFolder" = "Não foi possível criar a pasta temporária."; diff --git a/SharedResources/ro.lproj/Localizable.strings b/SharedResources/ro.lproj/Localizable.strings index 250210c77..96bba5938 100644 --- a/SharedResources/ro.lproj/Localizable.strings +++ b/SharedResources/ro.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Activează"; "common.button.next" = "Următor"; "common.button.ok" = "OK"; +"common.button.refresh" = "Împrospătează"; "common.button.remove" = "Șterge"; "common.button.retry" = "Încercați din nou"; "common.button.signOut" = "Deconectare"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "Nu a fost furnizată nicio locație. Vă rugăm să alegeți o cale validă pentru care un dosar trebuie returnat."; "getFolderIntent.error.noVaultSelected" = "Nici un seif nu a fost ales."; +"hubAuthentication.accessNotGranted" = "Dispozitivul dvs. nu a fost autorizat să acceseze acest seif. Solicitați proprietarului seifului să va autorizeze accesul."; +"hubAuthentication.licenseExceeded" = "Instanța Hub are o licență invalidă. Vă rugăm să informați un administrator Hub să actualizeze sau să reînnoiască licența."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Numele dispozitivului"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Pentru a accesa acest seif, dispozitivul dvs. trebuie să fie autorizat de proprietarul seifului."; + "intents.saveFile.missingFile" = "Fișierul ales nu este valid."; "intents.saveFile.invalidFolder" = "Dosarul ales nu este valid."; "intents.saveFile.missingTemporaryFolder" = "Nu s-a putut crea dosarului temporar."; @@ -265,7 +271,7 @@ "vaultDetail.moveVault.detectedMasterkey.text" = "Cryptomator a detectat un seif în această locație.\nPentru a crea un nou seif, vă rugăm să alegeți un folder diferit."; "vaultDetail.moveVault.progress" = "Se mută…"; "vaultDetail.removeVault.footer" = "Acest lucru va elimina doar seiful din lista de seifuri și nu va șterge niciun fișier criptat."; -"vaultDetail.renameVault.progress" = "In proces de redenumire…"; +"vaultDetail.renameVault.progress" = "În proces de redenumire…"; "vaultDetail.unlocked.footer" = "Seiful dvs. este în prezent deblocat în aplicația fișiere."; "vaultDetail.unlockVault.footer" = "Introduceți parola pentru \"%@\" pentru a o stoca în iOS keychain și pentru a activa %@."; diff --git a/SharedResources/ru.lproj/Localizable.strings b/SharedResources/ru.lproj/Localizable.strings index da8c44010..ab803903f 100644 --- a/SharedResources/ru.lproj/Localizable.strings +++ b/SharedResources/ru.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Включить"; "common.button.next" = "Далее"; "common.button.ok" = "OK"; +"common.button.refresh" = "Обновить"; +"common.button.register" = "Регистрация"; "common.button.remove" = "Удалить"; "common.button.retry" = "Повторить"; "common.button.signOut" = "Выйти"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Где находится хранилище?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator обнаружил хранилище \"%@\".\nДобавить это хранилище?"; "addVault.openExistingVault.detectedMasterkey.add" = "Добавить это хранилище"; +"addVault.openExistingVault.downloadVault.progress" = "Загрузка хранилища…"; "addVault.openExistingVault.password.footer" = "Введите пароль для \"%@\"."; "addVault.openExistingVault.progress" = "Добавление хранилища…"; "addVault.success.info" = "Хранилище \"%@\" добавлено.\nДоступ к нему - через приложение \"Файлы\"."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Не указан путь. Укажите правильный путь, по которому должна быть возвращена папка."; "getFolderIntent.error.noVaultSelected" = "Не выбрано хранилище."; + +"hubAuthentication.title" = "Хаб-хранилище"; +"hubAuthentication.accessNotGranted" = "Ваше устройство ещё не авторизовано для доступа к этому хранилищу. Попросите владельца хранилища предоставить разрешение."; +"hubAuthentication.licenseExceeded" = "У вашего Cryptomator Hub неверная лицензия. Попросите Hub администратора обновить или продлить лицензию."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Имя устройства"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Похоже, это первый доступ к Hub с данного устройства. Чтобы идентифицировать его для предоставления доступа, нужно дать устройству имя."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Для входа в систему из новых приложений или браузеров требуется ключ вашего аккаунта. Он находится в вашем профиле."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Устройство успешно зарегистрировано"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Для доступа к хранилищу ваше устройство должно быть авторизовано владельцем хранилища."; +"hubAuthentication.requireAccountInit.alert.title" = "Требуется действие"; +"hubAuthentication.requireAccountInit.alert.message" = "Для продолжения выполните необходимые шаги в вашем профиле пользователя Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Перейти в профиль"; + "intents.saveFile.missingFile" = "Выбран некорректный файл."; "intents.saveFile.invalidFolder" = "Выбрана некорректная папка."; "intents.saveFile.missingTemporaryFolder" = "Не удалось создать папку для временных файлов."; diff --git a/SharedResources/sk.lproj/Localizable.strings b/SharedResources/sk.lproj/Localizable.strings index c1c355473..c39c398e9 100644 --- a/SharedResources/sk.lproj/Localizable.strings +++ b/SharedResources/sk.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Povoliť"; "common.button.next" = "Ďalej"; "common.button.ok" = "OK"; +"common.button.refresh" = "Obnoviť"; +"common.button.register" = "Registrovať"; "common.button.remove" = "Odstrániť"; "common.button.retry" = "Skúsiť znovu"; "common.button.signOut" = "Odhlásiť"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Kde je trezor umiestnený?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator zdetekoval trezor \"%@\".\n Prejete si pridať tento trezor?"; "addVault.openExistingVault.detectedMasterkey.add" = "Pridať tento trezor"; +"addVault.openExistingVault.downloadVault.progress" = "Sťahovanie trezora…"; "addVault.openExistingVault.password.footer" = "Zadajte heslo pre \"%@\"."; "addVault.openExistingVault.progress" = "Pridávanie trezora…"; "addVault.success.info" = "Úspešne pridaný trezor \"%@\".\n Sprístupnený tento trezor prostredníctvom Súborovej aplikácie."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Nebola poskytnutá žiadna cesta. Prosím poskytnite platnú cestu pre adresár, ktorý by mal byť vrátený."; "getFolderIntent.error.noVaultSelected" = "Nebol zvolený žiaden trezor."; + +"hubAuthentication.title" = "Hub trezora"; +"hubAuthentication.accessNotGranted" = "Vaše zaradenie zatiaľ ešte nebolo autorizované pre pristúp tohto trezora. Požiadajte majiteľa trezora o autorizovanie."; +"hubAuthentication.licenseExceeded" = "Vaša inštancia Cryptomator Hub-u má neplatnú licenciu. Prosím informujte Hub administrátora pre aktualizáciu alebo obnovenie licencie."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Názov zariadenia"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Zdá sa, že ide o prvý prístup k Hub-u z tohto zariadenia. Z dôvodu identifikácie prístupovej autorizácie, je potrebné pomenovať toto zariadenie."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Váš kľúč účtu je vyžadovaný pre prihlásenie z nových aplikácii alebo prehliadačov. Môžete ho nájsť vo vašom profile."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Registrácia zariadenia úspešná"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Sprístupniť trezor, Vaše zariadenie musí byť autorizované vlastníkom trezora."; +"hubAuthentication.requireAccountInit.alert.title" = "Vyžaduje sa akcia"; +"hubAuthentication.requireAccountInit.alert.message" = "Pre pokračovanie, prosím splňte kroky požadované vo Vašom užívateľskom Hub profile."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Prejsť na profil"; + "intents.saveFile.missingFile" = "Poskytnutý súbor nie je platný."; "intents.saveFile.invalidFolder" = "Poskytnutý adresár nie je platný."; "intents.saveFile.missingTemporaryFolder" = "Nepodarilo sa vytvoriť dočasný adresár."; diff --git a/SharedResources/sl.lproj/Localizable.strings b/SharedResources/sl.lproj/Localizable.strings index 3b5442ffb..3510183c6 100644 --- a/SharedResources/sl.lproj/Localizable.strings +++ b/SharedResources/sl.lproj/Localizable.strings @@ -7,6 +7,7 @@ "common.button.edit" = "Uredi"; "common.button.next" = "Naslednji"; "common.button.ok" = "V redu"; +"common.button.refresh" = "Osveži"; "common.button.remove" = "Odstrani"; "common.button.retry" = "Poizkusi znova"; "common.cells.url" = "URL naslov"; diff --git a/SharedResources/sv.lproj/Localizable.strings b/SharedResources/sv.lproj/Localizable.strings index ab36b2083..f40d8decc 100644 --- a/SharedResources/sv.lproj/Localizable.strings +++ b/SharedResources/sv.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Aktivera"; "common.button.next" = "Nästa"; "common.button.ok" = "OK"; +"common.button.refresh" = "Uppdatera"; +"common.button.register" = "Skapa konto"; "common.button.remove" = "Ta bort"; "common.button.retry" = "Försök igen"; "common.button.signOut" = "Logga ut"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Var ligger valvet?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator upptäckte valvet \"%@\".\nVill du lägga till detta valv?"; "addVault.openExistingVault.detectedMasterkey.add" = "Lägg till detta valv"; +"addVault.openExistingVault.downloadVault.progress" = "Laddar ner valv…"; "addVault.openExistingVault.password.footer" = "Ange lösenord för \"%@\"."; "addVault.openExistingVault.progress" = "Lägger till valvet…"; "addVault.success.info" = "Framgångsrikt lagt till valvet \"%@\".\nKom åt detta valv via Fil-appen."; @@ -110,6 +113,15 @@ "getFolderIntent.error.missingPath" = "Ingen sökväg angavs. Vänligen ange en giltig sökväg för vilken en mapp ska returneras."; "getFolderIntent.error.noVaultSelected" = "Inget valv har valts."; + +"hubAuthentication.title" = "Hubb valv"; +"hubAuthentication.accessNotGranted" = "Din enhet har ännu inte behörighet att komma åt detta valv. Be valvägaren att godkänna det."; +"hubAuthentication.licenseExceeded" = "Din Cryptomator Hub-instans har en ogiltig licens. Vänligen informera en Hub administratör för att uppgradera eller förnya licensen."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Enhetsnamn"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Detta verkar vara den första Hub-åtkomsten från den här enheten. För att identifiera den för åtkomstbehörighet, måste du namnge den här enheten."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Enhetsregistrering lyckades"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "För att komma åt valvet måste din enhet godkännas av valvägaren."; + "intents.saveFile.missingFile" = "Den angivna filen är inte giltig."; "intents.saveFile.invalidFolder" = "Den angivna mappen är inte giltig."; "intents.saveFile.missingTemporaryFolder" = "Det gick inte att skapa en temporär mapp."; diff --git a/SharedResources/sw-TZ.lproj/Localizable.strings b/SharedResources/sw-TZ.lproj/Localizable.strings index aa43f831b..c75a0bd30 100644 --- a/SharedResources/sw-TZ.lproj/Localizable.strings +++ b/SharedResources/sw-TZ.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Wezesha"; "common.button.next" = "Nyingine"; "common.button.ok" = "Sawa"; +"common.button.refresh" = "Onesha upya"; "common.button.remove" = "Ondoa"; "common.button.retry" = "Jaribu tena"; "common.button.signOut" = "Ondoka"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "Hakuna njia iliyotolewa. Tafadhali toa njia halali ambayo folda inapaswa kurejeshwa."; "getFolderIntent.error.noVaultSelected" = "Hakuna kuba iliyochaguliwa."; +"hubAuthentication.accessNotGranted" = "Kifaa chako bado hakijaidhinishwa kufikia kuba hii. Uliza mwenye kuba aidhinishe."; +"hubAuthentication.licenseExceeded" = "Mfano wako wa Cryptomator Hub una leseni batili. Tafadhali mjulishe msimamizi wa Hub ili kuboresha au kusasisha leseni."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Jina la Kifaa"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Ili kufikia kuba, kifaa chako kinahitaji kuidhinishwa na mmiliki wa kuba."; + "intents.saveFile.missingFile" = "Faili iliyotolewa si sahihi."; "intents.saveFile.invalidFolder" = "Faili iliyotolewa si sahihi."; "intents.saveFile.missingTemporaryFolder" = "Imeshindwa kuunda folda ya muda."; diff --git a/SharedResources/ta.lproj/Localizable.strings b/SharedResources/ta.lproj/Localizable.strings index e721d2546..a6cb77c22 100644 --- a/SharedResources/ta.lproj/Localizable.strings +++ b/SharedResources/ta.lproj/Localizable.strings @@ -8,6 +8,7 @@ "common.button.enable" = "இயக்கு"; "common.button.next" = "அடுத்து"; "common.button.ok" = "சரி"; +"common.button.refresh" = "புதுப்பி"; "common.button.remove" = "நீக்கு"; "common.button.retry" = "மீண்டும் முயற்சிக்கவும்"; "common.cells.url" = "URL"; diff --git a/SharedResources/te.lproj/Localizable.strings b/SharedResources/te.lproj/Localizable.strings index e0554efb6..95a4286ac 100644 --- a/SharedResources/te.lproj/Localizable.strings +++ b/SharedResources/te.lproj/Localizable.strings @@ -4,6 +4,7 @@ "common.button.edit" = "మార్పు"; "common.button.enable" = "ప్రారంభించు"; "common.button.ok" = "సరే"; +"common.button.refresh" = "రిఫ్రెష్ చేయండి"; "common.button.remove" = "తొలగించు"; "common.button.retry" = "మళ్ళీ చేయండి"; "common.cells.url" = "URL"; diff --git a/SharedResources/tr.lproj/Localizable.strings b/SharedResources/tr.lproj/Localizable.strings index dd9f6357d..59f13b30e 100644 --- a/SharedResources/tr.lproj/Localizable.strings +++ b/SharedResources/tr.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "Etkinleştir"; "common.button.next" = "İleri"; "common.button.ok" = "Tamam"; +"common.button.refresh" = "Yenile"; +"common.button.register" = "Kaydol"; "common.button.remove" = "Kaldır"; "common.button.retry" = "Yeniden dene"; "common.button.signOut" = "Çıkış Yap"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "Kasa nerede bulunuyor?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator \"%@\" kasasını algıladı.\nBu kasayı eklemek ister misiniz?"; "addVault.openExistingVault.detectedMasterkey.add" = "Bu Kasayı Ekle"; +"addVault.openExistingVault.downloadVault.progress" = "Kasa indiriliyor…"; "addVault.openExistingVault.password.footer" = "\"%@\" için şifre girin."; "addVault.openExistingVault.progress" = "Kasa ekleniyor…"; "addVault.success.info" = "\"%@\" kasası başarıyla eklendi.\nDosyalar uygulamasından bu kasaya erişebilirsiniz."; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "Dizin sağlanmadı. Lütfen bir klasörün döndürüleceği geçerli bir dizin sağlayın."; "getFolderIntent.error.noVaultSelected" = "Herhangi bir kasa seçili değil."; + +"hubAuthentication.title" = "Hub Kasası"; +"hubAuthentication.accessNotGranted" = "Cihazınıza henüz bu kasaya erişim yetkisi verilmedi. Kasa sahibinden yetkilendirmesini isteyin."; +"hubAuthentication.licenseExceeded" = "Cryptomator Hub örneğinizde geçersiz bir lisans var. Lisansı yükseltmesi veya yenilemesi için lütfen bir Hub yöneticisini bilgilendirin."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Cihaz adı"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "Bu cihazdan ilk Hub erişimi gibi görünüyor. Erişim yetkilendirmesini tanımlamak için bu cihazı isimlendirmeniz gerekir."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Yeni uygulamalardan veya tarayıcılardan giriş yapmak için Hesap Anahtarınız gereklidir. Profilinizde bulunabilir."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Cihaz Kaydı Başarılı"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Kasaya erişmek için cihazınızın kasa sahibi tarafından yetkilendirilmesi gerekir."; +"hubAuthentication.requireAccountInit.alert.title" = "Eylem Gerekiyor"; +"hubAuthentication.requireAccountInit.alert.message" = "Devam etmek için lütfen Hub kullanıcı profilinizde gerekli adımları tamamlayın."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Profile Git"; + "intents.saveFile.missingFile" = "Sağlanan dosya geçersiz."; "intents.saveFile.invalidFolder" = "Sağlanan klasör geçersiz."; "intents.saveFile.missingTemporaryFolder" = "Geçici klasör oluşturulamadı."; diff --git a/SharedResources/uk.lproj/Localizable.strings b/SharedResources/uk.lproj/Localizable.strings index 32912f94b..dad5c5a84 100644 --- a/SharedResources/uk.lproj/Localizable.strings +++ b/SharedResources/uk.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "Увімкнути"; "common.button.next" = "Далі"; "common.button.ok" = "Гаразд"; +"common.button.refresh" = "Оновити"; "common.button.remove" = "Прибрати"; "common.button.retry" = "Повторити"; "common.button.signOut" = "Вийти"; @@ -58,6 +59,14 @@ "fileProvider.onboarding.button.openCryptomator" = "Відкрити Cryptomator"; "fileProvider.error.biometricalAuthWrongPassword.title" = "Невірний пароль"; "fileProvider.error.unlockButton" = "Розблокувати"; +"hubAuthentication.accessNotGranted" = "Ваш пристрій ще не має прав доступу до цього vault. Попросіть власника vault надати їх."; +"hubAuthentication.licenseExceeded" = "У вашого Cryptomator Hub недійсна ліцензія. Будь ласка, повідомте адміністратору Hub, що потрібно оновити або продовжити ліцензію."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Назва пристрою"; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Ваш Account Key необхідний для входу в систему з нових програм або браузерів. Його можна знайти в профілі."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Щоб отримати доступ до vault, ваш пристрій повинен бути авторизований власником vault."; +"hubAuthentication.requireAccountInit.alert.title" = "Необхідна дія"; +"hubAuthentication.requireAccountInit.alert.message" = "Щоб продовжити, будь ласка, виконайте необхідні кроки у вашому профілі користувача Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Перейти до профілю"; "keepUnlockedDuration.auto.shortDisplayName" = "Автоматично"; "localFileSystemAuthentication.openExistingVault.button" = "Виберіть папку сховища"; @@ -91,6 +100,7 @@ "settings.clearCache" = "Очистити кеш"; "settings.cloudServices" = "Хмарні сервіси"; "settings.debugMode" = "Режим вiдладки"; +"settings.debugMode.alert.message" = "У цьому режимі чутливі дані можуть бути записані в файл журналу на вашому пристрої (наприклад, імена файлів і шляхи). Паролі, файли cookie тощо не включаються.\n\nНе забудьте вимкнути режим налагодження за першої нагоди."; "settings.rateApp" = "Оцінити додаток"; "settings.sendLogFile" = "Надіслати лог-файл"; "settings.unlockFullVersion" = "Розблокувати повну версію"; diff --git a/SharedResources/vi.lproj/Localizable.strings b/SharedResources/vi.lproj/Localizable.strings index 4f8700fa5..b3a4d9e41 100644 --- a/SharedResources/vi.lproj/Localizable.strings +++ b/SharedResources/vi.lproj/Localizable.strings @@ -9,6 +9,7 @@ "common.button.enable" = "Bật"; "common.button.next" = "Tiếp"; "common.button.ok" = "OK"; +"common.button.refresh" = "Làm mới"; "common.button.remove" = "Xóa"; "common.button.retry" = "Thử lại"; "common.cells.password" = "Mật khẩu"; @@ -21,13 +22,25 @@ "addVault.createNewVault.chooseCloud.header" = "Cryptomator nên lưu trữ các tệp được mã hóa trong vault của bạn ở đâu?"; "addVault.createNewVault.password.confirmPassword.alert.message" = "QUAN TRỌNG: Nếu bạn quên mật khẩu, không có cách nào để khôi phục dữ liệu của bạn."; "addVault.openExistingVault.title" = "Mở Vault Hiện Có"; + +"biometryType.faceID" = "Face ID"; +"biometryType.touchID" = "Touch ID"; "fileProvider.error.unlockButton" = "Mở khoá"; +"hubAuthentication.accessNotGranted" = "Thiết bị của bạn chưa được phép truy cập vault này. Yêu cầu chủ sở hữu cấp phép."; +"hubAuthentication.licenseExceeded" = "Phiên bản Cryptomator Hub của bạn có giấy phép không hợp lệ. Vui lòng thông báo cho quản trị viên Hub để nâng cấp hoặc gia hạn giấy phép."; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "Tên thiết bị"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Để truy cập vault, thiết bị của bạn cần được chủ sở hữu cho phép."; "purchase.retry.button" = "Thử lại"; "settings.title" = "Cài đặt"; "settings.clearCache" = "Xóa bộ nhớ Cache"; +"settings.debugMode.alert.message" = "Ở chế độ này, dữ liệu nhạy cảm có thể được ghi vào tệp nhật ký trên thiết bị của bạn (ví dụ: tên tệp và đường dẫn). Mật khẩu, cookie, v.v. được loại trừ rõ ràng.\n\nHãy nhớ tắt chế độ gỡ lỗi càng sớm càng tốt."; "s3Authentication.displayName" = "Tên hiển thị"; +"s3Authentication.accessKey" = "Khoá Truy cập"; +"s3Authentication.secretKey" = "Khoá Bí mật"; +"s3Authentication.existingBucket" = "Bucket Hiện có"; +"s3Authentication.endpoint" = "Điểm cuối"; "s3Authentication.region" = "Khu vực"; "unlockVault.button.unlock" = "Mở khoá"; diff --git a/SharedResources/zh-HK.lproj/Localizable.strings b/SharedResources/zh-HK.lproj/Localizable.strings index 97a46b73b..ee2ee3ac8 100644 --- a/SharedResources/zh-HK.lproj/Localizable.strings +++ b/SharedResources/zh-HK.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "啟用"; "common.button.next" = "繼續"; "common.button.ok" = "確認"; +"common.button.refresh" = "重新整理"; "common.button.remove" = "移除"; "common.button.retry" = "重試"; "common.button.signOut" = "登出"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "沒有提供路徑。請提供應返回資料夾的有效路徑。"; "getFolderIntent.error.noVaultSelected" = "未選擇任何加密庫。"; +"hubAuthentication.accessNotGranted" = "您的設備權限尚未允許存取加密庫,請聯絡加密庫擁有者"; +"hubAuthentication.licenseExceeded" = "此 Cryptomator Hub 實例授權無效,請聯繫管理員升級或續訂授權。"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "設備名稱"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "想讀取檔加密庫,你的設備需得到檔案庫擁有者的授權。"; + "intents.saveFile.missingFile" = "提供的檔案無效。"; "intents.saveFile.invalidFolder" = "提供的資料夾無效。"; "intents.saveFile.missingTemporaryFolder" = "無法建立臨時資料夾。"; diff --git a/SharedResources/zh-Hans.lproj/Localizable.strings b/SharedResources/zh-Hans.lproj/Localizable.strings index 695e47f50..3b016e7fe 100644 --- a/SharedResources/zh-Hans.lproj/Localizable.strings +++ b/SharedResources/zh-Hans.lproj/Localizable.strings @@ -21,6 +21,8 @@ "common.button.enable" = "启用"; "common.button.next" = "下一步"; "common.button.ok" = "确定"; +"common.button.refresh" = "刷新"; +"common.button.register" = "注册"; "common.button.remove" = "删除"; "common.button.retry" = "重试"; "common.button.signOut" = "退出登录"; @@ -46,7 +48,7 @@ "addVault.createNewVault.chooseCloud.header" = "Cryptomator 应该在哪里存储您保险库的加密文件?"; "addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" 已存在于此位置,请更换保险库名称或位置。"; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator 已在此路径检测到保险库。\n若想创建一个新的保险库,请返回并选择其他路径。"; -"addVault.createNewVault.password.enterPassword.header" = "输入新密码。"; +"addVault.createNewVault.password.enterPassword.header" = "输入新密码"; "addVault.createNewVault.password.confirmPassword.header" = "确认新密码。"; "addVault.createNewVault.password.confirmPassword.alert.title" = "确认密码?"; "addVault.createNewVault.password.confirmPassword.alert.message" = "重要提示:一旦您忘记密码,将永远无法恢复您的数据。"; @@ -58,6 +60,7 @@ "addVault.openExistingVault.chooseCloud.header" = "该保险库的路径在哪?"; "addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator 检测到保险库 \"%@\"。\n您想要添加它吗?"; "addVault.openExistingVault.detectedMasterkey.add" = "添加此保险库"; +"addVault.openExistingVault.downloadVault.progress" = "正在下载保险库…"; "addVault.openExistingVault.password.footer" = "输入 \"%@\" 的密码。"; "addVault.openExistingVault.progress" = "正在添加保险库……"; "addVault.success.info" = "成功添加保险库 \"%@\"。\n现在可通过文管应用访问它。"; @@ -68,7 +71,7 @@ "changePassword.error.invalidOldPassword" = "当前密码错误,请重试。"; "changePassword.header.currentPassword.title" = "请输入当前密码。"; -"changePassword.header.newPassword.title" = "输入新密码。"; +"changePassword.header.newPassword.title" = "输入新密码"; "changePassword.header.newPasswordConfirmation.title" = "确认新密码。"; "changePassword.progress" = "正在修改密码……"; @@ -110,6 +113,19 @@ "getFolderIntent.error.missingPath" = "未提供路径,请提供文件夹应转到的有效路径。"; "getFolderIntent.error.noVaultSelected" = "未选择任何保险库。"; + +"hubAuthentication.title" = "Hub 保险库"; +"hubAuthentication.accessNotGranted" = "您的设备尚未授权访问此保险库,请联系保险库所有者,"; +"hubAuthentication.licenseExceeded" = "此 Cryptomator Hub 实例许可证无效,请联系Hub管理员升级或者续订许可证。"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "设备名称"; +"hubAuthentication.deviceRegistration.deviceName.footer.title" = "这似乎是设备的首次 Hub 访问。为了识别它以进行访问授权,您需要命名此设备"; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "从新应用或浏览器登录需要您的账户密钥,您可以在个人中心找到它"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "注册设备成功"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "要访问保险库,设备需得到保险库所有者的授权。"; +"hubAuthentication.requireAccountInit.alert.title" = "需要操作"; +"hubAuthentication.requireAccountInit.alert.message" = "要继续,请完成 Hub 用户中心所需的步骤"; +"hubAuthentication.requireAccountInit.alert.actionButton" = "前往个人中心"; + "intents.saveFile.missingFile" = "提供的文件无效。"; "intents.saveFile.invalidFolder" = "提供的文件夹无效。"; "intents.saveFile.missingTemporaryFolder" = "无法创建临时文件夹。"; diff --git a/SharedResources/zh-Hant.lproj/Localizable.strings b/SharedResources/zh-Hant.lproj/Localizable.strings index 194b20c31..a396ab7fc 100644 --- a/SharedResources/zh-Hant.lproj/Localizable.strings +++ b/SharedResources/zh-Hant.lproj/Localizable.strings @@ -21,6 +21,7 @@ "common.button.enable" = "啟用"; "common.button.next" = "繼續"; "common.button.ok" = "確認"; +"common.button.refresh" = "重新整理"; "common.button.remove" = "移除"; "common.button.retry" = "重試"; "common.button.signOut" = "登出"; @@ -110,6 +111,11 @@ "getFolderIntent.error.missingPath" = "沒有提供路徑。請提供應返回資料夾的有效路徑。"; "getFolderIntent.error.noVaultSelected" = "未選擇任何加密檔案庫。"; +"hubAuthentication.accessNotGranted" = "您的設備權限尚未允許存取檔案庫,請聯絡檔案庫擁有者"; +"hubAuthentication.licenseExceeded" = "此 Cryptomator Hub 實例授權無效,請聯繫管理員升級或續訂授權。"; +"hubAuthentication.deviceRegistration.deviceName.cells.name" = "設備名稱"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "想讀取檔案庫,你的設備需得到檔案庫擁有者的授權。"; + "intents.saveFile.missingFile" = "提供的檔案無效。"; "intents.saveFile.invalidFolder" = "提供的資料夾無效。"; "intents.saveFile.missingTemporaryFolder" = "無法建立臨時資料夾。"; From 11d8c34a6e54f0735acbd24f286d396f93cb2c74 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Thu, 11 Jan 2024 10:16:05 +0100 Subject: [PATCH 85/92] Increased supported minimum Hub version, updated redirect URL --- .../CryptomatorHubAuthenticator+HubAuthenticating.swift | 2 +- .../CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift index f34c331bd..566b7ad99 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommon/CryptomatorHubAuthenticator+HubAuthenticating.swift @@ -29,7 +29,7 @@ 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: "org.cryptomator.ios:/hub/auth") else { throw HubAuthenticationError.invalidRedirectURL } let configuration = OIDServiceConfiguration(authorizationEndpoint: authorizationEndpoint, diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 0420202f9..e0d65ccef 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -44,7 +44,7 @@ public enum CryptomatorHubAuthenticatorError: Error { public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving { private static let scheme = "hub+" - private static let minimumHubVersion = 1 + private static let minimumHubVersion = 2 @Dependency(\.cryptomatorHubKeyProvider) private var cryptomatorHubKeyProvider public init() {} From 50fd566f52b8f585fdb6cc01e9cc9dfc48f9e217 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Thu, 11 Jan 2024 11:06:12 +0100 Subject: [PATCH 86/92] Added Periphery config [ci skip] --- .periphery.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .periphery.yml diff --git a/.periphery.yml b/.periphery.yml new file mode 100644 index 000000000..51e8f9779 --- /dev/null +++ b/.periphery.yml @@ -0,0 +1,8 @@ +project: Cryptomator.xcodeproj +schemes: +- Cryptomator +targets: +- Cryptomator +- CryptomatorFileProvider +- FileProviderExtension +- FileProviderExtensionUI From 2e4bef047b5b199d528e75a33b176a6eccead37f Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Thu, 11 Jan 2024 11:15:14 +0100 Subject: [PATCH 87/92] Preparing 2.5.0 [ci skip] --- Cryptomator.xcodeproj/project.pbxproj | 4 ++-- fastlane/changelog.txt | 4 ++-- fastlane/config/freemium/metadata/de-DE/release_notes.txt | 4 ++-- fastlane/config/freemium/metadata/en-US/release_notes.txt | 4 ++-- fastlane/config/premium/metadata/de-DE/release_notes.txt | 4 ++-- fastlane/config/premium/metadata/en-US/release_notes.txt | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 9f8817d8d..73d8f063e 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -3285,7 +3285,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 2.4.9; + MARKETING_VERSION = 2.5.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -3347,7 +3347,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 2.4.9; + MARKETING_VERSION = 2.5.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=200 -Xfrontend -warn-long-function-bodies=200"; diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index 995f6a12f..9b1d51ebb 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -1,2 +1,2 @@ -- Added Hebrew translation -- Fixed "not found" error when listing a directory with unreachable files in iCloud Drive (#313) \ No newline at end of file +- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332) +- New vaults are now created with GCM encryption by default (#288) \ No newline at end of file diff --git a/fastlane/config/freemium/metadata/de-DE/release_notes.txt b/fastlane/config/freemium/metadata/de-DE/release_notes.txt index 3e52e6c8b..764f2bb8c 100644 --- a/fastlane/config/freemium/metadata/de-DE/release_notes.txt +++ b/fastlane/config/freemium/metadata/de-DE/release_notes.txt @@ -1,2 +1,2 @@ -- Hebräische Übersetzung hinzugefügt -- Fehler "nicht gefunden" beim Auflisten eines Verzeichnisses mit nicht erreichbaren Dateien in iCloud Drive behoben (#313) \ No newline at end of file +- Unterstützung für Cryptomator Hub hinzugefügt (#315, #322, #323, #326, #329, #332) +- Neue Tresore werden nun standardmäßig mit GCM-Verschlüsselung erstellt (#288) \ No newline at end of file diff --git a/fastlane/config/freemium/metadata/en-US/release_notes.txt b/fastlane/config/freemium/metadata/en-US/release_notes.txt index 995f6a12f..9b1d51ebb 100644 --- a/fastlane/config/freemium/metadata/en-US/release_notes.txt +++ b/fastlane/config/freemium/metadata/en-US/release_notes.txt @@ -1,2 +1,2 @@ -- Added Hebrew translation -- Fixed "not found" error when listing a directory with unreachable files in iCloud Drive (#313) \ No newline at end of file +- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332) +- New vaults are now created with GCM encryption by default (#288) \ No newline at end of file diff --git a/fastlane/config/premium/metadata/de-DE/release_notes.txt b/fastlane/config/premium/metadata/de-DE/release_notes.txt index 3e52e6c8b..764f2bb8c 100644 --- a/fastlane/config/premium/metadata/de-DE/release_notes.txt +++ b/fastlane/config/premium/metadata/de-DE/release_notes.txt @@ -1,2 +1,2 @@ -- Hebräische Übersetzung hinzugefügt -- Fehler "nicht gefunden" beim Auflisten eines Verzeichnisses mit nicht erreichbaren Dateien in iCloud Drive behoben (#313) \ No newline at end of file +- Unterstützung für Cryptomator Hub hinzugefügt (#315, #322, #323, #326, #329, #332) +- Neue Tresore werden nun standardmäßig mit GCM-Verschlüsselung erstellt (#288) \ No newline at end of file diff --git a/fastlane/config/premium/metadata/en-US/release_notes.txt b/fastlane/config/premium/metadata/en-US/release_notes.txt index 995f6a12f..9b1d51ebb 100644 --- a/fastlane/config/premium/metadata/en-US/release_notes.txt +++ b/fastlane/config/premium/metadata/en-US/release_notes.txt @@ -1,2 +1,2 @@ -- Added Hebrew translation -- Fixed "not found" error when listing a directory with unreachable files in iCloud Drive (#313) \ No newline at end of file +- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332) +- New vaults are now created with GCM encryption by default (#288) \ No newline at end of file From e9e86502d032e28e0845efcbec4eec657406f3d0 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Thu, 11 Jan 2024 11:24:14 +0100 Subject: [PATCH 88/92] Happy new year, updated dependency list [ci skip] --- Cryptomator/Resources/about.html | 6 ++++-- fastlane/config/freemium/metadata/copyright.txt | 2 +- fastlane/config/premium/metadata/copyright.txt | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cryptomator/Resources/about.html b/Cryptomator/Resources/about.html index 4a3e25c06..8c95d5856 100644 --- a/Cryptomator/Resources/about.html +++ b/Cryptomator/Resources/about.html @@ -59,7 +59,9 @@

Third-Party Dependencies

  • ObjectiveDropboxOfficial by Dropbox
  • PCloudSDKSwift by pCloud
  • Promises by Google
  • -
  • swift-log by Apple
  • +
  • Simple Swift Dependencies by Philipp Schmid
  • +
  • SwiftECC by Leif Ibsen
  • +
  • SwiftLog by Apple
  • TPInAppReceipt by Pavel Tikhonenko
  • @@ -67,6 +69,6 @@

    Disclaimer

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    Copyright

    -

    © 2016 – 2023 Skymatic GmbH. All rights reserved.

    +

    © 2016 – 2024 Skymatic GmbH. All rights reserved.

    diff --git a/fastlane/config/freemium/metadata/copyright.txt b/fastlane/config/freemium/metadata/copyright.txt index 0364a6b33..65cbf78b5 100644 --- a/fastlane/config/freemium/metadata/copyright.txt +++ b/fastlane/config/freemium/metadata/copyright.txt @@ -1 +1 @@ -2023 cryptomator.org \ No newline at end of file +2024 cryptomator.org \ No newline at end of file diff --git a/fastlane/config/premium/metadata/copyright.txt b/fastlane/config/premium/metadata/copyright.txt index 0364a6b33..65cbf78b5 100644 --- a/fastlane/config/premium/metadata/copyright.txt +++ b/fastlane/config/premium/metadata/copyright.txt @@ -1 +1 @@ -2023 cryptomator.org \ No newline at end of file +2024 cryptomator.org \ No newline at end of file From 7b2b97e49acfbe209bac46ab1350b5bbb3d75034 Mon Sep 17 00:00:00 2001 From: Philipp Schmid <25935690+phil1995@users.noreply.github.com> Date: Fri, 12 Jan 2024 18:06:07 +0100 Subject: [PATCH 89/92] Use hub subscription header --- .../Hub/CryptomatorHubAuthenticator.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index e0d65ccef..94877a1b7 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -67,9 +67,11 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving webAppURL: webAppURL) let encryptedVaultKey: String + let unlockHeader: [AnyHashable: Any] switch retrieveMasterkeyResponse { - case let .success(key): + case let .success(key, header): encryptedVaultKey = key + unlockHeader = header case .accessNotGranted: return .accessNotGranted case .licenseExceeded: @@ -93,7 +95,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving let encryptedUserKeyJWE = try JWE(compactSerialization: encryptedUserKey) let encryptedVaultKeyJWE = try JWE(compactSerialization: encryptedVaultKey) - return .success(.init(encryptedUserKey: encryptedUserKeyJWE, encryptedVaultKey: encryptedVaultKeyJWE, header: [:])) + return .success(.init(encryptedUserKey: encryptedUserKeyJWE, encryptedVaultKey: encryptedVaultKeyJWE, header: unlockHeader)) } /** @@ -240,7 +242,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving guard let body = String(data: data, encoding: .utf8) else { throw CryptomatorHubAuthenticatorError.unexpectedResponse } - return .success(encryptedVaultKey: body) + return .success(encryptedVaultKey: body, header: httpResponse?.allHeaderFields ?? [:]) case 402: return .licenseExceeded case 403, 410: @@ -299,7 +301,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving private enum RetrieveVaultMasterkeyEncryptedForUserResponse { // 200 - case success(encryptedVaultKey: String) + case success(encryptedVaultKey: String, header: [AnyHashable: Any]) // 403, 410 case accessNotGranted // 402 From 400e3d79e35ce86f4f75c6bd3f767b2d700167b5 Mon Sep 17 00:00:00 2001 From: Cryptobot Date: Tue, 16 Jan 2024 21:05:46 +0100 Subject: [PATCH 90/92] New Crowdin updates (#334) [ci skip] --- SharedResources/it.lproj/Localizable.strings | 4 ++++ SharedResources/sv.lproj/Localizable.strings | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/SharedResources/it.lproj/Localizable.strings b/SharedResources/it.lproj/Localizable.strings index 1518e7c9f..818c4139c 100644 --- a/SharedResources/it.lproj/Localizable.strings +++ b/SharedResources/it.lproj/Localizable.strings @@ -119,8 +119,12 @@ "hubAuthentication.licenseExceeded" = "La tua istanza Cryptomator Hub ha una licenza non valida. Si prega di informare un amministratore Hub per aggiornare o rinnovare la licenza."; "hubAuthentication.deviceRegistration.deviceName.cells.name" = "Nome Del Dispositivo"; "hubAuthentication.deviceRegistration.deviceName.footer.title" = "Sembra che questo sia il primo accesso alle Centrali delle Casseforti da questo dispositivo. Per poterlo identificare ai fini dell'autorizzazione all'accesso è necessario dare un nome a questo dispositivo."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "La chiave del tuo account è richiesta per accedere da nuove applicazioni o browser. Può essere trovata nel tuo profilo."; "hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Registrazione del dispositivo Riuscita"; "hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "Per accedere al vault, il tuo dispositivo deve essere autorizzato dal proprietario del vault."; +"hubAuthentication.requireAccountInit.alert.title" = "Azione richiesta"; +"hubAuthentication.requireAccountInit.alert.message" = "Per procedere, completa i passaggi richiesti nel tuo profilo dell'Hub."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Vai al profilo"; "intents.saveFile.missingFile" = "Il file fornito non è valido."; "intents.saveFile.invalidFolder" = "La cartella fornita non è valida."; diff --git a/SharedResources/sv.lproj/Localizable.strings b/SharedResources/sv.lproj/Localizable.strings index f40d8decc..d6b537e0e 100644 --- a/SharedResources/sv.lproj/Localizable.strings +++ b/SharedResources/sv.lproj/Localizable.strings @@ -119,8 +119,12 @@ "hubAuthentication.licenseExceeded" = "Din Cryptomator Hub-instans har en ogiltig licens. Vänligen informera en Hub administratör för att uppgradera eller förnya licensen."; "hubAuthentication.deviceRegistration.deviceName.cells.name" = "Enhetsnamn"; "hubAuthentication.deviceRegistration.deviceName.footer.title" = "Detta verkar vara den första Hub-åtkomsten från den här enheten. För att identifiera den för åtkomstbehörighet, måste du namnge den här enheten."; +"hubAuthentication.deviceRegistration.accountKey.footer.title" = "Din kontonyckel krävs för att logga in från nya appar eller webbläsare. Den finns i din profil."; "hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Enhetsregistrering lyckades"; "hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "För att komma åt valvet måste din enhet godkännas av valvägaren."; +"hubAuthentication.requireAccountInit.alert.title" = "Åtgärd krävs"; +"hubAuthentication.requireAccountInit.alert.message" = "För att fortsätta, vänligen fyll i de steg som krävs i din Hub-användarprofil."; +"hubAuthentication.requireAccountInit.alert.actionButton" = "Gå till profil"; "intents.saveFile.missingFile" = "Den angivna filen är inte giltig."; "intents.saveFile.invalidFolder" = "Den angivna mappen är inte giltig."; From b231161194fd69b2b1396d4e57b47bd90a52ca2c Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 16 Jan 2024 21:07:33 +0100 Subject: [PATCH 91/92] Updated release notes [ci skip] --- fastlane/changelog.txt | 2 +- fastlane/config/freemium/metadata/de-DE/release_notes.txt | 2 +- fastlane/config/freemium/metadata/en-US/release_notes.txt | 2 +- fastlane/config/premium/metadata/de-DE/release_notes.txt | 2 +- fastlane/config/premium/metadata/en-US/release_notes.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index 9b1d51ebb..9865594d0 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -1,2 +1,2 @@ -- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332) +- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332, #333) - New vaults are now created with GCM encryption by default (#288) \ No newline at end of file diff --git a/fastlane/config/freemium/metadata/de-DE/release_notes.txt b/fastlane/config/freemium/metadata/de-DE/release_notes.txt index 764f2bb8c..0ff2310e5 100644 --- a/fastlane/config/freemium/metadata/de-DE/release_notes.txt +++ b/fastlane/config/freemium/metadata/de-DE/release_notes.txt @@ -1,2 +1,2 @@ -- Unterstützung für Cryptomator Hub hinzugefügt (#315, #322, #323, #326, #329, #332) +- Unterstützung für Cryptomator Hub hinzugefügt (#315, #322, #323, #326, #329, #332, #333) - Neue Tresore werden nun standardmäßig mit GCM-Verschlüsselung erstellt (#288) \ No newline at end of file diff --git a/fastlane/config/freemium/metadata/en-US/release_notes.txt b/fastlane/config/freemium/metadata/en-US/release_notes.txt index 9b1d51ebb..9865594d0 100644 --- a/fastlane/config/freemium/metadata/en-US/release_notes.txt +++ b/fastlane/config/freemium/metadata/en-US/release_notes.txt @@ -1,2 +1,2 @@ -- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332) +- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332, #333) - New vaults are now created with GCM encryption by default (#288) \ No newline at end of file diff --git a/fastlane/config/premium/metadata/de-DE/release_notes.txt b/fastlane/config/premium/metadata/de-DE/release_notes.txt index 764f2bb8c..0ff2310e5 100644 --- a/fastlane/config/premium/metadata/de-DE/release_notes.txt +++ b/fastlane/config/premium/metadata/de-DE/release_notes.txt @@ -1,2 +1,2 @@ -- Unterstützung für Cryptomator Hub hinzugefügt (#315, #322, #323, #326, #329, #332) +- Unterstützung für Cryptomator Hub hinzugefügt (#315, #322, #323, #326, #329, #332, #333) - Neue Tresore werden nun standardmäßig mit GCM-Verschlüsselung erstellt (#288) \ No newline at end of file diff --git a/fastlane/config/premium/metadata/en-US/release_notes.txt b/fastlane/config/premium/metadata/en-US/release_notes.txt index 9b1d51ebb..9865594d0 100644 --- a/fastlane/config/premium/metadata/en-US/release_notes.txt +++ b/fastlane/config/premium/metadata/en-US/release_notes.txt @@ -1,2 +1,2 @@ -- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332) +- Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332, #333) - New vaults are now created with GCM encryption by default (#288) \ No newline at end of file From d98d8bf0738d9f9a1468fed4304c7cf16ff22b5c Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 16 Jan 2024 21:34:00 +0100 Subject: [PATCH 92/92] Enabled Bangla, Filipino, and Hungarian translations --- Cryptomator.xcodeproj/project.pbxproj | 15 +++++++++++++++ fastlane/changelog.txt | 3 ++- .../freemium/metadata/de-DE/release_notes.txt | 3 ++- .../freemium/metadata/en-US/release_notes.txt | 3 ++- .../premium/metadata/de-DE/release_notes.txt | 3 ++- .../premium/metadata/en-US/release_notes.txt | 3 ++- 6 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 73d8f063e..849360299 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -1002,6 +1002,12 @@ 7469AD99266E26B0000DCD45 /* URL+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Zip.swift"; sourceTree = ""; }; 747C35162762A3F500E4CA28 /* AttributedTextHeaderFooterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedTextHeaderFooterViewModel.swift; sourceTree = ""; }; 74833F9D27E4CCD800C1C5F0 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = ""; }; + 748BF2062B571AE7006304AD /* ba */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ba; path = ba.lproj/Localizable.strings; sourceTree = ""; }; + 748BF20D2B571B0A006304AD /* ba */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ba; path = ba.lproj/Intents.strings; sourceTree = ""; }; + 748BF20E2B571BAA006304AD /* fil */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fil; path = fil.lproj/Localizable.strings; sourceTree = ""; }; + 748BF20F2B571BB4006304AD /* fil */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fil; path = fil.lproj/Intents.strings; sourceTree = ""; }; + 748BF2102B571C0C006304AD /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + 748BF2112B571C11006304AD /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Intents.strings; sourceTree = ""; }; 74A1B13D2726A9E60098224B /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = ""; }; 74AE94EF27A0282300D71AEC /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; 74AE94F027A0283500D71AEC /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; @@ -2295,16 +2301,19 @@ Base, de, ar, + ba, bn, ca, cs, da, el, es, + fil, fr, he, hi, hr, + hu, id, it, ja, @@ -3046,16 +3055,19 @@ 4A2745F7284769B800E70D5F /* en */, 74275ACC28478DFA0058AD25 /* de */, 74275ACD28478DFC0058AD25 /* ar */, + 748BF20D2B571B0A006304AD /* ba */, 74275ACE28478DFD0058AD25 /* bn */, 74275ACF28478DFE0058AD25 /* ca */, 74275AD428478E020058AD25 /* cs */, 741CD1CF2939083C00577FDE /* da */, 74275AD728478E050058AD25 /* el */, 74275AE428478E130058AD25 /* es */, + 748BF20F2B571BB4006304AD /* fil */, 74275AD628478E040058AD25 /* fr */, 74B46E5629BB863C000C1CC0 /* he */, 74275AD828478E060058AD25 /* hi */, 74275AD328478E010058AD25 /* hr */, + 748BF2112B571C11006304AD /* hu */, 74275AD928478E070058AD25 /* id */, 74275ADA28478E080058AD25 /* it */, 74275ADB28478E090058AD25 /* ja */, @@ -3092,16 +3104,19 @@ 742679FA26A56B33004C61BC /* en */, 742679FE26A578E2004C61BC /* de */, 74AE94EF27A0282300D71AEC /* ar */, + 748BF2062B571AE7006304AD /* ba */, 74E93B742810109E0047A116 /* bn */, 74AE94F027A0283500D71AEC /* ca */, 74BDA62B26CE8AE1007FBD72 /* cs */, 741CD1C82939080D00577FDE /* da */, 74267A0326A5793E004C61BC /* el */, 74267A0426A57944004C61BC /* es */, + 748BF20E2B571BAA006304AD /* fil */, 74267A0526A57947004C61BC /* fr */, 74B46E5529BB8629000C1CC0 /* he */, 74A1B13D2726A9E60098224B /* hi */, 74E93B75281010E50047A116 /* hr */, + 748BF2102B571C0C006304AD /* hu */, 74AE94F127A0285400D71AEC /* id */, 74267A0A26A5795C004C61BC /* it */, 74267A0B26A57960004C61BC /* ja */, diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index 9865594d0..f38d2e1d6 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -1,2 +1,3 @@ - Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332, #333) -- New vaults are now created with GCM encryption by default (#288) \ No newline at end of file +- New vaults are now created with GCM encryption by default (#288) +- Added Bangla, Filipino, and Hungarian translations \ No newline at end of file diff --git a/fastlane/config/freemium/metadata/de-DE/release_notes.txt b/fastlane/config/freemium/metadata/de-DE/release_notes.txt index 0ff2310e5..c020ba9c2 100644 --- a/fastlane/config/freemium/metadata/de-DE/release_notes.txt +++ b/fastlane/config/freemium/metadata/de-DE/release_notes.txt @@ -1,2 +1,3 @@ - Unterstützung für Cryptomator Hub hinzugefügt (#315, #322, #323, #326, #329, #332, #333) -- Neue Tresore werden nun standardmäßig mit GCM-Verschlüsselung erstellt (#288) \ No newline at end of file +- Neue Tresore werden nun standardmäßig mit GCM-Verschlüsselung erstellt (#288) +- Übersetzungen für Bangla, Filipino und Ungarisch hinzugefügt \ No newline at end of file diff --git a/fastlane/config/freemium/metadata/en-US/release_notes.txt b/fastlane/config/freemium/metadata/en-US/release_notes.txt index 9865594d0..f38d2e1d6 100644 --- a/fastlane/config/freemium/metadata/en-US/release_notes.txt +++ b/fastlane/config/freemium/metadata/en-US/release_notes.txt @@ -1,2 +1,3 @@ - Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332, #333) -- New vaults are now created with GCM encryption by default (#288) \ No newline at end of file +- New vaults are now created with GCM encryption by default (#288) +- Added Bangla, Filipino, and Hungarian translations \ No newline at end of file diff --git a/fastlane/config/premium/metadata/de-DE/release_notes.txt b/fastlane/config/premium/metadata/de-DE/release_notes.txt index 0ff2310e5..c020ba9c2 100644 --- a/fastlane/config/premium/metadata/de-DE/release_notes.txt +++ b/fastlane/config/premium/metadata/de-DE/release_notes.txt @@ -1,2 +1,3 @@ - Unterstützung für Cryptomator Hub hinzugefügt (#315, #322, #323, #326, #329, #332, #333) -- Neue Tresore werden nun standardmäßig mit GCM-Verschlüsselung erstellt (#288) \ No newline at end of file +- Neue Tresore werden nun standardmäßig mit GCM-Verschlüsselung erstellt (#288) +- Übersetzungen für Bangla, Filipino und Ungarisch hinzugefügt \ No newline at end of file diff --git a/fastlane/config/premium/metadata/en-US/release_notes.txt b/fastlane/config/premium/metadata/en-US/release_notes.txt index 9865594d0..f38d2e1d6 100644 --- a/fastlane/config/premium/metadata/en-US/release_notes.txt +++ b/fastlane/config/premium/metadata/en-US/release_notes.txt @@ -1,2 +1,3 @@ - Added support for Cryptomator Hub (#315, #322, #323, #326, #329, #332, #333) -- New vaults are now created with GCM encryption by default (#288) \ No newline at end of file +- New vaults are now created with GCM encryption by default (#288) +- Added Bangla, Filipino, and Hungarian translations \ No newline at end of file