diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 632c7987c..277543c3f 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -595,6 +595,7 @@ 74267A1C26A5799F004C61BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; 74267A1D26A579A4004C61BC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 7469AD99266E26B0000DCD45 /* URL+Zip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Zip.swift"; sourceTree = ""; }; + 74BDA62B26CE8AE1007FBD72 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 74D365B9268B5DB0005ECD69 /* FilesAppUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesAppUtil.swift; sourceTree = ""; }; 74FC576025ADED030003ED27 /* VaultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1004,11 +1005,11 @@ 4A66F58125C487C9001BE15E /* PasswordFieldCell.swift */, 4A4B7E4726B2BAFB009BFDB1 /* SwitchCell.swift */, 4A4B7E4526B2B6A0009BFDB1 /* SwitchCellViewModel.swift */, + 4A4B7E4B26B2FF41009BFDB1 /* TableViewCell.swift */, 4A4B7E4326B2B1A5009BFDB1 /* TableViewCellViewModel.swift */, 4A66F57825C47BB2001BE15E /* TextFieldCell.swift */, 4AA22C15261CA8D800A17486 /* URLFieldCell.swift */, 4AA22C1D261CA94700A17486 /* UsernameFieldCell.swift */, - 4A4B7E4B26B2FF41009BFDB1 /* TableViewCell.swift */, ); path = Cells; sourceTree = ""; @@ -1021,12 +1022,12 @@ 4AE7D79325826A0900C5E1D8 /* FileProviderValidationServiceSource.h */, 4AE7D79425826A0900C5E1D8 /* FileProviderValidationServiceSource.m */, 4AA621DE249A6A8400A0BCBD /* Info.plist */, + 4AFD8C102693204900F77BA6 /* ErrorWrapper.swift */, 4AD0F61B24AF203F0026B765 /* FileProvider+Actions.swift */, 4AA621DC249A6A8400A0BCBD /* FileProviderEnumerator.swift */, 4AA621D8249A6A8400A0BCBD /* FileProviderExtension.swift */, 4A24001926AE9F3A009DBC2E /* VaultLockingServiceSource.swift */, 4A9BED63268F1DB000721BAA /* VaultUnlockingServiceSource.swift */, - 4AFD8C102693204900F77BA6 /* ErrorWrapper.swift */, ); path = FileProviderExtension; sourceTree = ""; @@ -1433,6 +1434,7 @@ en, Base, de, + cs, el, es, fr, @@ -1929,6 +1931,7 @@ children = ( 742679FA26A56B33004C61BC /* en */, 742679FE26A578E2004C61BC /* de */, + 74BDA62B26CE8AE1007FBD72 /* cs */, 74267A0326A5793E004C61BC /* el */, 74267A0426A57944004C61BC /* es */, 74267A0526A57947004C61BC /* fr */, diff --git a/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticationViewModel.swift b/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticationViewModel.swift index e8e3c56f2..089c587bb 100644 --- a/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticationViewModel.swift +++ b/Cryptomator/AddVault/LocalVault/LocalFileSystemAuthenticationViewModel.swift @@ -53,14 +53,8 @@ class LocalFileSystemAuthenticationViewModel: LocalFileSystemAuthenticationViewM } private func validate(credential: LocalFileSystemCredential) -> Promise { - let provider = LocalFileSystemProvider(rootURL: credential.rootURL) - return provider.fetchItemListExhaustively(forFolderAt: CloudPath("/")).recover { error -> CloudItemList in - if let error = error as? CloudProviderError { - throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: CloudPath("/")) - } else { - throw error - } - }.then { itemList in + let provider = LocalizedCloudProviderDecorator(delegate: LocalFileSystemProvider(rootURL: credential.rootURL)) + return provider.fetchItemListExhaustively(forFolderAt: CloudPath("/")).then { itemList in try self.validationLogic.validate(items: itemList.items) } } diff --git a/Cryptomator/Common/ActionButton.swift b/Cryptomator/Common/ActionButton.swift index ead6d1c34..8e1249c6d 100644 --- a/Cryptomator/Common/ActionButton.swift +++ b/Cryptomator/Common/ActionButton.swift @@ -7,6 +7,7 @@ // import UIKit + class ActionButton: UIButton { var primaryAction: ((UIButton) -> Void)? diff --git a/Cryptomator/Common/Bindable.swift b/Cryptomator/Common/Bindable.swift index 3a6191f67..a8afa9b96 100644 --- a/Cryptomator/Common/Bindable.swift +++ b/Cryptomator/Common/Bindable.swift @@ -8,6 +8,7 @@ import Combine import Foundation + class Bindable { @Published var value: Value diff --git a/Cryptomator/Common/Cells/SwitchCell.swift b/Cryptomator/Common/Cells/SwitchCell.swift index 96688f2cf..dfee21349 100644 --- a/Cryptomator/Common/Cells/SwitchCell.swift +++ b/Cryptomator/Common/Cells/SwitchCell.swift @@ -8,6 +8,7 @@ import Combine import UIKit + class SwitchCell: TableViewCell { var switchControl = UISwitch(frame: .zero) diff --git a/Cryptomator/Common/Cells/TableViewCell.swift b/Cryptomator/Common/Cells/TableViewCell.swift index 92b8bfc8a..24f3b61aa 100644 --- a/Cryptomator/Common/Cells/TableViewCell.swift +++ b/Cryptomator/Common/Cells/TableViewCell.swift @@ -8,6 +8,7 @@ import Combine import UIKit + class TableViewCell: UITableViewCell { lazy var subscribers = Set() private var viewModel: TableViewCellViewModel? diff --git a/Cryptomator/Common/Cells/TableViewCellViewModel.swift b/Cryptomator/Common/Cells/TableViewCellViewModel.swift index 5949d1b16..2a5077926 100644 --- a/Cryptomator/Common/Cells/TableViewCellViewModel.swift +++ b/Cryptomator/Common/Cells/TableViewCellViewModel.swift @@ -7,6 +7,7 @@ // import UIKit + protocol TableViewCellViewModel: AnyObject { var type: TableViewCell.Type { get } var title: Bindable { get } diff --git a/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift b/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift index 4663dac36..66dc5dc0f 100644 --- a/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift +++ b/Cryptomator/Common/ChooseFolder/ChooseFolderViewModel.swift @@ -38,7 +38,7 @@ class ChooseFolderViewModel: ChooseFolderViewModelProtocol { init(canCreateFolder: Bool, cloudPath: CloudPath, provider: CloudProvider) { self.canCreateFolder = canCreateFolder self.cloudPath = cloudPath - self.provider = provider + self.provider = LocalizedCloudProviderDecorator(delegate: provider) } func startListenForChanges(onError: @escaping (Error) -> Void, onChange: @escaping () -> Void, onVaultDetection: @escaping (VaultDetailItem) -> Void) { @@ -49,13 +49,7 @@ class ChooseFolderViewModel: ChooseFolderViewModelProtocol { } func refreshItems() { - provider.fetchItemListExhaustively(forFolderAt: cloudPath).recover { error -> CloudItemList in - if let error = error as? CloudProviderError { - throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: self.cloudPath) - } else { - throw error - } - }.then { itemList in + provider.fetchItemListExhaustively(forFolderAt: cloudPath).then { itemList in if let vaultItem = VaultDetector.getVaultItem(items: itemList.items, parentCloudPath: self.cloudPath) { self.foundMasterkey = true self.vaultListener?(vaultItem) diff --git a/Cryptomator/Common/ChooseFolder/CreateNewFolderViewModel.swift b/Cryptomator/Common/ChooseFolder/CreateNewFolderViewModel.swift index fc8c04940..32716d000 100644 --- a/Cryptomator/Common/ChooseFolder/CreateNewFolderViewModel.swift +++ b/Cryptomator/Common/ChooseFolder/CreateNewFolderViewModel.swift @@ -26,7 +26,7 @@ class CreateNewFolderViewModel: CreateNewFolderViewModelProtocol { init(parentPath: CloudPath, provider: CloudProvider) { self.parentPath = parentPath - self.provider = provider + self.provider = LocalizedCloudProviderDecorator(delegate: provider) } func createFolder() -> Promise { @@ -34,13 +34,7 @@ class CreateNewFolderViewModel: CreateNewFolderViewModelProtocol { return Promise(CreateNewFolderViewModelError.emptyFolderName) } let folderPath = parentPath.appendingPathComponent(folderName) - return provider.createFolder(at: folderPath).recover { error -> Void in - if let error = error as? CloudProviderError { - throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: folderPath) - } else { - throw error - } - }.then { + return provider.createFolder(at: folderPath).then { folderPath } } diff --git a/Cryptomator/Common/Combine/Publisher+OptionalAssign.swift b/Cryptomator/Common/Combine/Publisher+OptionalAssign.swift index 9908ecda8..072b6b2b6 100644 --- a/Cryptomator/Common/Combine/Publisher+OptionalAssign.swift +++ b/Cryptomator/Common/Combine/Publisher+OptionalAssign.swift @@ -8,6 +8,7 @@ import Combine import Foundation + extension Publisher where Failure == Never { func assign(to keyPath: ReferenceWritableKeyPath, on root: Root?) -> AnyCancellable { sink { [weak root] in diff --git a/Cryptomator/Common/Combine/UIControl+Publisher.swift b/Cryptomator/Common/Combine/UIControl+Publisher.swift index e35bfd563..7a8f195ae 100644 --- a/Cryptomator/Common/Combine/UIControl+Publisher.swift +++ b/Cryptomator/Common/Combine/UIControl+Publisher.swift @@ -8,8 +8,12 @@ import Combine import UIKit -// Taken from: https://www.avanderlee.com/swift/custom-combine-publisher/ -/// A custom subscription to capture UIControl target events. + +/** + A custom subscription to capture `UIControl` target events. + + Taken from: + */ final class UIControlSubscription: Subscription where SubscriberType.Input == Control { private var subscriber: SubscriberType? private let control: Control diff --git a/Cryptomator/Common/Combine/UISwitch+Publisher.swift b/Cryptomator/Common/Combine/UISwitch+Publisher.swift index e73bbf9d2..27a5ecaf9 100644 --- a/Cryptomator/Common/Combine/UISwitch+Publisher.swift +++ b/Cryptomator/Common/Combine/UISwitch+Publisher.swift @@ -8,6 +8,7 @@ import Combine import UIKit + extension UISwitch { func publisher(for events: UIControl.Event) -> AnyPublisher.Failure> { return publisher(for: events).map { $0.isOn }.eraseToAnyPublisher() diff --git a/Cryptomator/Common/Coordinator.swift b/Cryptomator/Common/Coordinator.swift index 6c7dcc67b..fd78c21e5 100644 --- a/Cryptomator/Common/Coordinator.swift +++ b/Cryptomator/Common/Coordinator.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CocoaLumberjackSwift import CryptomatorCommonCore import UIKit @@ -18,6 +19,7 @@ protocol Coordinator: AnyObject { 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) diff --git a/Cryptomator/Common/HeaderFooter/BaseHeaderFooterView.swift b/Cryptomator/Common/HeaderFooter/BaseHeaderFooterView.swift index 5cba4a139..7c3b33506 100644 --- a/Cryptomator/Common/HeaderFooter/BaseHeaderFooterView.swift +++ b/Cryptomator/Common/HeaderFooter/BaseHeaderFooterView.swift @@ -8,6 +8,7 @@ import Combine import UIKit + class BaseHeaderFooterView: UITableViewHeaderFooterView, HeaderFooterViewModelConfiguring { weak var tableView: UITableView? diff --git a/Cryptomator/Common/HeaderFooter/HeaderFooterViewModel.swift b/Cryptomator/Common/HeaderFooter/HeaderFooterViewModel.swift index 71c825c5a..1d8874d96 100644 --- a/Cryptomator/Common/HeaderFooter/HeaderFooterViewModel.swift +++ b/Cryptomator/Common/HeaderFooter/HeaderFooterViewModel.swift @@ -7,6 +7,7 @@ // import UIKit + protocol HeaderFooterViewModel { var viewType: HeaderFooterViewModelConfiguring.Type { get } var title: Bindable { get } diff --git a/Cryptomator/VaultDetail/VaultPasswordVerifying.swift b/Cryptomator/VaultDetail/VaultPasswordVerifying.swift index 85a9d65f7..880dae59e 100644 --- a/Cryptomator/VaultDetail/VaultPasswordVerifying.swift +++ b/Cryptomator/VaultDetail/VaultPasswordVerifying.swift @@ -7,6 +7,7 @@ // import Foundation + protocol VaultPasswordVerifying { func verifiedVaultPassword() func cancel() diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/LocalizedCloudProviderDecorator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/LocalizedCloudProviderDecorator.swift new file mode 100644 index 000000000..cd64797b9 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/LocalizedCloudProviderDecorator.swift @@ -0,0 +1,130 @@ +// +// LocalizedCloudProviderDecorator.swift +// CryptomatorCommonCore +// +// Created by Tobias Hagemann on 19.08.21. +// Copyright © 2020 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import Foundation +import Promises + +public class LocalizedCloudProviderDecorator: CloudProvider { + // swiftlint:disable:next weak_delegate + public let delegate: CloudProvider + + public init(delegate: CloudProvider) { + self.delegate = delegate + } + + public func fetchItemMetadata(at cloudPath: CloudPath) -> Promise { + return delegate.fetchItemMetadata(at: cloudPath).recover { error -> CloudItemMetadata in + if let error = error as? CloudProviderError { + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: cloudPath) + } else { + throw error + } + } + } + + public func fetchItemList(forFolderAt cloudPath: CloudPath, withPageToken pageToken: String?) -> Promise { + return delegate.fetchItemList(forFolderAt: cloudPath, withPageToken: pageToken).recover { error -> CloudItemList in + if let error = error as? CloudProviderError { + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: cloudPath) + } else { + throw error + } + } + } + + public func downloadFile(from cloudPath: CloudPath, to localURL: URL) -> Promise { + return delegate.downloadFile(from: cloudPath, to: localURL).recover { error -> Void in + if let error = error as? CloudProviderError { + switch error { + case .itemAlreadyExists: + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: CloudPath(localURL.path)) + default: + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: cloudPath) + } + } else { + throw error + } + } + } + + public func uploadFile(from localURL: URL, to cloudPath: CloudPath, replaceExisting: Bool) -> Promise { + return delegate.uploadFile(from: localURL, to: cloudPath, replaceExisting: replaceExisting).recover { error -> CloudItemMetadata in + if let error = error as? CloudProviderError { + switch error { + case .itemNotFound, .itemTypeMismatch: + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: CloudPath(localURL.path)) + default: + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: cloudPath) + } + } else { + throw error + } + } + } + + public func createFolder(at cloudPath: CloudPath) -> Promise { + return delegate.createFolder(at: cloudPath).recover { error -> Void in + if let error = error as? CloudProviderError { + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: cloudPath) + } else { + throw error + } + } + } + + public func deleteFile(at cloudPath: CloudPath) -> Promise { + return delegate.deleteFile(at: cloudPath).recover { error -> Void in + if let error = error as? CloudProviderError { + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: cloudPath) + } else { + throw error + } + } + } + + public func deleteFolder(at cloudPath: CloudPath) -> Promise { + return delegate.deleteFolder(at: cloudPath).recover { error -> Void in + if let error = error as? CloudProviderError { + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: cloudPath) + } else { + throw error + } + } + } + + public func moveFile(from sourceCloudPath: CloudPath, to targetCloudPath: CloudPath) -> Promise { + return delegate.moveFile(from: sourceCloudPath, to: targetCloudPath).recover { error -> Void in + if let error = error as? CloudProviderError { + switch error { + case .itemAlreadyExists, .parentFolderDoesNotExist: + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: targetCloudPath) + default: + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: sourceCloudPath) + } + } else { + throw error + } + } + } + + public func moveFolder(from sourceCloudPath: CloudPath, to targetCloudPath: CloudPath) -> Promise { + return delegate.moveFolder(from: sourceCloudPath, to: targetCloudPath).recover { error -> Void in + if let error = error as? CloudProviderError { + switch error { + case .itemAlreadyExists, .parentFolderDoesNotExist: + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: targetCloudPath) + default: + throw LocalizedCloudProviderError.convertToLocalized(error, cloudPath: sourceCloudPath) + } + } else { + throw error + } + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift index 01b07c673..1a357572a 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift @@ -8,6 +8,7 @@ import CryptomatorCloudAccessCore import Foundation + public protocol CloudProviderManager { func getProvider(with accountUID: String) throws -> CloudProvider static func providerShouldUpdate(with accountUID: String) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift index fd5254add..c90324b05 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultDBManager.swift @@ -22,13 +22,11 @@ public enum VaultManagerError: Error { public protocol VaultManager { func createNewVault(withVaultUID vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise - func manualUnlockVault(withUID vaultUID: String, kek: [UInt8]) throws -> CloudProvider - func getDecorator(forVaultUID vaultUID: String) throws -> CloudProvider func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise + func manualUnlockVault(withUID vaultUID: String, kek: [UInt8]) throws -> CloudProvider func removeVault(withUID vaultUID: String) throws -> Promise func removeAllUnusedFileProviderDomains() -> Promise - func getVaultPath(from masterkeyPath: CloudPath) -> CloudPath } public class VaultDBManager: VaultManager { @@ -47,16 +45,16 @@ public class VaultDBManager: VaultManager { self.passwordManager = passwordManager } - // MARK: Create New Vault + // MARK: - Create New Vault /** - - Precondition: There is no VaultAccount for the `vaultUID` in the database yet - - Precondition: It exists a CloudProviderAccount with the `delegateAccountUID` in the database + - Precondition: There is no `VaultAccount` for the `vaultUID` in the database yet. + - Precondition: A `CloudProviderAccount` with the `delegateAccountUID` exists in the database. - Postcondition: The root path was created in the cloud and the masterkey file was uploaded. - - Postcondition: The masterkey file and vault config token is cached under the corresponding `vaultUID` - - Postcondition: storePasswordInKeychain <=> the password for the masterkey is stored in the keychain. - - Postcondition: The passed `vaultUID`, `delegateAccountUID` and `vaultPath` are stored as VaultAccount in the database - - Postcondition: The created VaultDecorator is cached under the corresponding `vaultUID`. + - Postcondition: The masterkey file and vault config token are cached under the corresponding `vaultUID`. + - Postcondition: `storePasswordInKeychain` <=> the password for the masterkey is stored in the keychain. + - Postcondition: The passed `vaultUID`, `delegateAccountUID`, and `vaultPath` are stored as `VaultAccount` in the database. + - Postcondition: The created vault decorator is cached under the corresponding `vaultUID`. */ public func createNewVault(withVaultUID vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { guard VaultDBManager.cachedDecorators[vaultUID] == nil else { @@ -65,25 +63,25 @@ public class VaultDBManager: VaultManager { let tmpDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString, isDirectory: true) let vaultConfig = VaultConfig.createNew(format: 8, cipherCombo: .sivCTRMAC, shorteningThreshold: 220) let masterkey: Masterkey - let delegate: CloudProvider + let provider: LocalizedCloudProviderDecorator let vaultConfigToken: String do { try FileManager.default.createDirectory(at: tmpDirURL, withIntermediateDirectories: true) masterkey = try Masterkey.createNew() vaultConfigToken = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: masterkey.rawKey) - delegate = try providerManager.getProvider(with: delegateAccountUID) + provider = LocalizedCloudProviderDecorator(delegate: try providerManager.getProvider(with: delegateAccountUID)) } catch { return Promise(error) } - return delegate.createFolder(at: vaultPath).then { _ -> Promise in - try self.uploadMasterkey(masterkey, password: password, vaultPath: vaultPath, delegate: delegate, tmpDirURL: tmpDirURL) + return provider.createFolder(at: vaultPath).then { _ -> Promise in + try self.uploadMasterkey(masterkey, password: password, vaultPath: vaultPath, provider: provider, tmpDirURL: tmpDirURL) }.then { _ -> Promise in - try self.uploadVaultConfigToken(vaultConfigToken, vaultPath: vaultPath, delegate: delegate, tmpDirURL: tmpDirURL) + try self.uploadVaultConfigToken(vaultConfigToken, vaultPath: vaultPath, provider: provider, tmpDirURL: tmpDirURL) }.then { _ -> Promise in - try self.createVaultFolderStructure(masterkey: masterkey, vaultPath: vaultPath, delegate: delegate) + try self.createVaultFolderStructure(masterkey: masterkey, vaultPath: vaultPath, provider: provider) }.then { _ -> Promise in let unverifiedVaultConfig = try UnverifiedVaultConfig(token: vaultConfigToken) - let decorator = try VaultProviderFactory.createVaultProvider(from: unverifiedVaultConfig, masterkey: masterkey, vaultPath: vaultPath, with: delegate) + let decorator = try VaultProviderFactory.createVaultProvider(from: unverifiedVaultConfig, masterkey: masterkey, vaultPath: vaultPath, with: provider.delegate) VaultDBManager.cachedDecorators[vaultUID] = decorator return self.addFileProviderDomain(forVaultUID: vaultUID, displayName: vaultPath.lastPathComponent) }.then { @@ -97,104 +95,54 @@ public class VaultDBManager: VaultManager { } } - private func uploadMasterkey(_ masterkey: Masterkey, password: String, vaultPath: CloudPath, delegate: CloudProvider, tmpDirURL: URL) throws -> Promise { + private func uploadMasterkey(_ masterkey: Masterkey, password: String, vaultPath: CloudPath, provider: CloudProvider, tmpDirURL: URL) throws -> Promise { let localMasterkeyURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) let masterkeyData = try exportMasterkey(masterkey, vaultVersion: VaultDBManager.fakeVaultVersion, password: password) try masterkeyData.write(to: localMasterkeyURL) let masterkeyCloudPath = vaultPath.appendingPathComponent("masterkey.cryptomator") - return delegate.uploadFile(from: localMasterkeyURL, to: masterkeyCloudPath, replaceExisting: false) + return provider.uploadFile(from: localMasterkeyURL, to: masterkeyCloudPath, replaceExisting: false) } - private func uploadVaultConfigToken(_ token: String, vaultPath: CloudPath, delegate: CloudProvider, tmpDirURL: URL) throws -> Promise { + private func uploadVaultConfigToken(_ token: String, vaultPath: CloudPath, provider: CloudProvider, tmpDirURL: URL) throws -> Promise { let localVaultConfigURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) try token.write(to: localVaultConfigURL, atomically: true, encoding: .utf8) let vaultConfigCloudPath = vaultPath.appendingPathComponent("vault.cryptomator") - return delegate.uploadFile(from: localVaultConfigURL, to: vaultConfigCloudPath, replaceExisting: false) + return provider.uploadFile(from: localVaultConfigURL, to: vaultConfigCloudPath, replaceExisting: false) } - private func createVaultFolderStructure(masterkey: Masterkey, vaultPath: CloudPath, delegate: CloudProvider) throws -> Promise { + private func createVaultFolderStructure(masterkey: Masterkey, vaultPath: CloudPath, provider: CloudProvider) throws -> Promise { let cryptor = Cryptor(masterkey: masterkey) let rootDirPath = try VaultDBManager.getRootDirectoryPath(for: cryptor, vaultPath: vaultPath) let dPath = vaultPath.appendingPathComponent("d") - return delegate.createFolder(at: dPath).then { _ -> Promise in + return provider.createFolder(at: dPath).then { _ -> Promise in let twoCharsPath = rootDirPath.deletingLastPathComponent() - return delegate.createFolder(at: twoCharsPath) + return provider.createFolder(at: twoCharsPath) }.then { - delegate.createFolder(at: rootDirPath) + provider.createFolder(at: rootDirPath) } } - // MARK: - Manual Unlock Vault - - /** - Manually unlock a vault via KEK. - - This method is used to unlock the vault with `vaultUID` if the user does not want to store his vault password in the keychain. - - Postcondition: The created VaultDecorator is cached under the corresponding `vaultUID` - */ - public func manualUnlockVault(withUID vaultUID: String, kek: [UInt8]) throws -> CloudProvider { - let cachedVault = try vaultCache.getCachedVault(withVaultUID: vaultUID) - let masterkeyFile = try MasterkeyFile.withContentFromData(data: cachedVault.masterkeyFileData) - let masterkey = try masterkeyFile.unlock(kek: kek) - return try createVaultDecorator(from: masterkey, vaultUID: vaultUID, vaultVersion: masterkeyFile.version, vaultConfigToken: cachedVault.vaultConfigToken) - } - - func createVaultDecorator(from masterkey: Masterkey, vaultUID: String, vaultVersion: Int, vaultConfigToken: String?) throws -> CloudProvider { - let vaultAccount = try vaultAccountManager.getAccount(with: vaultUID) - let delegate = try providerManager.getProvider(with: vaultAccount.delegateAccountUID) - if let vaultConfigToken = vaultConfigToken { - let unverifiedVaultConfig = try UnverifiedVaultConfig(token: vaultConfigToken) - return try createVaultDecorator(from: masterkey, unverifiedVaultConfig: unverifiedVaultConfig, delegate: delegate, vaultPath: vaultAccount.vaultPath, vaultUID: vaultUID) - } else { - return try createLegacyVaultDecorator(from: masterkey, delegate: delegate, vaultPath: vaultAccount.vaultPath, vaultUID: vaultUID, vaultVersion: vaultVersion) - } - } - - func createVaultDecorator(from masterkey: Masterkey, unverifiedVaultConfig: UnverifiedVaultConfig, delegate: CloudProvider, vaultPath: CloudPath, vaultUID: String) throws -> CloudProvider { - let decorator = try VaultProviderFactory.createVaultProvider(from: unverifiedVaultConfig, masterkey: masterkey, vaultPath: vaultPath, with: delegate) - VaultDBManager.cachedDecorators[vaultUID] = decorator - return decorator - } - - func createLegacyVaultDecorator(from masterkey: Masterkey, delegate: CloudProvider, vaultPath: CloudPath, vaultUID: String, vaultVersion: Int) throws -> CloudProvider { - let decorator = try VaultProviderFactory.createLegacyVaultProvider(from: masterkey, vaultVersion: vaultVersion, vaultPath: vaultPath, with: delegate) - VaultDBManager.cachedDecorators[vaultUID] = decorator - return decorator - } - - public func getDecorator(forVaultUID vaultUID: String) throws -> CloudProvider { - if let cachedDecorator = VaultDBManager.cachedDecorators[vaultUID] { - // MARK: Add here masterkey up to date check - - return cachedDecorator - } - let password = try passwordManager.getPassword(forVaultUID: vaultUID) - let cachedVault = try vaultCache.getCachedVault(withVaultUID: vaultUID) - let masterkeyFile = try MasterkeyFile.withContentFromData(data: cachedVault.masterkeyFileData) - let masterkey = try masterkeyFile.unlock(passphrase: password) - return try createVaultDecorator(from: masterkey, vaultUID: vaultUID, vaultVersion: masterkeyFile.version, vaultConfigToken: cachedVault.vaultConfigToken) - } - - // MARK: Open Existing Vault + // MARK: - Open Existing Vault /** Imports an existing Vault. - - Precondition: There is no VaultAccount for the `vaultUID` in the database yet - - Precondition: It exists a CloudProviderAccount with the `delegateAccountUID` in the database - - Precondition: The masterkey file at `vaultItem.vaultPath.appendingPathComponent("masterkey.cryptomator")` does exist in the cloud - - Postcondition: The masterkey file and vault config token is cached under the corresponding `vaultUID` - - Postcondition: storePasswordInKeychain <=> the password for the masterkey is stored in the keychain. - - Postcondition: The passed `vaultUID`, `delegateAccountUID` and the `vaultPath` derived from `masterkeyPath` are stored as VaultAccount in the database - - Postcondition: The created VaultDecorator is cached under the corresponding `vaultUID` + - Precondition: There is no `VaultAccount` for the `vaultUID` in the database yet. + - Precondition: A `CloudProviderAccount` with the `delegateAccountUID` exists in the database. + - Precondition: The vault config file at `vaultItem.vaultPath.appendingPathComponent("vault.cryptomator")` exists in the cloud. + - Precondition: The masterkey file at `vaultItem.vaultPath.appendingPathComponent("masterkey.cryptomator")` exists in the cloud. + - Postcondition: The masterkey file and vault config token are cached under the corresponding `vaultUID`. + - Postcondition: `storePasswordInKeychain` <=> the password for the masterkey is stored in the keychain. + - Postcondition: The passed `vaultUID`, `delegateAccountUID`, and `vaultItem.vaultPath` are stored as `VaultAccount` in the database. + - Postcondition: The created vault decorator is cached under the corresponding `vaultUID`. */ public func createFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { - let delegate: CloudProvider + let provider: LocalizedCloudProviderDecorator do { guard VaultDBManager.cachedDecorators[vaultUID] == nil else { throw VaultManagerError.vaultAlreadyExists } - delegate = try providerManager.getProvider(with: delegateAccountUID) + provider = LocalizedCloudProviderDecorator(delegate: try providerManager.getProvider(with: delegateAccountUID)) } catch { return Promise(error) } @@ -204,15 +152,15 @@ public class VaultDBManager: VaultManager { let vaultPath = vaultItem.vaultPath let vaultConfigPath = vaultPath.appendingPathComponent("vault.cryptomator") let masterkeyPath = vaultPath.appendingPathComponent("masterkey.cryptomator") - return delegate.downloadFile(from: vaultConfigPath, to: localVaultConfigURL).then { - delegate.downloadFile(from: masterkeyPath, to: localMasterkeyURL) + return provider.downloadFile(from: vaultConfigPath, to: localVaultConfigURL).then { + provider.downloadFile(from: masterkeyPath, to: localMasterkeyURL) }.then { _ -> Promise<(Masterkey, String, Void)> in let token = try String(contentsOf: localVaultConfigURL, encoding: .utf8) let unverifiedVaultConfig = try UnverifiedVaultConfig(token: token) let masterkeyFile = try MasterkeyFile.withContentFromURL(url: localMasterkeyURL) let masterkey = try masterkeyFile.unlock(passphrase: password) - let vaultProvider = try VaultProviderFactory.createVaultProvider(from: unverifiedVaultConfig, masterkey: masterkey, vaultPath: vaultPath, with: delegate) - VaultDBManager.cachedDecorators[vaultUID] = vaultProvider + let decorator = try VaultProviderFactory.createVaultProvider(from: unverifiedVaultConfig, masterkey: masterkey, vaultPath: vaultPath, with: provider.delegate) + VaultDBManager.cachedDecorators[vaultUID] = decorator return all(Promise(masterkey), Promise(token), self.addFileProviderDomain(forVaultUID: vaultUID, displayName: vaultItem.name)) }.then { masterkey, token, _ -> Void in let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: vaultItem.name) @@ -228,23 +176,23 @@ public class VaultDBManager: VaultManager { /** Imports an existing legacy Vault. - Supported legacy vault formats are 6 & 7 + Supported legacy vault formats are 6 & 7. - - Precondition: There is no VaultAccount for the `vaultUID` in the database yet - - Precondition: It exists a CloudProviderAccount with the `delegateAccountUID` in the database - - Precondition: The masterkey file at `vaultItem.vaultPath.appendingPathComponent("masterkey.cryptomator")` does exist in the cloud - - Postcondition: The masterkey file is cached under the corresponding `vaultUID` - - Postcondition: storePasswordInKeychain <=> the password for the masterkey is stored in the keychain. - - Postcondition: The passed `vaultUID`, `delegateAccountUID` and the `vaultPath` derived from `masterkeyPath` are stored as VaultAccount in the database - - Postcondition: The created VaultDecorator is cached under the corresponding `vaultUID` + - Precondition: There is no `VaultAccount` for the `vaultUID` in the database yet. + - Precondition: A `CloudProviderAccount` with the `delegateAccountUID` exists in the database. + - Precondition: The masterkey file at `vaultItem.vaultPath.appendingPathComponent("masterkey.cryptomator")` exists in the cloud. + - Postcondition: The masterkey file is cached under the corresponding `vaultUID`. + - Postcondition: `storePasswordInKeychain` <=> the password for the masterkey is stored in the keychain. + - Postcondition: The passed `vaultUID`, `delegateAccountUID`, and `vaultItem.vaultPath` are stored as `VaultAccount` in the database. + - Postcondition: The created vault decorator is cached under the corresponding `vaultUID`. */ public func createLegacyFromExisting(withVaultUID vaultUID: String, delegateAccountUID: String, vaultItem: VaultItem, password: String, storePasswordInKeychain: Bool) -> Promise { - let delegate: CloudProvider + let provider: LocalizedCloudProviderDecorator do { guard VaultDBManager.cachedDecorators[vaultUID] == nil else { throw VaultManagerError.vaultAlreadyExists } - delegate = try providerManager.getProvider(with: delegateAccountUID) + provider = LocalizedCloudProviderDecorator(delegate: try providerManager.getProvider(with: delegateAccountUID)) } catch { return Promise(error) } @@ -252,11 +200,11 @@ public class VaultDBManager: VaultManager { let localMasterkeyURL = tmpDirURL.appendingPathComponent(UUID().uuidString, isDirectory: false) let vaultPath = vaultItem.vaultPath let masterkeyPath = vaultPath.appendingPathComponent("masterkey.cryptomator") - return delegate.downloadFile(from: masterkeyPath, to: localMasterkeyURL).then { _ -> Promise<(Masterkey, MasterkeyFile, Void)> in + return provider.downloadFile(from: masterkeyPath, to: localMasterkeyURL).then { _ -> Promise<(Masterkey, MasterkeyFile, Void)> in let masterkeyFile = try MasterkeyFile.withContentFromURL(url: localMasterkeyURL) let masterkey = try masterkeyFile.unlock(passphrase: password) - let vaultProvider = try self.createLegacyVaultDecorator(from: masterkey, delegate: delegate, vaultPath: vaultPath, vaultUID: vaultUID, vaultVersion: masterkeyFile.version) - VaultDBManager.cachedDecorators[vaultUID] = vaultProvider + let decorator = try VaultProviderFactory.createLegacyVaultProvider(from: masterkey, vaultVersion: masterkeyFile.version, vaultPath: vaultPath, with: provider.delegate) + VaultDBManager.cachedDecorators[vaultUID] = decorator return all(Promise(masterkey), Promise(masterkeyFile), self.addFileProviderDomain(forVaultUID: vaultUID, displayName: vaultItem.name)) }.then { masterkey, masterkeyFile, _ -> Void in let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: vaultItem.name) @@ -269,15 +217,15 @@ public class VaultDBManager: VaultManager { } } - // MARK: Remove Vault Locally + // MARK: - Remove Vault Locally /** - - Precondition: It exists a `VaultAccount` for the `vaultUID` in the database - - Precondition: It exists a `NSFileProviderDomain` with the `vaultUID` as `identifier` - - Postcondition: No `VaultAccount` exists for the `vaultUID` in the database - - Postcondition: No password is stored for this `vaultUID` - - Postcondition: No `VaultDecorator` is cached under the corresponding `vaultUID` - - Postcondition: The `NSFileProviderDomain` with the `vaultUID` as `identifier` was removed from the NSFileProvider + - Precondition: A `VaultAccount` for the `vaultUID` exists in the database. + - Precondition: A `NSFileProviderDomain` with the `vaultUID` as `identifier` exists. + - Postcondition: The `VaultAccount` for the `vaultUID` was removed in the database. + - Postcondition: The password for the `vaultUID` was removed. + - Postcondition: The vault decorator under the corresponding `vaultUID` was removed from cache. + - Postcondition: The `NSFileProviderDomain` with the `vaultUID` as `identifier` was removed from the `NSFileProvider`. */ public func removeVault(withUID vaultUID: String) throws -> Promise { do { @@ -322,7 +270,33 @@ public class VaultDBManager: VaultManager { } } - // MARK: Internal + // MARK: - Manual Unlock Vault + + /** + Manually unlock a vault via KEK. + + This method is used to unlock the vault with `vaultUID` if the user does not want to store his vault password in the keychain. + + - Postcondition: The created vault decorator is cached under the corresponding `vaultUID`. + */ + public func manualUnlockVault(withUID vaultUID: String, kek: [UInt8]) throws -> CloudProvider { + let cachedVault = try vaultCache.getCachedVault(withVaultUID: vaultUID) + let masterkeyFile = try MasterkeyFile.withContentFromData(data: cachedVault.masterkeyFileData) + let masterkey = try masterkeyFile.unlock(kek: kek) + let vaultAccount = try vaultAccountManager.getAccount(with: vaultUID) + let provider = try providerManager.getProvider(with: vaultAccount.delegateAccountUID) + let decorator: CloudProvider + if let vaultConfigToken = cachedVault.vaultConfigToken { + let unverifiedVaultConfig = try UnverifiedVaultConfig(token: vaultConfigToken) + decorator = try VaultProviderFactory.createVaultProvider(from: unverifiedVaultConfig, masterkey: masterkey, vaultPath: vaultAccount.vaultPath, with: provider) + } else { + decorator = try VaultProviderFactory.createLegacyVaultProvider(from: masterkey, vaultVersion: masterkeyFile.version, vaultPath: vaultAccount.vaultPath, with: provider) + } + VaultDBManager.cachedDecorators[vaultUID] = decorator + return decorator + } + + // MARK: - Internal func postProcessVaultCreation(for masterkey: Masterkey, forVaultUID vaultUID: String, vaultConfigToken: String, password: String, storePasswordInKeychain: Bool) throws { let masterkeyFileData = try exportMasterkey(masterkey, vaultVersion: VaultDBManager.fakeVaultVersion, password: password) @@ -349,11 +323,6 @@ public class VaultDBManager: VaultManager { return vaultPath.appendingPathComponent("d/\(digest[.. CloudPath { - precondition(masterkeyPath.path.hasSuffix("masterkey.cryptomator")) - return masterkeyPath.deletingLastPathComponent() - } - func exportMasterkey(_ masterkey: Masterkey, vaultVersion: Int, password: String) throws -> Data { return try MasterkeyFile.lock(masterkey: masterkey, vaultVersion: vaultVersion, passphrase: password) } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift index e2c35a6a9..fb73a0ce9 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/VaultPasswordManager.swift @@ -8,6 +8,7 @@ import Foundation import LocalAuthentication + public protocol VaultPasswordManager { func setPassword(_ password: String, forVaultUID vaultUID: String) throws func getPassword(forVaultUID vaultUID: String) throws -> String diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/URLSessionError+Localization.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/URLSessionError+Localization.swift new file mode 100644 index 000000000..4b2d7d955 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/URLSessionError+Localization.swift @@ -0,0 +1,36 @@ +// +// URLSessionError+Localization.swift +// CryptomatorCommonCore +// +// Created by Tobias Hagemann on 19.08.21. +// Copyright © 2021 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import Foundation + +extension URLSessionError: LocalizedError { + public var errorDescription: String? { + switch self { + case let .httpError(_, statusCode: statusCode): + switch statusCode { + case 401: + return String(format: LocalizedString.getValue("urlSession.error.httpError.401"), statusCode) + case 403: + return String(format: LocalizedString.getValue("urlSession.error.httpError.403"), statusCode) + case 404: + return String(format: LocalizedString.getValue("urlSession.error.httpError.404"), statusCode) + case 405: + return String(format: LocalizedString.getValue("urlSession.error.httpError.405"), statusCode) + case 409: + return String(format: LocalizedString.getValue("urlSession.error.httpError.409"), statusCode) + case 412: + return String(format: LocalizedString.getValue("urlSession.error.httpError.412"), statusCode) + default: + return String(format: LocalizedString.getValue("urlSession.error.httpError.default"), statusCode) + } + case .unexpectedResponse: + return LocalizedString.getValue("urlSession.error.unexpectedResponse") + } + } +} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/VaultItem.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/VaultItem.swift index 04e18fb62..d59fa68ae 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/VaultItem.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/VaultItem.swift @@ -5,8 +5,10 @@ // Created by Philipp Schmid on 24.06.21. // Copyright © 2021 Skymatic GmbH. All rights reserved. // + import CryptomatorCloudAccessCore import Foundation + public protocol VaultItem { var name: String { get } var vaultPath: CloudPath { get } diff --git a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift index 7426efcef..dc2459896 100644 --- a/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift +++ b/CryptomatorCommon/Tests/CryptomatorCommonCoreTests/Manager/VaultManagerTests.swift @@ -269,7 +269,7 @@ class VaultManagerTests: XCTestCase { wait(for: [expectation], timeout: 1.0) } - func testCreateVaultDecoratorV8() throws { + func testManualUnlockVaultV8() throws { let delegateAccountUID = UUID().uuidString let account = CloudProviderAccount(accountUID: delegateAccountUID, cloudProviderType: .dropbox) try providerAccountManager.saveNewAccount(account) @@ -278,9 +278,14 @@ class VaultManagerTests: XCTestCase { let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: "VaultV8") try accountManager.saveNewAccount(vaultAccount) let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) + let masterkeyFileData = try MasterkeyFile.lock(masterkey: masterkey, vaultVersion: 999, passphrase: "asd", pepper: [UInt8](), scryptCostParam: 2) let vaultConfig = VaultConfig(id: "ABB9F673-F3E8-41A7-A43B-D29F5DA65068", format: 8, cipherCombo: .sivCTRMAC, shorteningThreshold: 220) - let token = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: masterkey.rawKey) - let decorator = try manager.createVaultDecorator(from: masterkey, vaultUID: vaultUID, vaultVersion: 7, vaultConfigToken: token) + let vaultConfigToken = try vaultConfig.toToken(keyId: "masterkeyfile:masterkey.cryptomator", rawKey: masterkey.rawKey) + let cachedVault = CachedVault(vaultUID: vaultUID, masterkeyFileData: masterkeyFileData, vaultConfigToken: vaultConfigToken, lastUpToDateCheck: Date()) + try vaultCacheMock.cache(cachedVault) + let masterkeyFile = try MasterkeyFile.withContentFromData(data: masterkeyFileData) + let kek = try masterkeyFile.deriveKey(passphrase: "asd") + let decorator = try manager.manualUnlockVault(withUID: vaultUID, kek: kek) guard decorator is VaultFormat8ShorteningProviderDecorator else { XCTFail("Decorator is not a VaultFormat8ShorteningProviderDecorator") return @@ -288,7 +293,7 @@ class VaultManagerTests: XCTestCase { XCTAssertNotNil(VaultDBManager.cachedDecorators[vaultUID]) } - func testCreateVaultDecoratorV7() throws { + func testManualUnlockVaultV7() throws { let delegateAccountUID = UUID().uuidString let account = CloudProviderAccount(accountUID: delegateAccountUID, cloudProviderType: .dropbox) try providerAccountManager.saveNewAccount(account) @@ -297,7 +302,12 @@ class VaultManagerTests: XCTestCase { let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: "VaultV7") try accountManager.saveNewAccount(vaultAccount) let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) - let decorator = try manager.createVaultDecorator(from: masterkey, vaultUID: vaultUID, vaultVersion: 7, vaultConfigToken: nil) + let masterkeyFileData = try MasterkeyFile.lock(masterkey: masterkey, vaultVersion: 7, passphrase: "asd", pepper: [UInt8](), scryptCostParam: 2) + let cachedVault = CachedVault(vaultUID: vaultUID, masterkeyFileData: masterkeyFileData, vaultConfigToken: nil, lastUpToDateCheck: Date()) + try vaultCacheMock.cache(cachedVault) + let masterkeyFile = try MasterkeyFile.withContentFromData(data: masterkeyFileData) + let kek = try masterkeyFile.deriveKey(passphrase: "asd") + let decorator = try manager.manualUnlockVault(withUID: vaultUID, kek: kek) guard decorator is VaultFormat7ShorteningProviderDecorator else { XCTFail("Decorator is not a VaultFormat7ShorteningProviderDecorator") return @@ -305,16 +315,21 @@ class VaultManagerTests: XCTestCase { XCTAssertNotNil(VaultDBManager.cachedDecorators[vaultUID]) } - func testCreateVaultDecoratorV6() throws { + func testManualUnlockVaultV6() throws { let delegateAccountUID = UUID().uuidString let account = CloudProviderAccount(accountUID: delegateAccountUID, cloudProviderType: .dropbox) try providerAccountManager.saveNewAccount(account) let vaultUID = UUID().uuidString - let vaultPath = CloudPath("/VaultV6/") - let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: "VaultV6") + let vaultPath = CloudPath("/VaultV1/") + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: "VaultV1") try accountManager.saveNewAccount(vaultAccount) let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) - let decorator = try manager.createVaultDecorator(from: masterkey, vaultUID: vaultUID, vaultVersion: 6, vaultConfigToken: nil) + let masterkeyFileData = try MasterkeyFile.lock(masterkey: masterkey, vaultVersion: 6, passphrase: "asd", pepper: [UInt8](), scryptCostParam: 2) + let cachedVault = CachedVault(vaultUID: vaultUID, masterkeyFileData: masterkeyFileData, vaultConfigToken: nil, lastUpToDateCheck: Date()) + try vaultCacheMock.cache(cachedVault) + let masterkeyFile = try MasterkeyFile.withContentFromData(data: masterkeyFileData) + let kek = try masterkeyFile.deriveKey(passphrase: "asd") + let decorator = try manager.manualUnlockVault(withUID: vaultUID, kek: kek) guard decorator is VaultFormat6ShorteningProviderDecorator else { XCTFail("Decorator is not a VaultFormat6ShorteningProviderDecorator") return @@ -322,21 +337,27 @@ class VaultManagerTests: XCTestCase { XCTAssertNotNil(VaultDBManager.cachedDecorators[vaultUID]) } - func testCreateVaultDecoratorThrowsForNonSupportedVaultVersion() throws { + func testManualUnlockVaultThrowsForNonSupportedVaultVersion() throws { let delegateAccountUID = UUID().uuidString let account = CloudProviderAccount(accountUID: delegateAccountUID, cloudProviderType: .dropbox) try providerAccountManager.saveNewAccount(account) let vaultUID = UUID().uuidString - let vaultPath = CloudPath("/VaultV1/") - let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: "VaultV1") + let vaultPath = CloudPath("/VaultV6/") + let vaultAccount = VaultAccount(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, vaultName: "VaultV6") try accountManager.saveNewAccount(vaultAccount) let masterkey = Masterkey.createFromRaw(aesMasterKey: [UInt8](repeating: 0x55, count: 32), macMasterKey: [UInt8](repeating: 0x77, count: 32)) - XCTAssertThrowsError(try manager.createVaultDecorator(from: masterkey, vaultUID: vaultUID, vaultVersion: 1, vaultConfigToken: nil)) { error in + let masterkeyFileData = try MasterkeyFile.lock(masterkey: masterkey, vaultVersion: 1, passphrase: "asd", pepper: [UInt8](), scryptCostParam: 2) + let cachedVault = CachedVault(vaultUID: vaultUID, masterkeyFileData: masterkeyFileData, vaultConfigToken: nil, lastUpToDateCheck: Date()) + try vaultCacheMock.cache(cachedVault) + let masterkeyFile = try MasterkeyFile.withContentFromData(data: masterkeyFileData) + let kek = try masterkeyFile.deriveKey(passphrase: "asd") + XCTAssertThrowsError(try manager.manualUnlockVault(withUID: vaultUID, kek: kek)) { error in guard case VaultProviderFactoryError.unsupportedVaultVersion = error else { XCTFail("Throws the wrong error: \(error)") return } } + XCTAssertNil(VaultDBManager.cachedDecorators[vaultUID]) } } diff --git a/CryptomatorCommonHostedTests/Keychain/VaultPasswordKeychainManagerTests.swift b/CryptomatorCommonHostedTests/Keychain/VaultPasswordKeychainManagerTests.swift index f1ed750c3..0d96ffbb1 100644 --- a/CryptomatorCommonHostedTests/Keychain/VaultPasswordKeychainManagerTests.swift +++ b/CryptomatorCommonHostedTests/Keychain/VaultPasswordKeychainManagerTests.swift @@ -8,6 +8,7 @@ import XCTest @testable import CryptomatorCommonCore + class VaultPasswordKeychainManagerTests: XCTestCase { func testSetAndRetrievePassword() throws { let passwordManager = VaultPasswordKeychainManager() diff --git a/CryptomatorFileProvider/FileProviderAdapterManager.swift b/CryptomatorFileProvider/FileProviderAdapterManager.swift index 46f5caff7..7cc5fd76d 100644 --- a/CryptomatorFileProvider/FileProviderAdapterManager.swift +++ b/CryptomatorFileProvider/FileProviderAdapterManager.swift @@ -11,6 +11,10 @@ import FileProvider import Foundation import Promises +public enum FileProviderAdapterManagerError: Error { + case cachedAdapterNotFound +} + public enum FileProviderAdapterManager { private static let queue = DispatchQueue(label: "FileProviderAdapterManager") private static var cachedAdapters = [NSFileProviderDomainIdentifier: FileProviderAdapter]() @@ -22,7 +26,7 @@ public enum FileProviderAdapterManager { if let cachedAdapter = cachedAdapters[domain.identifier] { return cachedAdapter } else { - throw VaultPasswordManagerError.passwordNotFound + throw FileProviderAdapterManagerError.cachedAdapterNotFound } } } diff --git a/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift b/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift index 86f1b43fa..1588cbc18 100644 --- a/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift +++ b/CryptomatorTests/CreateNewVaultPasswordViewModelTests.swift @@ -172,16 +172,13 @@ class CreateNewVaultPasswordViewModelTests: XCTestCase { private class VaultManagerMock: VaultManager { var createdVaults = [CreatedVault]() + func createNewVault(withVaultUID vaultUID: String, delegateAccountUID: String, vaultPath: CloudPath, password: String, storePasswordInKeychain: Bool) -> Promise { let vault = CreatedVault(vaultUID: vaultUID, delegateAccountUID: delegateAccountUID, vaultPath: vaultPath, password: password, storePasswordInKeychain: storePasswordInKeychain) createdVaults.append(vault) return Promise(()) } - func manualUnlockVault(withUID vaultUID: String, kek: [UInt8]) throws -> CloudProvider { - throw MockError.notMocked - } - func getDecorator(forVaultUID vaultUID: String) throws -> CloudProvider { throw MockError.notMocked } @@ -194,6 +191,10 @@ private class VaultManagerMock: VaultManager { return Promise(MockError.notMocked) } + func manualUnlockVault(withUID vaultUID: String, kek: [UInt8]) throws -> CloudProvider { + throw MockError.notMocked + } + func removeVault(withUID vaultUID: String) throws -> Promise { return Promise(MockError.notMocked) } @@ -201,10 +202,6 @@ private class VaultManagerMock: VaultManager { func removeAllUnusedFileProviderDomains() -> Promise { return Promise(MockError.notMocked) } - - func getVaultPath(from masterkeyPath: CloudPath) -> CloudPath { - return masterkeyPath - } } private struct CreatedVault { diff --git a/FileProviderExtensionUI/FileProviderCoordinator.swift b/FileProviderExtensionUI/FileProviderCoordinator.swift index f4cdc488c..336b600f8 100644 --- a/FileProviderExtensionUI/FileProviderCoordinator.swift +++ b/FileProviderExtensionUI/FileProviderCoordinator.swift @@ -6,7 +6,9 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CocoaLumberjackSwift import CryptomatorCommonCore +import CryptomatorFileProvider import FileProviderUI import UIKit @@ -39,7 +41,8 @@ class FileProviderCoordinator { return } switch internalError { - case let internalError as NSError where internalError == VaultPasswordManagerError.passwordNotFound as NSError: + // Workaround since iOS 15: It used to be possible to check for the equality `internalError == FileProviderAdapterManagerError.cachedAdapterNotFound as NSError` but it doesn't work anymore for unknown reasons. + case let internalError as NSError where internalError.domain == (FileProviderAdapterManagerError.cachedAdapterNotFound as NSError).domain && internalError.code == (FileProviderAdapterManagerError.cachedAdapterNotFound as NSError).code: let domain = NSFileProviderDomain(identifier: domainIdentifier, displayName: vaultName, pathRelativeToDocumentStorage: pathRelativeToDocumentStorage) showPasswordScreen(for: domain) default: @@ -48,6 +51,7 @@ class FileProviderCoordinator { } 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) diff --git a/FileProviderExtensionUI/RootViewController.swift b/FileProviderExtensionUI/RootViewController.swift index ba8f31436..966e70fd3 100644 --- a/FileProviderExtensionUI/RootViewController.swift +++ b/FileProviderExtensionUI/RootViewController.swift @@ -10,6 +10,7 @@ import CocoaLumberjackSwift import CryptomatorCommonCore import FileProviderUI import UIKit + class RootViewController: FPUIActionExtensionViewController { private lazy var coordinator: FileProviderCoordinator = { return FileProviderCoordinator(extensionContext: extensionContext, hostViewController: self) diff --git a/FileProviderExtensionUI/UnlockVaultViewModel.swift b/FileProviderExtensionUI/UnlockVaultViewModel.swift index b3a0964c0..623ef376a 100644 --- a/FileProviderExtensionUI/UnlockVaultViewModel.swift +++ b/FileProviderExtensionUI/UnlockVaultViewModel.swift @@ -172,16 +172,16 @@ class UnlockVaultViewModel { } catch { return Promise(error) } - if storePasswordInKeychain { - do { - try passwordManager.setPassword(password, forVaultUID: vaultUID) - } catch { - return Promise(error) - } - } - return fileProviderConnector.getProxy(serviceName: VaultUnlockingService.name, domain: domain).then { proxy -> Promise in return self.proxyUnlockVault(proxy, kek: kek) + }.then { + if storePasswordInKeychain { + do { + try self.passwordManager.setPassword(password, forVaultUID: self.vaultUID) + } catch { + throw error + } + } } } diff --git a/SharedResources/ca.lproj/Localizable.strings b/SharedResources/ca.lproj/Localizable.strings index d3a8a81e6..056757a7e 100644 --- a/SharedResources/ca.lproj/Localizable.strings +++ b/SharedResources/ca.lproj/Localizable.strings @@ -63,5 +63,6 @@ "unlockVault.button.unlock" = "Desbloqueja"; "unlockVault.password.footer" = "Introduïu la contrasenya de \"%@\""; +"urlSession.error.unexpectedResponse" = "Hi ha hagut una resposta de xarxa inesperada."; "webDAVAuthentication.title" = "WebDAV"; diff --git a/SharedResources/cs.lproj/Localizable.strings b/SharedResources/cs.lproj/Localizable.strings index 5ad57bdc2..bcee6d640 100644 --- a/SharedResources/cs.lproj/Localizable.strings +++ b/SharedResources/cs.lproj/Localizable.strings @@ -21,6 +21,9 @@ "common.cells.url" = "URL"; "common.cells.username" = "Uživatel"; "common.footer.learnMore" = "Více informací."; + +"accountList.header.title" = "Ověření"; +"accountList.emptyList.message" = "Klepnutím sem přidáte účet"; "accountList.signOut.alert.title" = "Odstranit přidružené trezory?"; "accountList.signOut.alert.message" = "Odhlášením budou všechny přidružené trezory odstraněny ze seznamu trezorů. Žádná zašifrovaná data nebudou odstraněna. Můžete se znovu přihlásit a trezory znovu přidat později."; @@ -28,15 +31,100 @@ "addVault.createNewVault.title" = "Vytvořit nový trezor"; "addVault.createNewVault.setVaultName.header.title" = "Zvolte jméno trezoru."; "addVault.createNewVault.setVaultName.cells.name" = "Název trezoru"; +"addVault.createNewVault.setVaultName.error.emptyVaultName" = "Je potřeba zadat název trezoru."; +"addVault.createNewVault.setVaultName.error.invalidInput" = "Název trezoru by nesmí obsahovat \\ / : * ? \" < > | nebo končit tečkou."; "addVault.createNewVault.chooseCloud.header" = "Kde by měl Cryptomator ukládat šifrované soubory vašeho trezoru?"; +"addVault.createNewVault.chooseFolder.error.vaultNameCollision" = "\"%@\" již v tomtom umístění existuje. Vyberte jiný trezor nebo umístění."; "addVault.createNewVault.detectedMasterkey.text" = "Cryptomator v tomto umístění zjistil existující trezor.\nChcete-li vytvořit nový trezor, vraťte se zpět a vyberte jinou složku."; "addVault.createNewVault.password.enterPassword.header" = "Zadejte nové heslo."; "addVault.createNewVault.password.confirmPassword.header" = "Potvrď nové heslo."; +"addVault.createNewVault.password.confirmPassword.alert.title" = "Potvrdit heslo?"; +"addVault.createNewVault.password.confirmPassword.alert.message" = "DŮLEŽITÉ: Pokud zapomenete své heslo, neexistuje způsob jak obnovit vaše data."; +"addVault.createNewVault.password.error.emptyPassword" = "Heslo nemůže zůstat nevyplněno."; "addVault.createNewVault.password.error.nonMatchingPasswords" = "Hesla se neshodují."; +"addVault.createNewVault.password.error.tooShortPassword" = "Heslo musí obsahovat alespoň 8 znaků."; "addVault.openExistingVault.title" = "Otevřít existující trezor"; +"addVault.openExistingVault.chooseCloud.header" = "Kde je trezor umístěn?"; +"addVault.openExistingVault.detectedMasterkey.text" = "Cryptomator detekoval trezor \"%@\".\nChcete přidat tento trezor?"; +"addVault.openExistingVault.detectedMasterkey.add" = "Přidat tento trezor"; "addVault.openExistingVault.password.footer" = "Zadejte heslo pro \"%@\""; +"addVault.success.info" = "Úspěšně přidán trezor \"%@\".\nPřistupujte k tomuto trezoru prostřednictvím aplikace Soubory."; +"addVault.success.openFilesApp" = "Otevřít aplikaci Soubory"; +"addVault.success.footer" = "Pokud jste již neučinili, povolte Cryptomator v aplikaci Soubory."; + +"biometryType.faceID" = "Face ID"; +"biometryType.touchID" = "Touch ID"; + +"chooseFolder.emptyFolder.footer" = "Složka je prázdná"; +"chooseFolder.createNewFolder.header.title" = "Zvolte název složky."; +"chooseFolder.createNewFolder.cells.name" = "Název složky"; +"chooseFolder.createNewFolder.error.emptyFolderName" = "Název složky nemůže být prázdný."; + +"cloudProvider.error.itemNotFound" = "\"%@\" nebylo nalezeno."; +"cloudProvider.error.itemAlreadyExists" = "\"%@\" již existuje."; +"cloudProvider.error.itemTypeMismatch" = "\"%@\" má neočekávaný typ položky."; +"cloudProvider.error.parentFolderDoesNotExist" = "Nadřazená složka \"%@\" neexistuje."; +"cloudProvider.error.pageTokenInvalid" = "Načítání obsahu adresáře nemůže pokračovat."; +"cloudProvider.error.quotaInsufficient" = "Vaše úložiště nemá dostatek místa."; +"cloudProvider.error.unauthorized" = "Nelze provést neautorizovanou operaci."; +"cloudProvider.error.noInternetConnection" = "Pro tuto operaci je nutné připojení k internetu."; "cloudProviderType.localFileSystem" = "Další poskytovatel souborů"; +"localFileSystemAuthentication.createNewVault.header" = "Na další obrazovce vyberte umístění úložiště pro váš nový trezor."; +"localFileSystemAuthentication.createNewVault.button" = "Vyberte umístění úložiště"; +"localFileSystemAuthentication.createNewVault.error.detectedExistingVault" = "V tomto umístění již trezor existuje. Zkuste to prosím znovu s jiným umístěním."; +"localFileSystemAuthentication.openExistingVault.header" = "Na další obrazovce vyberte složku vašeho existujícího trezoru."; +"localFileSystemAuthentication.openExistingVault.button" = "Vybrat složku trezoru"; +"localFileSystemAuthentication.openExistingVault.error.noVaultFound" = "Vybraná složka není trezor. Zkuste to prosím znovu s jinou složkou."; + +"onboarding.title" = "Vítejte"; +"onboarding.info" = "Děkujeme, že jste si vybrali Cryptomator pro ochranu vašich souborů. Chcete-li začít, přejděte do hlavní aplikace a přidejte trezor."; +"onboarding.openCryptomator.button" = "Otevřít Cryptomator"; + +"settings.title" = "Nastavení"; +"settings.aboutCryptomator" = "O Cryptomatoru"; +"settings.aboutCryptomator.title" = "Verze %@ (%@)"; +"settings.sendLogFile" = "Odeslat log soubor"; + "unlockVault.button.unlock" = "Odemknout"; +"unlockVault.button.unlockVia" = "Odemknout pomocí %@"; "unlockVault.password.footer" = "Zadejte heslo pro \"%@\""; +"unlockVault.enableBiometricalUnlock.switch" = "Povolit %@"; +"unlockVault.enableBiometricalUnlock.footer" = "Namísto odemknutí vašeho trezoru pomocí hesla jej můžete odemknout přes %@."; +"unlockVault.evaluatePolicy.reason" = "Odemknout váš trezor"; + +"untrustedTLSCertificate.title" = "Neplatný TLS certifikát"; +"untrustedTLSCertificate.message" = "TLS certifikát \"%@\" je neplatný. Chcete mu přesto důvěřovat?\n\n SHA-256: %@"; +"untrustedTLSCertificate.add" = "Důvěřovat"; +"untrustedTLSCertificate.dismiss" = "Nedůvěřovat"; + +"urlSession.error.httpError.401" = "Nesprávné uživatelské jméno a/nebo heslo."; +"urlSession.error.httpError.403" = "Nedostatečná práva k požadovanému zdroji."; +"urlSession.error.httpError.404" = "Požadovaný zdroj nebyl nalezen."; +"urlSession.error.httpError.405" = "Metoda požadavku není podporována cílovým zdrojem."; +"urlSession.error.httpError.409" = "Konflikt požadavku se současným stavem cílového zdroje."; +"urlSession.error.httpError.412" = "Přístup k cílovému zdroji byl odepřen."; +"urlSession.error.httpError.default" = "Připojení k síti selhalo se stavovým kódem %ld."; +"urlSession.error.unexpectedResponse" = "Došlo k neočekávané odpovědi sítě."; + +"vaultDetail.button.lock" = "Uzamknout nyní"; +"vaultDetail.button.removeVault" = "Odebrat ze seznamu trezorů"; +"vaultDetail.disabledBiometricalUnlock.footer" = "Pokud povolíte %@, vaše heslo k trezoru bude uloženo v iOS klíčence."; +"vaultDetail.enabledBiometricalUnlock.footer" = "Vaše heslo k trezoru bude vyžadováno pouze v případě, že %@ ověření selže."; +"vaultDetail.info.footer.accessVault" = "Přistoupit k trezoru prostřednictvím aplikace Soubory."; +"vaultDetail.info.footer.accountInfo" = "Přihlášen jako %@ přes %@."; +"vaultDetail.locked.footer" = "Váš trezor je momentálně uzamčen."; +"vaultDetail.removeVault.footer" = "Tímto odstraníte pouze trezor ze seznamu trezorů a neodstraníte žádné šifrované soubory."; +"vaultDetail.unlocked.footer" = "Váš trezor je v současné době odemčen v aplikaci Soubory."; +"vaultDetail.unlockVault.footer" = "Zadejte heslo pro \"%@\" pro uložení do iOS klíčenky a pro povolení %@."; + +"vaultList.header.title" = "Trezory"; +"vaultList.emptyList.message" = "Klikněte zde pro přidání nového trezoru"; +"vaultList.remove.alert.title" = "Odstranit trezor?"; +"vaultList.remove.alert.message" = "Tímto odstraníte trezor pouze ze seznamu trezorů. Šifrovaná data nebudou smazána. Později můžete trezor znovu přidat."; + +"vaultProviderFactory.error.unsupportedVaultConfig" = "Konfigurace trezoru není podporována. Ujistěte se, že používáte nejnovější verzi Cryptomatoru."; +"vaultProviderFactory.error.unsupportedVaultVersion" = "Verze trezoru není podporována. Tento trezor byl vytvořen se starší nebo novější verzí Cryptomatoru."; + +"webDAVAuthentication.title" = "WebDAV"; diff --git a/SharedResources/de.lproj/Localizable.strings b/SharedResources/de.lproj/Localizable.strings index 3dc46d0a4..bfd8ea035 100644 --- a/SharedResources/de.lproj/Localizable.strings +++ b/SharedResources/de.lproj/Localizable.strings @@ -53,7 +53,7 @@ "addVault.success.footer" = "Falls noch nicht erledigt, aktiviere Cryptomator in der App „Dateien“."; "biometryType.faceID" = "Face ID"; -"biometryType.touchID" = "Fingerabdruck"; +"biometryType.touchID" = "Touch ID"; "chooseFolder.emptyFolder.footer" = "Ordner ist leer"; "chooseFolder.createNewFolder.header.title" = "Wähle einen Namen für den Ordner."; @@ -99,6 +99,15 @@ "untrustedTLSCertificate.add" = "Vertrauen"; "untrustedTLSCertificate.dismiss" = "Nicht vertrauen"; +"urlSession.error.httpError.401" = "Benutzername und/oder Passwort falsch."; +"urlSession.error.httpError.403" = "Unzureichende Rechte für die angeforderte Ressource."; +"urlSession.error.httpError.404" = "Angeforderte Ressource nicht gefunden."; +"urlSession.error.httpError.405" = "Anfragemethode wird von der Zielressource nicht unterstützt."; +"urlSession.error.httpError.409" = "Anfragekonflikt mit dem aktuellen Zustand der Zielressource."; +"urlSession.error.httpError.412" = "Zugriff auf die Zielressource verweigert."; +"urlSession.error.httpError.default" = "Netzwerkverbindung mit Statuscode %ld fehlgeschlagen."; +"urlSession.error.unexpectedResponse" = "Ein unerwarteter Netzwerkfehler ist aufgetreten."; + "vaultDetail.button.lock" = "Jetzt sperren"; "vaultDetail.button.removeVault" = "Aus Tresorliste entfernen"; "vaultDetail.disabledBiometricalUnlock.footer" = "Wenn du %@ aktivierst, wird dein Tresor-Passwort im iOS-Schlüsselbund gespeichert."; diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 74bd27197..0803fe937 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -99,6 +99,15 @@ "untrustedTLSCertificate.add" = "Trust"; "untrustedTLSCertificate.dismiss" = "Don't Trust"; +"urlSession.error.httpError.401" = "Wrong username and/or password."; +"urlSession.error.httpError.403" = "Insufficient rights to requested resource."; +"urlSession.error.httpError.404" = "Requested resource not found."; +"urlSession.error.httpError.405" = "Request method not supported by the target resource."; +"urlSession.error.httpError.409" = "Request conflict with current state of the target resource."; +"urlSession.error.httpError.412" = "Access to the target resource denied."; +"urlSession.error.httpError.default" = "Network connection failed with status code %ld."; +"urlSession.error.unexpectedResponse" = "There was an unexpected network response."; + "vaultDetail.button.lock" = "Lock Now"; "vaultDetail.button.removeVault" = "Remove from Vault List"; "vaultDetail.disabledBiometricalUnlock.footer" = "If you enable %@, your vault password will be stored in the iOS keychain."; diff --git a/SharedResources/it.lproj/Localizable.strings b/SharedResources/it.lproj/Localizable.strings index afe8d2556..998f5db1a 100644 --- a/SharedResources/it.lproj/Localizable.strings +++ b/SharedResources/it.lproj/Localizable.strings @@ -99,6 +99,15 @@ "untrustedTLSCertificate.add" = "Fidati"; "untrustedTLSCertificate.dismiss" = "Non Fidarti"; +"urlSession.error.httpError.401" = "Nome utente e/o password errati."; +"urlSession.error.httpError.403" = "Diritti insufficienti per la risorsa richiesta."; +"urlSession.error.httpError.404" = "Risorsa richiesta non trovata."; +"urlSession.error.httpError.405" = "Metodo di richiesta non supportato dalla risorsa di destinazione."; +"urlSession.error.httpError.409" = "Conflitto di richiesta con lo stato corrente della risorsa di destinazione."; +"urlSession.error.httpError.412" = "Accesso alla risorsa di destinazione negato."; +"urlSession.error.httpError.default" = "Connessione di rete non riuscita con codice di stato %ld."; +"urlSession.error.unexpectedResponse" = "Si è verificata una risposta di rete imprevista."; + "vaultDetail.button.lock" = "Blocca Ora"; "vaultDetail.button.removeVault" = "Rimuovi dalla Lista della Cassaforte"; "vaultDetail.disabledBiometricalUnlock.footer" = "Se abiliti %@, la password della cassaforte sarà memorizzata nel portachiavi iOS."; diff --git a/SharedResources/pl.lproj/Localizable.strings b/SharedResources/pl.lproj/Localizable.strings index 92adc9d17..1dd53a471 100644 --- a/SharedResources/pl.lproj/Localizable.strings +++ b/SharedResources/pl.lproj/Localizable.strings @@ -98,6 +98,7 @@ "untrustedTLSCertificate.message" = "Certyfikat TLS \"%@\" jest nieprawidłowy. Czy mimo to jest zaufany?\n\n SHA-256: %@"; "untrustedTLSCertificate.add" = "Zaufany"; "untrustedTLSCertificate.dismiss" = "Niezaufany"; +"urlSession.error.unexpectedResponse" = "Nieoczekiwana odpowiedź z sieci."; "vaultDetail.button.lock" = "Zablokuj"; "vaultDetail.button.removeVault" = "Usuń z listy sejfów"; diff --git a/SharedResources/sk.lproj/Localizable.strings b/SharedResources/sk.lproj/Localizable.strings index df8e81e91..d2ace2164 100644 --- a/SharedResources/sk.lproj/Localizable.strings +++ b/SharedResources/sk.lproj/Localizable.strings @@ -99,6 +99,15 @@ "untrustedTLSCertificate.add" = "Veriť"; "untrustedTLSCertificate.dismiss" = "Neveriť"; +"urlSession.error.httpError.401" = "Nesprávne používateľské meno alebo heslo."; +"urlSession.error.httpError.403" = "Nedostatočné práva k požadovanému zdroju."; +"urlSession.error.httpError.404" = "Požadovaný zdroj nenájdený."; +"urlSession.error.httpError.405" = "Požadovaná metóda nie je podporovaná cieľovým zdrojom."; +"urlSession.error.httpError.409" = "Konflikt požiadavky so súčasným stavom cieľového zdroja."; +"urlSession.error.httpError.412" = "Prístup do cieľového zdroja zamietnutý."; +"urlSession.error.httpError.default" = "Sieťové pripojenie zlyhalo s kódom %ld."; +"urlSession.error.unexpectedResponse" = "Udiala sa neočakávaná sieťová reakcia."; + "vaultDetail.button.lock" = "Uzamknúť teraz"; "vaultDetail.button.removeVault" = "Odstrániť zo zoznamu trezora"; "vaultDetail.disabledBiometricalUnlock.footer" = "Ak povolíte %@, Vaše heslo trezorova bude uchované v iOS keychain."; diff --git a/SharedResources/zh-Hant.lproj/Localizable.strings b/SharedResources/zh-Hant.lproj/Localizable.strings index 991db29ca..c9d4fe144 100644 --- a/SharedResources/zh-Hant.lproj/Localizable.strings +++ b/SharedResources/zh-Hant.lproj/Localizable.strings @@ -1,3 +1,11 @@ +/* + Localizable.strings + Cryptomator + + Copyright © 2021 Skymatic GmbH. All rights reserved. +*/ + +"common.alert.error.title" = "錯誤"; "common.button.cancel" = "取消"; "common.button.done" = "完成"; "common.button.next" = "繼續"; @@ -14,5 +22,10 @@ "addVault.createNewVault.password.error.nonMatchingPasswords" = "密碼不符."; "addVault.openExistingVault.title" = "開啟現有加密檔案庫"; +"biometryType.faceID" = "Face ID"; +"biometryType.touchID" = "Touch ID"; + +"cloudProviderType.localFileSystem" = "其他儲存空間"; + "unlockVault.button.unlock" = "解鎖"; "vaultList.remove.alert.title" = "移除加密檔案庫?"; diff --git a/fastlane/changelog.txt b/fastlane/changelog.txt index 80c309c79..b35d68828 100644 --- a/fastlane/changelog.txt +++ b/fastlane/changelog.txt @@ -6,11 +6,10 @@ We're looking forward to your feedback! --- -## Changes Since 2.0.0 Beta 3 -- Removed Cryptomator itself as a file provider for third-party apps (see #48) -- Fixed file upload in OneDrive (#78) -- Fixed greyed out files (#67) -- Fixed some UI inconsistencies (#75, #76) -- Improved logging +## Changes Since 2.0.0 Beta 4 +- Added Czech translation +- Fixed unshown password screen on iOS 15 (#81) +- Fixed wrong password being potentially stored in keychain for Touch ID / Face ID +- Improved logging and WebDAV error messages Be aware that with this update, you may have to set up your vaults again. This will happen during the beta from time to time, probably more often in the beginning. Sorry for the inconvenience that this may cause. \ No newline at end of file