diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 239704ee85..d1047da8a1 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -222,6 +222,7 @@ F70CEF5623E9C7E50007035B /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; }; F70D7C3725FFBF82002B9E34 /* NCCollectionViewCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70D7C3525FFBF81002B9E34 /* NCCollectionViewCommon.swift */; }; F70D8D8124A4A9BF000A5756 /* NCNetworkingProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70D8D8024A4A9BF000A5756 /* NCNetworkingProcess.swift */; }; + F71070AB2F7E49F200AEE58A /* NCEndToEndSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71070AA2F7E49E100AEE58A /* NCEndToEndSetup.swift */; }; F710D1F52405770F00A6033D /* NCViewerPDF.swift in Sources */ = {isa = PBXBuildFile; fileRef = F710D1F42405770F00A6033D /* NCViewerPDF.swift */; }; F710D2022405826100A6033D /* NCContextMenuViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F710D2012405826100A6033D /* NCContextMenuViewer.swift */; }; F710FC80277B7D2700AA9FBF /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F710FC7F277B7D2700AA9FBF /* RealmSwift */; }; @@ -495,7 +496,6 @@ F76882252C0DD1E7001CF441 /* NCSettingsAdvancedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768820A2C0DD1E7001CF441 /* NCSettingsAdvancedModel.swift */; }; F76882262C0DD1E7001CF441 /* NCSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768820C2C0DD1E7001CF441 /* NCSettingsView.swift */; }; F76882272C0DD1E7001CF441 /* NCManageE2EEView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768820E2C0DD1E7001CF441 /* NCManageE2EEView.swift */; }; - F76882282C0DD1E7001CF441 /* NCEndToEndInitialize.swift in Sources */ = {isa = PBXBuildFile; fileRef = F768820F2C0DD1E7001CF441 /* NCEndToEndInitialize.swift */; }; F76882292C0DD1E7001CF441 /* NCManageE2EEModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76882102C0DD1E7001CF441 /* NCManageE2EEModel.swift */; }; F768822A2C0DD1E7001CF441 /* NCSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76882112C0DD1E7001CF441 /* NCSettingsModel.swift */; }; F768822C2C0DD1E7001CF441 /* NCPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76882132C0DD1E7001CF441 /* NCPreferences.swift */; }; @@ -656,7 +656,6 @@ F799DF852C4B7E56003410B5 /* NCSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F799DF842C4B7E56003410B5 /* NCSectionHeader.swift */; }; F799DF862C4B7E56003410B5 /* NCSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F799DF842C4B7E56003410B5 /* NCSectionHeader.swift */; }; F799DF882C4B83CC003410B5 /* NCCollectionViewCommon+EasyTipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F799DF872C4B83CC003410B5 /* NCCollectionViewCommon+EasyTipView.swift */; }; - F799DF8B2C4B84EB003410B5 /* NCCollectionViewCommon+EndToEndInitialize.swift in Sources */ = {isa = PBXBuildFile; fileRef = F799DF8A2C4B84EB003410B5 /* NCCollectionViewCommon+EndToEndInitialize.swift */; }; F79A65C32191D90F00FF6DCC /* NCSelect.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F79A65C22191D90F00FF6DCC /* NCSelect.storyboard */; }; F79A65C62191D95E00FF6DCC /* NCSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79A65C52191D95E00FF6DCC /* NCSelect.swift */; }; F79B646026CA661600838ACA /* UIControl+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79B645F26CA661600838ACA /* UIControl+Extension.swift */; }; @@ -1325,6 +1324,7 @@ F70D7C3525FFBF81002B9E34 /* NCCollectionViewCommon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCCollectionViewCommon.swift; sourceTree = ""; }; F70D8D8024A4A9BF000A5756 /* NCNetworkingProcess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingProcess.swift; sourceTree = ""; }; F70F96AF2874394B006C8379 /* Nextcloud-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Nextcloud-Bridging-Header.h"; sourceTree = ""; }; + F71070AA2F7E49E100AEE58A /* NCEndToEndSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndSetup.swift; sourceTree = ""; }; F710D1F42405770F00A6033D /* NCViewerPDF.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCViewerPDF.swift; sourceTree = ""; }; F710D2012405826100A6033D /* NCContextMenuViewer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCContextMenuViewer.swift; sourceTree = ""; }; F711A4DB2AF92CAD00095DD8 /* NCUtility+Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCUtility+Date.swift"; sourceTree = ""; }; @@ -1467,7 +1467,6 @@ F768820A2C0DD1E7001CF441 /* NCSettingsAdvancedModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCSettingsAdvancedModel.swift; sourceTree = ""; }; F768820C2C0DD1E7001CF441 /* NCSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCSettingsView.swift; sourceTree = ""; }; F768820E2C0DD1E7001CF441 /* NCManageE2EEView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCManageE2EEView.swift; sourceTree = ""; }; - F768820F2C0DD1E7001CF441 /* NCEndToEndInitialize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCEndToEndInitialize.swift; sourceTree = ""; }; F76882102C0DD1E7001CF441 /* NCManageE2EEModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCManageE2EEModel.swift; sourceTree = ""; }; F76882112C0DD1E7001CF441 /* NCSettingsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCSettingsModel.swift; sourceTree = ""; }; F76882132C0DD1E7001CF441 /* NCPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCPreferences.swift; sourceTree = ""; }; @@ -1582,7 +1581,6 @@ F799DF812C4B7DCC003410B5 /* NCSectionFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCSectionFooter.swift; sourceTree = ""; }; F799DF842C4B7E56003410B5 /* NCSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCSectionHeader.swift; sourceTree = ""; }; F799DF872C4B83CC003410B5 /* NCCollectionViewCommon+EasyTipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+EasyTipView.swift"; sourceTree = ""; }; - F799DF8A2C4B84EB003410B5 /* NCCollectionViewCommon+EndToEndInitialize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCCollectionViewCommon+EndToEndInitialize.swift"; sourceTree = ""; }; F79A65C22191D90F00FF6DCC /* NCSelect.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCSelect.storyboard; sourceTree = ""; }; F79A65C52191D95E00FF6DCC /* NCSelect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCSelect.swift; sourceTree = ""; }; F79B645F26CA661600838ACA /* UIControl+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+Extension.swift"; sourceTree = ""; }; @@ -2526,7 +2524,6 @@ F747EB0C2C4AC1FF00F959A8 /* NCCollectionViewCommon+CollectionViewDelegateFlowLayout.swift */, F7D890742BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift */, F799DF872C4B83CC003410B5 /* NCCollectionViewCommon+EasyTipView.swift */, - F799DF8A2C4B84EB003410B5 /* NCCollectionViewCommon+EndToEndInitialize.swift */, F778231D2C42C07C001BB94F /* NCCollectionViewCommon+MediaLayout.swift */, F7865FF02F39D32500D09AE4 /* NCCollectionViewCommon+Search.swift */, F36E64F62B9245210085ABB5 /* NCCollectionViewCommon+SelectTabBarDelegate.swift */, @@ -2619,7 +2616,7 @@ F768820D2C0DD1E7001CF441 /* E2EE */ = { isa = PBXGroup; children = ( - F768820F2C0DD1E7001CF441 /* NCEndToEndInitialize.swift */, + F71070AA2F7E49E100AEE58A /* NCEndToEndSetup.swift */, F76882102C0DD1E7001CF441 /* NCManageE2EEModel.swift */, F768820E2C0DD1E7001CF441 /* NCManageE2EEView.swift */, ); @@ -4477,7 +4474,6 @@ F761856B29E98543006EB3B0 /* NCIntroViewController.swift in Sources */, F7743A142C33F13A0034F670 /* NCCollectionViewCommon+CollectionViewDataSource.swift in Sources */, F321DA8A2B71205A00DDA0E6 /* NCTrashSelectTabBar.swift in Sources */, - F76882282C0DD1E7001CF441 /* NCEndToEndInitialize.swift in Sources */, F702F2CD25EE5B4F008F8E80 /* AppDelegate.swift in Sources */, F769454022E9F077000A798A /* NCSharePaging.swift in Sources */, F7C687E92D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */, @@ -4665,6 +4661,7 @@ F7A03E352D427312007AA677 /* NCMainNavigationController.swift in Sources */, F769CA192966EA3C00039397 /* ComponentView.swift in Sources */, F7C9B91D2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */, + F71070AB2F7E49F200AEE58A /* NCEndToEndSetup.swift in Sources */, AF93474C27E34120002537EE /* NCUtility+Image.swift in Sources */, F702F30125EE5D2C008F8E80 /* NYMnemonic.m in Sources */, AF93474E27E3F212002537EE /* NCShareNewUserAddComment.swift in Sources */, @@ -4756,7 +4753,6 @@ F3DDFE0F2F15453900A784C8 /* NCAssistantChat.swift in Sources */, F7D68FCC28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift in Sources */, F76882292C0DD1E7001CF441 /* NCManageE2EEModel.swift in Sources */, - F799DF8B2C4B84EB003410B5 /* NCCollectionViewCommon+EndToEndInitialize.swift in Sources */, F7CCAB512ECF316700F8E68B /* NCCollectionViewCommon+SyncMetadata.swift in Sources */, AA8E041D2D300FDE00E7E89C /* NCShareNetworkingDelegate.swift in Sources */, F78E2D6529AF02DB0024D4F3 /* Database.swift in Sources */, diff --git a/iOSClient/Extensions/UIAlertController+Extension.swift b/iOSClient/Extensions/UIAlertController+Extension.swift index 35b5487bfa..ef1cd7473a 100644 --- a/iOSClient/Extensions/UIAlertController+Extension.swift +++ b/iOSClient/Extensions/UIAlertController+Extension.swift @@ -70,7 +70,7 @@ extension UIAlertController { } } if createFolderResults.error == .success { - let error = await NCNetworkingE2EEMarkFolder().markFolderE2ee(account: session.account, serverUrlFileName: serverUrlFileName, userId: session.userId, sceneIdentifier: sceneIdentifier) + let error = await NCNetworkingE2EEMarkFolder().markFolderE2ee(account: session.account, serverUrlFileName: serverUrlFileName, userId: session.userId, sceneIdentifier: nil) if let banner, let token { if error == .success { completeHudIndeterminateBannerSuccess(token: token, banner: banner) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift index 2940ace9c0..2725d002ae 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift @@ -16,10 +16,26 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { if metadata.e2eEncrypted { if capabilities.e2EEEnabled { if !NCPreferences().isEndToEndEnabled(account: metadata.account) { - let e2ee = NCEndToEndInitialize() - e2ee.delegate = self - e2ee.initEndToEndEncryption(controller: self.controller, metadata: metadata) - return + do { + let e2ee = NCEndToEndSetup(controller: controller) + try await e2ee.start() + } catch let error as NKError { + if error.errorCode == NSUserCancelledError { + return + } + await showErrorBanner( + windowScene: windowScene, + text: error.errorDescription + ) + return + } catch { + // fallback (non NKError) + await showErrorBanner( + windowScene: windowScene, + text: error.localizedDescription + ) + return + } } } else { await showInfoBanner(windowScene: windowScene, text: "_e2e_server_disabled_") diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+EndToEndInitialize.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+EndToEndInitialize.swift deleted file mode 100644 index dc885f6a28..0000000000 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+EndToEndInitialize.swift +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2024 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - -import Foundation -import UIKit - -extension NCCollectionViewCommon: NCEndToEndInitializeDelegate { - func endToEndInitializeSuccess(metadata: tableMetadata?) { - if let metadata { - pushMetadata(metadata) - } - } -} diff --git a/iOSClient/Networking/E2EE/NCNetworkingE2EEMarkFolder.swift b/iOSClient/Networking/E2EE/NCNetworkingE2EEMarkFolder.swift index 1f2fc2e72e..113e19079e 100644 --- a/iOSClient/Networking/E2EE/NCNetworkingE2EEMarkFolder.swift +++ b/iOSClient/Networking/E2EE/NCNetworkingE2EEMarkFolder.swift @@ -29,7 +29,8 @@ class NCNetworkingE2EEMarkFolder: NSObject { // BANNER // #if !EXTENSION - if let windowScene = SceneManager.shared.getWindow(sceneIdentifier: sceneIdentifier)?.windowScene { + if let sceneIdentifier, + let windowScene = SceneManager.shared.getWindow(sceneIdentifier: sceneIdentifier)?.windowScene { (banner, token) = showHudIndeterminateBanner(windowScene: windowScene, title: "_e2ee_encrypt_folder_") } #endif diff --git a/iOSClient/Settings/E2EE/NCEndToEndInitialize.swift b/iOSClient/Settings/E2EE/NCEndToEndInitialize.swift deleted file mode 100644 index 874c0708f6..0000000000 --- a/iOSClient/Settings/E2EE/NCEndToEndInitialize.swift +++ /dev/null @@ -1,366 +0,0 @@ -// SPDX-FileCopyrightText: Nextcloud GmbH -// SPDX-FileCopyrightText: 2017 Marino Faggiana -// SPDX-License-Identifier: GPL-3.0-or-later - -import UIKit -import NextcloudKit - -@MainActor -@objc protocol NCEndToEndInitializeDelegate { - func endToEndInitializeSuccess(metadata: tableMetadata?) -} - -@MainActor -class NCEndToEndInitialize: NSObject { - @objc weak var delegate: NCEndToEndInitializeDelegate? - let utilityFileSystem = NCUtilityFileSystem() - var extractedPublicKey: String? - var controller: NCMainTabBarController? - var metadata: tableMetadata? - - var session: NCSession.Session { - NCSession.shared.getSession(controller: controller) - } - - var windowScene: UIWindowScene? { - SceneManager.shared.getWindowScene(controller: controller) - } - - // -------------------------------------------------------------------------------------------- - // MARK: Initialize - // -------------------------------------------------------------------------------------------- - - func initEndToEndEncryption(controller: NCMainTabBarController?, metadata: tableMetadata?) { - self.controller = controller - self.metadata = metadata - - // Clear all keys - NCPreferences().clearAllKeysEndToEnd(account: session.account) - self.getPublicKey() - } - - func statusOfService(session: NCSession.Session, completion: @escaping (_ error: NKError?) -> Void) { - NextcloudKit.shared.getE2EECertificate(account: session.account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: session.account, - name: "getE2EECertificate") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, _, _, _, error in - completion(error) - } - } - - private func getPublicKey() { - NextcloudKit.shared.getE2EECertificate(account: session.account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.session.account, - name: "getE2EECertificate") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { account, certificate, _, _, error in - if error == .success, let certificate { - NCPreferences().setEndToEndCertificate(account: account, certificate: certificate) - self.extractedPublicKey = NCEndToEndEncryption.shared().extractPublicKey(fromCertificate: certificate) - // Request PrivateKey chiper to Server - self.getPrivateKeyCipher() - } else if error != .success { - switch error.errorCode { - case NCGlobal.shared.errorBadRequest: - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "E2E get publicKey - Bad request: internal error") - } - case NCGlobal.shared.errorResourceNotFound: - guard let csr = NCEndToEndEncryption.shared().createCSR(self.session.userId, directory: self.utilityFileSystem.directoryUserData) else { - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "Error creating CSR") - } - return - } - - NextcloudKit.shared.signE2EECertificate(certificate: csr, account: account) {task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, - name: "signE2EECertificate") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { account, certificate, _, error in - if error == .success, let certificate { - // TEST publicKey - let extractedPublicKey = NCEndToEndEncryption.shared().extractPublicKey(fromCertificate: certificate) - if extractedPublicKey != NCEndToEndEncryption.shared().generatedPublicKey { - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "E2E sign publicKey: the public key is incorrect") - } - } else { - NCPreferences().setEndToEndCertificate(account: account, certificate: certificate) - // Request PrivateKey chiper to Server - self.getPrivateKeyCipher() - } - } else if error != .success { - Task { - switch error.errorCode { - case NCGlobal.shared.errorBadRequest: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E sign publicKey: bad request: internal error") - case NCGlobal.shared.errorConflict: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E sign publicKey: conflict, a public key for the user already exists") - default: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E sign publicKey: \(error.errorDescription)") - } - } - } - } - case NCGlobal.shared.errorConflict: - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "E2E get publicKey: forbidden, the user can't access the public keys") - } - default: - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "E2E get publicKey: \(error.errorDescription)") - } - } - } - } - } - - func detectPrivateKeyFormat(from data: Data) -> String { - print("🔍 Hex dump:", data.prefix(32).map { String(format: "%02X", $0) }.joined(separator: " ")) - - // PKCS#8 has OBJECT IDENTIFIER for RSA - let oidRsaPrefix: [UInt8] = [0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01] - - if data.range(of: Data(oidRsaPrefix)) != nil { - print("🔐 Format: PKCS#8 (BEGIN PRIVATE KEY)") - return "PKCS#8" - } else if data.starts(with: [0x30, 0x82]) { - print("🔐 Format: PKCS#1 (BEGIN RSA PRIVATE KEY)") - return "PKCS#1" - } else { - print("❌ Unknown key format") - return "Unknown" - } - } - - private func getPrivateKeyCipher() { - // Request PrivateKey chiper to Server - NextcloudKit.shared.getE2EEPrivateKey(account: session.account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.session.account, - name: "getE2EEPrivateKey") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { account, privateKeyChiper, _, error in - if error == .success { - // request Passphrase - var passphraseTextField: UITextField? - let alertController = UIAlertController(title: NSLocalizedString("_e2e_passphrase_request_title_", comment: ""), message: NSLocalizedString("_e2e_passphrase_request_message_", comment: ""), preferredStyle: .alert) - let ok = UIAlertAction(title: "OK", style: .default, handler: { _ in - let passphrase = passphraseTextField?.text ?? "" - if let privateKeyData = NCEndToEndEncryption.shared().decryptPrivateKey(privateKeyChiper, passphrase: passphrase), - let keyData = Data(base64Encoded: privateKeyData), - let privateKey = String(data: keyData, encoding: .utf8) { - NCPreferences().setEndToEndPrivateKey(account: account, privateKey: privateKey) - } else { - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "E2E decrypt privateKey: serious internal error to decrypt Private Key") - } - return - } - // Save to keychain - NCPreferences().setEndToEndPassphrase(account: account, passphrase: passphrase) - // request server publicKey - NextcloudKit.shared.getE2EEPublicKey(account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.session.account, - name: "getE2EEPublicKey") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { account, publicKey, _, error in - if error == .success, let publicKey { - - // Verify Certificate - var verifyCertificate: Bool = false - if let certificate = NCPreferences().getEndToEndCertificate(account: account) { - verifyCertificate = NCEndToEndEncryption.shared().verifyCertificate(certificate, publicKey: publicKey) - } - if verifyCertificate == false { - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "E2E verify certificate server: serious internal error to verify certificate") - } - return - } - - NCPreferences().setEndToEndPublicKey(account: account, publicKey: publicKey) - NCManageDatabase.shared.clearTablesE2EE(account: account) - - self.delegate?.endToEndInitializeSuccess(metadata: self.metadata) - - } else if error != .success { - Task { - switch error.errorCode { - case NCGlobal.shared.errorBadRequest: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E Server publicKey: bad request: internal error") - case NCGlobal.shared.errorResourceNotFound: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E Server publicKey: server public key doesn't exist") - case NCGlobal.shared.errorConflict: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E Server publicKey: forbidden, the user can't access the Server public key") - default: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E Server publicKey: \(error.errorDescription)") - } - } - } - } - }) - - let cancel = UIAlertAction(title: "Cancel", style: .cancel) - alertController.addAction(ok) - alertController.addAction(cancel) - alertController.addTextField { textField in - passphraseTextField = textField - passphraseTextField?.placeholder = NSLocalizedString("_enter_passphrase_", comment: "") - } - - self.controller?.present(alertController, animated: true) - } else if error != .success { - switch error.errorCode { - case NCGlobal.shared.errorBadRequest: - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "E2E get privateKey: bad request, internal error") - } - case NCGlobal.shared.errorResourceNotFound: - // message - guard let e2ePassphrase = NYMnemonic.generateString(128, language: "english") else { return } - let message = "\n" + NSLocalizedString("_e2e_settings_view_passphrase_", comment: "") + "\n\n" + e2ePassphrase - let alertController = UIAlertController(title: NSLocalizedString("_e2e_settings_title_", comment: ""), message: NSLocalizedString(message, comment: ""), preferredStyle: .alert) - let OKAction = UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default) { _ in - self.createNewE2EE(e2ePassphrase: e2ePassphrase, error: error, copyPassphrase: false) - } - let copyAction = UIAlertAction(title: NSLocalizedString("_ok_copy_passphrase_", comment: ""), style: .default) { _ in - self.createNewE2EE(e2ePassphrase: e2ePassphrase, error: error, copyPassphrase: true) - } - - alertController.addAction(OKAction) - alertController.addAction(copyAction) - - self.controller?.present(alertController, animated: true) - case NCGlobal.shared.errorConflict: - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "E2E get privateKey: forbidden, the user can't access the private key") - } - default: - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "E2E get privateKey: \(error.errorDescription)") - } - } - } - } - } - - private func createNewE2EE(e2ePassphrase: String, error: NKError, copyPassphrase: Bool) { - var privateKeyString: NSString? - guard let privateKeyCipher = NCEndToEndEncryption.shared().encryptPrivateKey(session.userId, directory: utilityFileSystem.directoryUserData, passphrase: e2ePassphrase, privateKey: &privateKeyString) else { - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "E2E privateKey: error creating private key cipher") - } - return - } - - // privateKeyChiper - print(privateKeyCipher) - - NextcloudKit.shared.storeE2EEPrivateKey(privateKey: privateKeyCipher, account: session.account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.session.account, - name: "storeE2EEPrivateKey") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { account, _, _, error in - if error == .success, let privateKey = privateKeyString { - - NCPreferences().setEndToEndPrivateKey(account: account, privateKey: String(privateKey)) - NCPreferences().setEndToEndPassphrase(account: account, passphrase: e2ePassphrase) - - // request server publicKey - NextcloudKit.shared.getE2EEPublicKey(account: account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.session.account, - name: "getE2EEPublicKey") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { account, publicKey, _, error in - if error == .success, let publicKey { - - var verifyCertificate: Bool = false - if let certificate = NCPreferences().getEndToEndCertificate(account: account) { - verifyCertificate = NCEndToEndEncryption.shared().verifyCertificate(certificate, publicKey: publicKey) - } - if verifyCertificate == false { - Task { - await showErrorBanner(windowScene: self.windowScene, - text: "E2E verify certificate server: serious internal error to verify certificate") - } - return - } - - NCPreferences().setEndToEndPublicKey(account: account, publicKey: publicKey) - NCManageDatabase.shared.clearTablesE2EE(account: account) - - if copyPassphrase { - UIPasteboard.general.string = e2ePassphrase - } - self.delegate?.endToEndInitializeSuccess(metadata: self.metadata) - } else if error != .success { - Task { - switch error.errorCode { - case NCGlobal.shared.errorBadRequest: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E Server publicKey: bad request, internal error") - case NCGlobal.shared.errorResourceNotFound: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E Server publicKey: server public key doesn't exist") - case NCGlobal.shared.errorConflict: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E Server publicKey: forbidden, the user can't access the Server public key",) - default: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E Server publicKey: \(error.errorDescription)") - } - } - } - } - } else if error != .success { - Task { - switch error.errorCode { - case NCGlobal.shared.errorBadRequest: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E store privateKey: bad request, internal error") - case NCGlobal.shared.errorConflict: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E store privateKey: conflict, a private key for the user already exists") - default: - await showErrorBanner(windowScene: self.windowScene, - text: "E2E store privateKey: \(error.errorDescription)") - } - } - } - } - } -} diff --git a/iOSClient/Settings/E2EE/NCEndToEndSetup.swift b/iOSClient/Settings/E2EE/NCEndToEndSetup.swift new file mode 100644 index 0000000000..58ca9b9718 --- /dev/null +++ b/iOSClient/Settings/E2EE/NCEndToEndSetup.swift @@ -0,0 +1,394 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2017-2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit + +/// Coordinates the full End-to-End Encryption (E2EE) initialization flow. +/// +/// Responsibilities: +/// - Ensures the user certificate exists (or creates/signs it if missing) +/// - Retrieves or creates the encrypted private key +/// - Handles user passphrase input when required +/// - Decrypts and stores the private key locally +/// - Fetches and verifies the server public key +/// - Finalizes the E2EE setup and clears local metadata tables +/// +/// Notes: +/// - This class runs entirely on the MainActor due to UI interactions (alerts) +/// - Networking is performed via NextcloudKit async APIs +/// - Errors are propagated using `throws` and must be handled by the caller +@MainActor +class NCEndToEndSetup { + let utilityFileSystem = NCUtilityFileSystem() + let global = NCGlobal.shared + var extractedPublicKey: String? + var controller: NCMainTabBarController? + + var session: NCSession.Session { + NCSession.shared.getSession(controller: controller) + } + + var windowScene: UIWindowScene? { + SceneManager.shared.getWindowScene(controller: controller) + } + + enum PassphraseChoice { + case ok(passphrase: String) + case copy(passphrase: String) + } + + init(controller: NCMainTabBarController?) { + self.controller = controller + // Clear all keys + NCPreferences().clearAllKeysEndToEnd(account: session.account) + } + + /// Starts the E2EE initialization pipeline. + /// + /// Flow: + /// 1. Ensure a valid certificate exists (fetch or create/sign) + /// 2. Ensure a valid private key exists (fetch or create) + /// + /// - Throws: `NKError` if any step fails (network, crypto, validation, or user cancellation) + func start() async throws { + try await getPublicKey() + try await getPrivateKey() + } + + /// Ensures that a valid user certificate is available. + /// + /// Behavior: + /// - If the certificate exists, it is validated and stored locally + /// - If missing, a CSR is generated and sent to the server for signing + /// - The returned certificate is verified against the locally generated public key + /// + /// - Throws: + /// - `NKError` if CSR generation fails + /// - `NKError` if certificate is missing or invalid + /// - Server errors propagated from NextcloudKit + private func getPublicKey() async throws { + let results = await NextcloudKit.shared.getE2EECertificateAsync(account: session.account) + + switch results.error.errorCode { + case .zero: + guard let certificate = results.certificate else { + throw NKError(errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_get_certificate_", comment: "")) + } + NCPreferences().setEndToEndCertificate(account: self.session.account, certificate: certificate) + self.extractedPublicKey = NCEndToEndEncryption.shared().extractPublicKey(fromCertificate: certificate) + + case NCGlobal.shared.errorResourceNotFound: + // Create CSR + guard let csr = NCEndToEndEncryption.shared().createCSR(self.session.userId, directory: self.utilityFileSystem.directoryUserData) else { + throw NKError(errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_create_csr_", comment: "")) + } + + // Get certificate from server + let results = await NextcloudKit.shared.signE2EECertificateAsync(certificate: csr, account: self.session.account) + guard results.error == .success, + let certificate = results.certificate + else { + throw results.error == .success + ? NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_sign_certificate_", comment: "") + ) + : results.error + } + + // Verify PublicKey + let extractedPublicKey = NCEndToEndEncryption.shared().extractPublicKey(fromCertificate: certificate) + guard extractedPublicKey == NCEndToEndEncryption.shared().generatedPublicKey else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_extract_publickey_", comment: "") + ) + } + NCPreferences().setEndToEndCertificate(account: self.session.account, certificate: certificate) + + default: + throw results.error + } + } + + /// Ensures that a valid private key is available and usable. + /// + /// Behavior: + /// - If the encrypted private key exists: + /// - Prompts the user for passphrase + /// - Decrypts and stores the private key locally + /// - If missing: + /// - Generates a new passphrase (user-confirmed) + /// - Creates and uploads a new encrypted private key + /// + /// After success: + /// - Fetches the server public key + /// - Verifies certificate consistency + /// - Clears E2EE database tables + /// + /// - Throws: + /// - `NKError` for decryption failures + /// - `NKError` for missing data + /// - `NSUserCancelledError` if user cancels input + /// - Server errors propagated from NextcloudKit + private func getPrivateKey() async throws { + let results = await NextcloudKit.shared.getE2EEPrivateKeyAsync(account: self.session.account) + + switch results.error.errorCode { + case .zero: + guard let privateKeyCipher = results.privateKey else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_get_privatekey_", comment: "") + ) + } + + let passphrase = try await requestPassphraseAsync() + + guard let privateKeyData = NCEndToEndEncryption.shared().decryptPrivateKey(privateKeyCipher, passphrase: passphrase), + let keyData = Data(base64Encoded: privateKeyData), + let privateKey = String(data: keyData, encoding: .utf8) + else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_passphrase_error_", comment: "") + ) + } + + // Save + NCPreferences().setEndToEndPrivateKey(account: session.account, privateKey: privateKey) + NCPreferences().setEndToEndPassphrase(account: session.account, passphrase: passphrase) + + let results = await NextcloudKit.shared.getE2EEPublicKeyAsync(account: self.session.account) + guard results.error == .success, + let publicKey = results.publicKey + else { + throw results.error == .success + ? NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_get_publickey_", comment: "") + ) + : results.error + } + + try verifyPublicKey(publicKey) + + NCPreferences().setEndToEndPublicKey(account: self.session.account, publicKey: publicKey) + NCManageDatabase.shared.clearTablesE2EE(account: self.session.account) + + case NCGlobal.shared.errorResourceNotFound: + let choice = try await requestNewPassphraseAsync() + + switch choice { + case .ok(let passphrase): + try await createNewE2EE(e2ePassphrase: passphrase, copyPassphrase: false) + + case .copy(let passphrase): + try await createNewE2EE(e2ePassphrase: passphrase, copyPassphrase: true) + } + default: + throw results.error + } + } + + /// Creates and stores a new E2EE private key. + /// + /// Steps: + /// 1. Generates a new encrypted private key using the provided passphrase + /// 2. Uploads the encrypted key (cipher) to the server + /// 3. Stores the plaintext private key and passphrase locally + /// 4. Fetches and verifies the server public key + /// 5. Finalizes E2EE setup (clears metadata tables) + /// + /// - Parameters: + /// - e2ePassphrase: User-generated passphrase + /// - copyPassphrase: Whether to copy the passphrase to the pasteboard + /// + /// - Throws: + /// - `NKError` if encryption fails + /// - `NKError` if server responses are invalid + /// - Server errors propagated from NextcloudKit + private func createNewE2EE(e2ePassphrase: String, copyPassphrase: Bool) async throws { + var privateKeyString: NSString? + + guard let privateKeyCipher = NCEndToEndEncryption.shared().encryptPrivateKey( + session.userId, + directory: utilityFileSystem.directoryUserData, + passphrase: e2ePassphrase, + privateKey: &privateKeyString + ) else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_encript_privatekey_", comment: "") + ) + } + + // Store cipher on server + + let storeResults = await NextcloudKit.shared.storeE2EEPrivateKeyAsync( + privateKey: privateKeyCipher, + account: session.account + ) + + switch storeResults.error.errorCode { + case .zero: + + guard let privateKeyString else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_store_privatekey_", comment: "") + ) + } + + let privateKey = String(privateKeyString) + + // Save locally + NCPreferences().setEndToEndPrivateKey(account: session.account, privateKey: privateKey) + NCPreferences().setEndToEndPassphrase(account: session.account, passphrase: e2ePassphrase) + + // Fetch server public key + + let publicKeyResults = await NextcloudKit.shared.getE2EEPublicKeyAsync(account: session.account) + + guard publicKeyResults.error == .success, + let publicKey = publicKeyResults.publicKey + else { + throw publicKeyResults.error == .success + ? NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_get_publickey_", comment: "") + ) + : publicKeyResults.error + } + + // Verify + + try verifyPublicKey(publicKey) + + // Finalize + + NCPreferences().setEndToEndPublicKey(account: session.account, publicKey: publicKey) + NCManageDatabase.shared.clearTablesE2EE(account: session.account) + + if copyPassphrase { + UIPasteboard.general.string = e2ePassphrase + } + + default: + throw storeResults.error + } + } + + /// Verifies that the server public key matches the locally stored certificate. + /// + /// - Parameter publicKey: Public key retrieved from the server + /// + /// - Throws: + /// - `NKError` if certificate is missing or validation fails + private func verifyPublicKey(_ publicKey: String) throws { + guard let certificate = NCPreferences().getEndToEndCertificate(account: session.account), + NCEndToEndEncryption.shared().verifyCertificate(certificate, publicKey: publicKey) + else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_verify_publickey_", comment: "") + ) + } + } + + /// Presents a secure alert asking the user for the E2EE passphrase. + /// + /// - Returns: The user-entered passphrase + /// + /// - Throws: + /// - `NKError` with `NSUserCancelledError` if the user cancels the dialog + /// + /// - Note: + /// - Always executed on MainActor due to UIKit usage + private func requestPassphraseAsync() async throws -> String { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let alertController = UIAlertController( + title: NSLocalizedString("_e2e_passphrase_request_title_", comment: ""), + message: NSLocalizedString("_e2e_passphrase_request_message_", comment: ""), + preferredStyle: .alert + ) + + var passphraseTextField: UITextField? + + let ok = UIAlertAction(title: "OK", style: .default) { _ in + let passphrase = passphraseTextField?.text ?? "" + continuation.resume(returning: passphrase) + } + + let cancel = UIAlertAction(title: "Cancel", style: .cancel) { _ in + continuation.resume(throwing: NKError( + errorCode: NSUserCancelledError, + errorDescription: "User cancelled" + )) + } + + alertController.addAction(ok) + alertController.addAction(cancel) + + alertController.addTextField { textField in + passphraseTextField = textField + textField.placeholder = NSLocalizedString("_enter_passphrase_", comment: "") + textField.isSecureTextEntry = true + } + + self.controller?.present(alertController, animated: true) + } + } + + /// Generates and presents a new passphrase to the user. + /// + /// The user can: + /// - Accept the passphrase + /// - Accept and copy it to clipboard + /// + /// - Returns: `PassphraseChoice` indicating user action and passphrase + /// + /// - Throws: + /// - `NKError` if passphrase generation fails + /// + /// - Note: + /// - Always executed on MainActor due to UIKit usage + func requestNewPassphraseAsync() async throws -> PassphraseChoice { + guard let e2ePassphrase = NYMnemonic.generateString(128, language: "english") else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("_e2ee_setup_generate_passphrase_", comment: "") + ) + } + + let message = "\n" + + NSLocalizedString("_e2e_settings_view_passphrase_", comment: "") + + "\n\n" + e2ePassphrase + + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + + let alertController = UIAlertController( + title: NSLocalizedString("_e2e_settings_title_", comment: ""), + message: message, + preferredStyle: .alert + ) + + let okAction = UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default) { _ in + continuation.resume(returning: .ok(passphrase: e2ePassphrase)) + } + + let copyAction = UIAlertAction(title: NSLocalizedString("_ok_copy_passphrase_", comment: ""), style: .default) { _ in + continuation.resume(returning: .copy(passphrase: e2ePassphrase)) + } + + alertController.addAction(okAction) + alertController.addAction(copyAction) + + self.controller?.present(alertController, animated: true) + } + } +} diff --git a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift index f26f5bc9bd..06dcee000a 100644 --- a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift +++ b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift @@ -8,8 +8,7 @@ import NextcloudKit import LocalAuthentication @MainActor -class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, NCEndToEndInitializeDelegate, TOPasscodeViewControllerDelegate { - let endToEndInitialize = NCEndToEndInitialize() +class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscodeViewControllerDelegate { var passcodeType = "" @Published var controller: NCMainTabBarController? @@ -32,7 +31,6 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, NCEndToEnd init(controller: NCMainTabBarController?) { super.init() self.controller = controller - endToEndInitialize.delegate = self onViewAppear() } @@ -43,7 +41,8 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, NCEndToEnd if isEndToEndEnabled { statusOfService = NSLocalizedString("_status_e2ee_configured_", comment: "") } else { - endToEndInitialize.statusOfService(session: session) { error in + NextcloudKit.shared.getE2EECertificate(account: session.account) { _ in + } completion: { _, _, _, _, error in if error == .success { self.statusOfService = NSLocalizedString("_status_e2ee_on_server_", comment: "") } else { @@ -69,8 +68,7 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, NCEndToEnd self.passcodeType = passcodeType correctPasscode() return - #endif - + #else let laContext = LAContext() var error: NSError? let passcodeViewController = TOPasscodeViewController(passcodeType: .sixDigits, allowCancel: true) @@ -92,12 +90,33 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, NCEndToEnd self.passcodeType = passcodeType controller?.present(passcodeViewController, animated: true) + #endif } @objc func correctPasscode() { switch self.passcodeType { case "startE2E": - endToEndInitialize.initEndToEndEncryption(controller: controller, metadata: nil) + Task { + do { + let e2ee = NCEndToEndSetup(controller: controller) + try await e2ee.start() + isEndToEndEnabled = true + } catch let error as NKError { + if error.errorCode == NSUserCancelledError { + return + } + await showErrorBanner( + windowScene: windowScene, + text: error.errorDescription + ) + } catch { + // fallback (non NKError) + await showErrorBanner( + windowScene: windowScene, + text: error.localizedDescription + ) + } + } case "readPassphrase": if let e2ePassphrase = NCPreferences().getEndToEndPassphrase(account: session.account) { print("[INFO]Passphrase: " + e2ePassphrase) diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 3716a040e4..33b9cc35c7 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -790,3 +790,14 @@ You can stop it at any time, adjust the settings, and enable it again."; "_e2ee_create_folder_" = "End-to-end encryption directory creation in progress, please wait …"; "_e2ee_rename_file_" = "End-to-end encryption rename in progress, please wait …"; "_e2ee_encrypt_folder_" = "End-to-end encryption folder in progress, please wait …"; +"_e2ee_setup_get_certificate_" = "Server did not return a certificate"; +"_e2ee_setup_create_csr_" = "Unable to generate certificate signing request (CSR)"; +"_e2ee_setup_sign_certificate_" = "Server did not return a certificate after signing request"; +"_e2ee_setup_extract_publickey_" = "Public key mismatch between generated key and certificate"; +"_e2ee_setup_get_privatekey_" = "Server did not return an encrypted private key"; +"_e2ee_setup_passphrase_error_" = "The provided passphrase is incorrect"; +"_e2ee_setup_get_publickey_" = "Server did not return an encrypted public key"; +"_e2ee_setup_encript_privatekey_" = "Unable to encrypt the private key"; +"_e2ee_setup_store_privatekey_" = "Private key generation returned no usable key"; +"_e2ee_setup_verify_publickey_" = "Public key does not match the certificate"; +"_e2ee_setup_generate_passphrase_" = "Unable to generate a passphrase";