From 31b90f2f4f3f59b12607551c4ce9deb97ee4babd Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 2 Apr 2026 11:10:03 +0200 Subject: [PATCH 1/8] code Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 4 + iOSClient/Settings/E2EE/NCEndToEndInit.swift | 242 +++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 iOSClient/Settings/E2EE/NCEndToEndInit.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 239704ee85..e21d616d10 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 /* NCEndToEndInit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71070AA2F7E49E100AEE58A /* NCEndToEndInit.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 */; }; @@ -1325,6 +1326,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 /* NCEndToEndInit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndInit.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 = ""; }; @@ -2620,6 +2622,7 @@ isa = PBXGroup; children = ( F768820F2C0DD1E7001CF441 /* NCEndToEndInitialize.swift */, + F71070AA2F7E49E100AEE58A /* NCEndToEndInit.swift */, F76882102C0DD1E7001CF441 /* NCManageE2EEModel.swift */, F768820E2C0DD1E7001CF441 /* NCManageE2EEView.swift */, ); @@ -4665,6 +4668,7 @@ F7A03E352D427312007AA677 /* NCMainNavigationController.swift in Sources */, F769CA192966EA3C00039397 /* ComponentView.swift in Sources */, F7C9B91D2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */, + F71070AB2F7E49F200AEE58A /* NCEndToEndInit.swift in Sources */, AF93474C27E34120002537EE /* NCUtility+Image.swift in Sources */, F702F30125EE5D2C008F8E80 /* NYMnemonic.m in Sources */, AF93474E27E3F212002537EE /* NCShareNewUserAddComment.swift in Sources */, diff --git a/iOSClient/Settings/E2EE/NCEndToEndInit.swift b/iOSClient/Settings/E2EE/NCEndToEndInit.swift new file mode 100644 index 0000000000..7e2d57f52e --- /dev/null +++ b/iOSClient/Settings/E2EE/NCEndToEndInit.swift @@ -0,0 +1,242 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2017-2026 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit + +@MainActor +class NCEndToEndInit: NSObject { + let utilityFileSystem = NCUtilityFileSystem() + let global = NCGlobal.shared + 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) + } + + init(controller: NCMainTabBarController?, metadata: tableMetadata?) { + super.init() + + self.controller = controller + self.metadata = metadata + + // Clear all keys + NCPreferences().clearAllKeysEndToEnd(account: session.account) + } + + func start() async throws { + try await getPublicKey() + try await getPrivateKey() + } + + func statusOfService(session: NCSession.Session) async -> NKError { + let results = await NextcloudKit.shared.getE2EECertificateAsync(account: session.account) + return results.error + } + + 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("E2E get publicKey - Bad request: internal error", 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("Error creating 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: "certificate absent" + ) + : results.error + } + + // Verify PublicKey + let extractedPublicKey = NCEndToEndEncryption.shared().extractPublicKey(fromCertificate: certificate) + guard extractedPublicKey == NCEndToEndEncryption.shared().generatedPublicKey else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: NSLocalizedString("E2E sign publicKey: the public key is incorrect", comment: "") + ) + } + NCPreferences().setEndToEndCertificate(account: self.session.account, certificate: certificate) + + default: + throw results.error + } + } + + private func getPrivateKey() async throws { + let results = await NextcloudKit.shared.getE2EEPrivateKeyAsync(account: self.session.account) + switch results.error.errorCode { + case .zero: + guard let privateKeyChiper = results.privateKey else { + throw NKError(errorCode: global.errorInternalError, + errorDescription: "PrivateKey absent" + ) + } + // request Passphrase + let passphrase = try await requestPassphraseAsync() + + guard let privateKeyData = NCEndToEndEncryption.shared().decryptPrivateKey(privateKeyChiper, passphrase: passphrase), + let keyData = Data(base64Encoded: privateKeyData), + let privateKey = String(data: keyData, encoding: .utf8) + else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: "E2E decrypt privateKey failed" + ) + } + + // Save + NCPreferences().setEndToEndPrivateKey(account: session.account, privateKey: privateKey) + NCPreferences().setEndToEndPassphrase(account: session.account, passphrase: passphrase) + + // Verify Certificate + 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: "PublicKey absent" + ) + : results.error + } + + var verifyCertificate: Bool = false + if let certificate = NCPreferences().getEndToEndCertificate(account: self.session.account) { + verifyCertificate = NCEndToEndEncryption.shared().verifyCertificate(certificate, publicKey: publicKey) + } + if !verifyCertificate { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: "verify PublicKey error" + ) + } + + NCPreferences().setEndToEndPublicKey(account: self.session.account, publicKey: publicKey) + NCManageDatabase.shared.clearTablesE2EE(account: self.session.account) + case NCGlobal.shared.errorResourceNotFound: + break + default: + throw results.error + } + } + + private func createNewE2EE(e2ePassphrase: String, error: NKError, copyPassphrase: Bool) async throws { + var privateKeyString: NSString? + + guard let privateKey = NCEndToEndEncryption.shared().encryptPrivateKey(session.userId, + directory: utilityFileSystem.directoryUserData, + passphrase: e2ePassphrase, + privateKey: &privateKeyString) else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: "error creating private key cipher" + ) + } + + let results = await NextcloudKit.shared.storeE2EEPrivateKeyAsync(privateKey: privateKey, account: self.session.account) + switch results.error.errorCode { + case .zero: + guard let privateKey = results.privateKey else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: "privateKey absent" + ) + } + + NCPreferences().setEndToEndPrivateKey(account: self.session.account, privateKey: String(privateKey)) + NCPreferences().setEndToEndPassphrase(account: self.session.account, passphrase: e2ePassphrase) + + let results = await NextcloudKit.shared.getE2EEPublicKeyAsync(account: session.account) + guard let publicKey = results.publicKey else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: "publicKey absent" + ) + } + + var verifyCertificate: Bool = false + if let certificate = NCPreferences().getEndToEndCertificate(account: self.session.account) { + verifyCertificate = NCEndToEndEncryption.shared().verifyCertificate(certificate, publicKey: publicKey) + } + if !verifyCertificate { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: "verify PublicKey error" + ) + } + + NCPreferences().setEndToEndPublicKey(account: session.account, publicKey: publicKey) + NCManageDatabase.shared.clearTablesE2EE(account: session.account) + + if copyPassphrase { + UIPasteboard.general.string = e2ePassphrase + } + + default: + throw results.error + } + } + + @MainActor + 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) + } + } +} From 87cd1e83723fba949b1831c39b3fb52e02f5f3cb Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 2 Apr 2026 11:32:30 +0200 Subject: [PATCH 2/8] code Signed-off-by: Marino Faggiana --- iOSClient/Settings/E2EE/NCEndToEndInit.swift | 91 ++++++++++++++----- .../Settings/E2EE/NCEndToEndInitialize.swift | 6 +- 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/iOSClient/Settings/E2EE/NCEndToEndInit.swift b/iOSClient/Settings/E2EE/NCEndToEndInit.swift index 7e2d57f52e..e60dc038f9 100644 --- a/iOSClient/Settings/E2EE/NCEndToEndInit.swift +++ b/iOSClient/Settings/E2EE/NCEndToEndInit.swift @@ -21,6 +21,11 @@ class NCEndToEndInit: NSObject { SceneManager.shared.getWindowScene(controller: controller) } + enum PassphraseChoice { + case ok(passphrase: String) + case copy(passphrase: String) + } + init(controller: NCMainTabBarController?, metadata: tableMetadata?) { super.init() @@ -114,7 +119,6 @@ class NCEndToEndInit: NSObject { NCPreferences().setEndToEndPrivateKey(account: session.account, privateKey: privateKey) NCPreferences().setEndToEndPassphrase(account: session.account, passphrase: passphrase) - // Verify Certificate let results = await NextcloudKit.shared.getE2EEPublicKeyAsync(account: self.session.account) guard results.error == .success, let publicKey = results.publicKey @@ -127,27 +131,27 @@ class NCEndToEndInit: NSObject { : results.error } - var verifyCertificate: Bool = false - if let certificate = NCPreferences().getEndToEndCertificate(account: self.session.account) { - verifyCertificate = NCEndToEndEncryption.shared().verifyCertificate(certificate, publicKey: publicKey) - } - if !verifyCertificate { - throw NKError( - errorCode: global.errorInternalError, - errorDescription: "verify PublicKey error" - ) - } + try await verifyPublicKey(publicKey) NCPreferences().setEndToEndPublicKey(account: self.session.account, publicKey: publicKey) NCManageDatabase.shared.clearTablesE2EE(account: self.session.account) + case NCGlobal.shared.errorResourceNotFound: - break + 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 } } - private func createNewE2EE(e2ePassphrase: String, error: NKError, copyPassphrase: Bool) async throws { + private func createNewE2EE(e2ePassphrase: String, copyPassphrase: Bool) async throws { var privateKeyString: NSString? guard let privateKey = NCEndToEndEncryption.shared().encryptPrivateKey(session.userId, @@ -181,16 +185,7 @@ class NCEndToEndInit: NSObject { ) } - var verifyCertificate: Bool = false - if let certificate = NCPreferences().getEndToEndCertificate(account: self.session.account) { - verifyCertificate = NCEndToEndEncryption.shared().verifyCertificate(certificate, publicKey: publicKey) - } - if !verifyCertificate { - throw NKError( - errorCode: global.errorInternalError, - errorDescription: "verify PublicKey error" - ) - } + try await verifyPublicKey(publicKey) NCPreferences().setEndToEndPublicKey(account: session.account, publicKey: publicKey) NCManageDatabase.shared.clearTablesE2EE(account: session.account) @@ -204,6 +199,19 @@ class NCEndToEndInit: NSObject { } } + private func verifyPublicKey(_ publicKey: String) async throws { + var verifyCertificate: Bool = false + if let certificate = NCPreferences().getEndToEndCertificate(account: self.session.account) { + verifyCertificate = NCEndToEndEncryption.shared().verifyCertificate(certificate, publicKey: publicKey) + } + if !verifyCertificate { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: "verify PublicKey error" + ) + } + } + @MainActor private func requestPassphraseAsync() async throws -> String { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in @@ -239,4 +247,41 @@ class NCEndToEndInit: NSObject { self.controller?.present(alertController, animated: true) } } + + @MainActor + func requestNewPassphraseAsync() async throws -> PassphraseChoice { + + guard let e2ePassphrase = NYMnemonic.generateString(128, language: "english") else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: "Failed to generate passphrase" + ) + } + + 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/NCEndToEndInitialize.swift b/iOSClient/Settings/E2EE/NCEndToEndInitialize.swift index 874c0708f6..e8cbd7065e 100644 --- a/iOSClient/Settings/E2EE/NCEndToEndInitialize.swift +++ b/iOSClient/Settings/E2EE/NCEndToEndInitialize.swift @@ -248,10 +248,10 @@ class NCEndToEndInitialize: NSObject { 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) + self.createNewE2EE(e2ePassphrase: e2ePassphrase, copyPassphrase: false) } let copyAction = UIAlertAction(title: NSLocalizedString("_ok_copy_passphrase_", comment: ""), style: .default) { _ in - self.createNewE2EE(e2ePassphrase: e2ePassphrase, error: error, copyPassphrase: true) + self.createNewE2EE(e2ePassphrase: e2ePassphrase, copyPassphrase: true) } alertController.addAction(OKAction) @@ -273,7 +273,7 @@ class NCEndToEndInitialize: NSObject { } } - private func createNewE2EE(e2ePassphrase: String, error: NKError, copyPassphrase: Bool) { + private func createNewE2EE(e2ePassphrase: String, copyPassphrase: Bool) { var privateKeyString: NSString? guard let privateKeyCipher = NCEndToEndEncryption.shared().encryptPrivateKey(session.userId, directory: utilityFileSystem.directoryUserData, passphrase: e2ePassphrase, privateKey: &privateKeyString) else { Task { From c79d7b19951bf275bc30c994b793811d9fcefff2 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 2 Apr 2026 11:54:10 +0200 Subject: [PATCH 3/8] doc Signed-off-by: Marino Faggiana --- iOSClient/Settings/E2EE/NCEndToEndInit.swift | 187 +++++++++++++++---- 1 file changed, 152 insertions(+), 35 deletions(-) diff --git a/iOSClient/Settings/E2EE/NCEndToEndInit.swift b/iOSClient/Settings/E2EE/NCEndToEndInit.swift index e60dc038f9..8e6d97154f 100644 --- a/iOSClient/Settings/E2EE/NCEndToEndInit.swift +++ b/iOSClient/Settings/E2EE/NCEndToEndInit.swift @@ -5,6 +5,20 @@ 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 NCEndToEndInit: NSObject { let utilityFileSystem = NCUtilityFileSystem() @@ -36,6 +50,13 @@ class NCEndToEndInit: NSObject { 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() @@ -46,6 +67,17 @@ class NCEndToEndInit: NSObject { return results.error } + /// 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) @@ -93,25 +125,46 @@ class NCEndToEndInit: NSObject { } } + /// 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 privateKeyChiper = results.privateKey else { - throw NKError(errorCode: global.errorInternalError, - errorDescription: "PrivateKey absent" + guard let privateKeyCipher = results.privateKey else { + throw NKError( + errorCode: global.errorInternalError, + errorDescription: "Private key cipher absent" ) } - // request Passphrase + let passphrase = try await requestPassphraseAsync() - guard let privateKeyData = NCEndToEndEncryption.shared().decryptPrivateKey(privateKeyChiper, passphrase: passphrase), + 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: "E2E decrypt privateKey failed" + errorDescription: "Decrypt private key failed" ) } @@ -131,7 +184,7 @@ class NCEndToEndInit: NSObject { : results.error } - try await verifyPublicKey(publicKey) + try verifyPublicKey(publicKey) NCPreferences().setEndToEndPublicKey(account: self.session.account, publicKey: publicKey) NCManageDatabase.shared.clearTablesE2EE(account: self.session.account) @@ -151,41 +204,81 @@ class NCEndToEndInit: NSObject { } } + /// 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 privateKey = NCEndToEndEncryption.shared().encryptPrivateKey(session.userId, - directory: utilityFileSystem.directoryUserData, - passphrase: e2ePassphrase, - privateKey: &privateKeyString) else { + guard let privateKeyCipher = NCEndToEndEncryption.shared().encryptPrivateKey( + session.userId, + directory: utilityFileSystem.directoryUserData, + passphrase: e2ePassphrase, + privateKey: &privateKeyString + ) else { throw NKError( errorCode: global.errorInternalError, - errorDescription: "error creating private key cipher" + errorDescription: "Error creating private key cipher" ) } - let results = await NextcloudKit.shared.storeE2EEPrivateKeyAsync(privateKey: privateKey, account: self.session.account) - switch results.error.errorCode { + // Store cipher on server + + let storeResults = await NextcloudKit.shared.storeE2EEPrivateKeyAsync( + privateKey: privateKeyCipher, + account: session.account + ) + + switch storeResults.error.errorCode { case .zero: - guard let privateKey = results.privateKey else { + + guard let privateKeyString else { throw NKError( errorCode: global.errorInternalError, - errorDescription: "privateKey absent" + errorDescription: "Private key (plaintext) missing" ) } - NCPreferences().setEndToEndPrivateKey(account: self.session.account, privateKey: String(privateKey)) - NCPreferences().setEndToEndPassphrase(account: self.session.account, passphrase: e2ePassphrase) + let privateKey = String(privateKeyString) - let results = await NextcloudKit.shared.getE2EEPublicKeyAsync(account: session.account) - guard let publicKey = results.publicKey else { - throw NKError( - errorCode: global.errorInternalError, - errorDescription: "publicKey absent" - ) + // 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: "Public key absent" + ) + : publicKeyResults.error } - try await verifyPublicKey(publicKey) + // Verify + + try verifyPublicKey(publicKey) + + // Finalize NCPreferences().setEndToEndPublicKey(account: session.account, publicKey: publicKey) NCManageDatabase.shared.clearTablesE2EE(account: session.account) @@ -195,24 +288,36 @@ class NCEndToEndInit: NSObject { } default: - throw results.error + throw storeResults.error } } - private func verifyPublicKey(_ publicKey: String) async throws { - var verifyCertificate: Bool = false - if let certificate = NCPreferences().getEndToEndCertificate(account: self.session.account) { - verifyCertificate = NCEndToEndEncryption.shared().verifyCertificate(certificate, publicKey: publicKey) - } - if !verifyCertificate { + /// 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: "verify PublicKey error" + errorDescription: "Verify public key error" ) } } - @MainActor + /// 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( @@ -248,7 +353,19 @@ class NCEndToEndInit: NSObject { } } - @MainActor + /// 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 { From 5843f95c0a4ec1c291b4f81a838ce140d2d2cb41 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 2 Apr 2026 12:15:57 +0200 Subject: [PATCH 4/8] code Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 8 - ...ionViewCommon+CollectionViewDelegate.swift | 2 + ...lectionViewCommon+EndToEndInitialize.swift | 14 - .../Settings/E2EE/NCEndToEndInitialize.swift | 366 ------------------ ...dToEndInit.swift => NCEndToEndSetup.swift} | 14 +- .../Settings/E2EE/NCManageE2EEModel.swift | 25 +- 6 files changed, 15 insertions(+), 414 deletions(-) delete mode 100644 iOSClient/Main/Collection Common/NCCollectionViewCommon+EndToEndInitialize.swift delete mode 100644 iOSClient/Settings/E2EE/NCEndToEndInitialize.swift rename iOSClient/Settings/E2EE/{NCEndToEndInit.swift => NCEndToEndSetup.swift} (97%) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index e21d616d10..2a9ae75cd6 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -496,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 */; }; @@ -657,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 */; }; @@ -1469,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 = ""; }; @@ -1584,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 = ""; }; @@ -2528,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 */, @@ -2621,7 +2616,6 @@ F768820D2C0DD1E7001CF441 /* E2EE */ = { isa = PBXGroup; children = ( - F768820F2C0DD1E7001CF441 /* NCEndToEndInitialize.swift */, F71070AA2F7E49E100AEE58A /* NCEndToEndInit.swift */, F76882102C0DD1E7001CF441 /* NCManageE2EEModel.swift */, F768820E2C0DD1E7001CF441 /* NCManageE2EEView.swift */, @@ -4480,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 */, @@ -4760,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/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift index 2940ace9c0..7fb4387099 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift @@ -13,6 +13,7 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { func didSelectMetadata(_ metadata: tableMetadata, withOcIds: Bool) async { let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) + /* if metadata.e2eEncrypted { if capabilities.e2EEEnabled { if !NCPreferences().isEndToEndEnabled(account: metadata.account) { @@ -26,6 +27,7 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { return } } + */ func downloadFile() async { var downloadRequest: DownloadRequest? 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/Settings/E2EE/NCEndToEndInitialize.swift b/iOSClient/Settings/E2EE/NCEndToEndInitialize.swift deleted file mode 100644 index e8cbd7065e..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, copyPassphrase: false) - } - let copyAction = UIAlertAction(title: NSLocalizedString("_ok_copy_passphrase_", comment: ""), style: .default) { _ in - self.createNewE2EE(e2ePassphrase: e2ePassphrase, 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, 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/NCEndToEndInit.swift b/iOSClient/Settings/E2EE/NCEndToEndSetup.swift similarity index 97% rename from iOSClient/Settings/E2EE/NCEndToEndInit.swift rename to iOSClient/Settings/E2EE/NCEndToEndSetup.swift index 8e6d97154f..9e5f761142 100644 --- a/iOSClient/Settings/E2EE/NCEndToEndInit.swift +++ b/iOSClient/Settings/E2EE/NCEndToEndSetup.swift @@ -20,12 +20,11 @@ import NextcloudKit /// - Networking is performed via NextcloudKit async APIs /// - Errors are propagated using `throws` and must be handled by the caller @MainActor -class NCEndToEndInit: NSObject { +class NCEndToEndInit { let utilityFileSystem = NCUtilityFileSystem() let global = NCGlobal.shared var extractedPublicKey: String? var controller: NCMainTabBarController? - var metadata: tableMetadata? var session: NCSession.Session { NCSession.shared.getSession(controller: controller) @@ -40,12 +39,8 @@ class NCEndToEndInit: NSObject { case copy(passphrase: String) } - init(controller: NCMainTabBarController?, metadata: tableMetadata?) { - super.init() - + init(controller: NCMainTabBarController?) { self.controller = controller - self.metadata = metadata - // Clear all keys NCPreferences().clearAllKeysEndToEnd(account: session.account) } @@ -62,11 +57,6 @@ class NCEndToEndInit: NSObject { try await getPrivateKey() } - func statusOfService(session: NCSession.Session) async -> NKError { - let results = await NextcloudKit.shared.getE2EECertificateAsync(account: session.account) - return results.error - } - /// Ensures that a valid user certificate is available. /// /// Behavior: diff --git a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift index f26f5bc9bd..76c60a1fa8 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,23 +31,18 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, NCEndToEnd init(controller: NCMainTabBarController?) { super.init() self.controller = controller - endToEndInitialize.delegate = self onViewAppear() } /// Triggered when the view appears. func onViewAppear() { if capabilities.e2EEEnabled { - isEndToEndEnabled = NCPreferences().isEndToEndEnabled(account: session.account) - if isEndToEndEnabled { - statusOfService = NSLocalizedString("_status_e2ee_configured_", comment: "") - } else { - endToEndInitialize.statusOfService(session: session) { error in - if error == .success { - self.statusOfService = NSLocalizedString("_status_e2ee_on_server_", comment: "") - } else { - self.statusOfService = NSLocalizedString("_status_e2ee_not_setup_", comment: "") - } + NextcloudKit.shared.getE2EECertificate(account: session.account) { _ in + } completion: { _, _, _, _, error in + if error == .success { + self.statusOfService = NSLocalizedString("_status_e2ee_on_server_", comment: "") + } else { + self.statusOfService = NSLocalizedString("_status_e2ee_not_setup_", comment: "") } } } else { @@ -97,7 +91,10 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, NCEndToEnd @objc func correctPasscode() { switch self.passcodeType { case "startE2E": - endToEndInitialize.initEndToEndEncryption(controller: controller, metadata: nil) + Task { + let e2ee = NCEndToEndInit(controller: controller) + try await e2ee.start() + } case "readPassphrase": if let e2ePassphrase = NCPreferences().getEndToEndPassphrase(account: session.account) { print("[INFO]Passphrase: " + e2ePassphrase) From a7f653f46faf9b30aa42e8dd1ac315c6fb4fbf38 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 2 Apr 2026 12:16:04 +0200 Subject: [PATCH 5/8] code Signed-off-by: Marino Faggiana --- Nextcloud.xcodeproj/project.pbxproj | 8 ++-- iOSClient/Settings/E2EE/NCEndToEndSetup.swift | 2 +- .../Settings/E2EE/NCManageE2EEModel.swift | 42 ++++++++++++++----- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 2a9ae75cd6..d1047da8a1 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -222,7 +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 /* NCEndToEndInit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71070AA2F7E49E100AEE58A /* NCEndToEndInit.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 */; }; @@ -1324,7 +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 /* NCEndToEndInit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCEndToEndInit.swift; 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 = ""; }; @@ -2616,7 +2616,7 @@ F768820D2C0DD1E7001CF441 /* E2EE */ = { isa = PBXGroup; children = ( - F71070AA2F7E49E100AEE58A /* NCEndToEndInit.swift */, + F71070AA2F7E49E100AEE58A /* NCEndToEndSetup.swift */, F76882102C0DD1E7001CF441 /* NCManageE2EEModel.swift */, F768820E2C0DD1E7001CF441 /* NCManageE2EEView.swift */, ); @@ -4661,7 +4661,7 @@ F7A03E352D427312007AA677 /* NCMainNavigationController.swift in Sources */, F769CA192966EA3C00039397 /* ComponentView.swift in Sources */, F7C9B91D2B582F550064EA91 /* NCManageDatabase+SecurityGuard.swift in Sources */, - F71070AB2F7E49F200AEE58A /* NCEndToEndInit.swift in Sources */, + F71070AB2F7E49F200AEE58A /* NCEndToEndSetup.swift in Sources */, AF93474C27E34120002537EE /* NCUtility+Image.swift in Sources */, F702F30125EE5D2C008F8E80 /* NYMnemonic.m in Sources */, AF93474E27E3F212002537EE /* NCShareNewUserAddComment.swift in Sources */, diff --git a/iOSClient/Settings/E2EE/NCEndToEndSetup.swift b/iOSClient/Settings/E2EE/NCEndToEndSetup.swift index 9e5f761142..43dcbd0ae7 100644 --- a/iOSClient/Settings/E2EE/NCEndToEndSetup.swift +++ b/iOSClient/Settings/E2EE/NCEndToEndSetup.swift @@ -20,7 +20,7 @@ import NextcloudKit /// - Networking is performed via NextcloudKit async APIs /// - Errors are propagated using `throws` and must be handled by the caller @MainActor -class NCEndToEndInit { +class NCEndToEndSetup { let utilityFileSystem = NCUtilityFileSystem() let global = NCGlobal.shared var extractedPublicKey: String? diff --git a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift index 76c60a1fa8..af441c9472 100644 --- a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift +++ b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift @@ -37,12 +37,17 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode /// Triggered when the view appears. func onViewAppear() { if capabilities.e2EEEnabled { - NextcloudKit.shared.getE2EECertificate(account: session.account) { _ in - } completion: { _, _, _, _, error in - if error == .success { - self.statusOfService = NSLocalizedString("_status_e2ee_on_server_", comment: "") - } else { - self.statusOfService = NSLocalizedString("_status_e2ee_not_setup_", comment: "") + isEndToEndEnabled = NCPreferences().isEndToEndEnabled(account: session.account) + if isEndToEndEnabled { + statusOfService = NSLocalizedString("_status_e2ee_configured_", comment: "") + } else { + NextcloudKit.shared.getE2EECertificate(account: session.account) { _ in + } completion: { _, _, _, _, error in + if error == .success { + self.statusOfService = NSLocalizedString("_status_e2ee_on_server_", comment: "") + } else { + self.statusOfService = NSLocalizedString("_status_e2ee_not_setup_", comment: "") + } } } } else { @@ -63,8 +68,7 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode self.passcodeType = passcodeType correctPasscode() return - #endif - + #else let laContext = LAContext() var error: NSError? let passcodeViewController = TOPasscodeViewController(passcodeType: .sixDigits, allowCancel: true) @@ -86,14 +90,32 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode self.passcodeType = passcodeType controller?.present(passcodeViewController, animated: true) + #endif } @objc func correctPasscode() { switch self.passcodeType { case "startE2E": Task { - let e2ee = NCEndToEndInit(controller: controller) - try await e2ee.start() + 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 + ) + } catch { + // fallback (non NKError) + await showErrorBanner( + windowScene: windowScene, + text: error.localizedDescription + ) + } } case "readPassphrase": if let e2ePassphrase = NCPreferences().getEndToEndPassphrase(account: session.account) { From b387354496d0718f1f200316192048a7e5032219 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 2 Apr 2026 15:10:51 +0200 Subject: [PATCH 6/8] strings Signed-off-by: Marino Faggiana --- iOSClient/Settings/E2EE/NCEndToEndSetup.swift | 26 +++++++++---------- .../Settings/E2EE/NCManageE2EEModel.swift | 2 +- .../en.lproj/Localizable.strings | 11 ++++++++ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/iOSClient/Settings/E2EE/NCEndToEndSetup.swift b/iOSClient/Settings/E2EE/NCEndToEndSetup.swift index 43dcbd0ae7..58ca9b9718 100644 --- a/iOSClient/Settings/E2EE/NCEndToEndSetup.swift +++ b/iOSClient/Settings/E2EE/NCEndToEndSetup.swift @@ -75,7 +75,7 @@ class NCEndToEndSetup { case .zero: guard let certificate = results.certificate else { throw NKError(errorCode: global.errorInternalError, - errorDescription: NSLocalizedString("E2E get publicKey - Bad request: internal error", comment: "")) + errorDescription: NSLocalizedString("_e2ee_setup_get_certificate_", comment: "")) } NCPreferences().setEndToEndCertificate(account: self.session.account, certificate: certificate) self.extractedPublicKey = NCEndToEndEncryption.shared().extractPublicKey(fromCertificate: certificate) @@ -84,7 +84,7 @@ class NCEndToEndSetup { // Create CSR guard let csr = NCEndToEndEncryption.shared().createCSR(self.session.userId, directory: self.utilityFileSystem.directoryUserData) else { throw NKError(errorCode: global.errorInternalError, - errorDescription: NSLocalizedString("Error creating CSR", comment: "")) + errorDescription: NSLocalizedString("_e2ee_setup_create_csr_", comment: "")) } // Get certificate from server @@ -95,7 +95,7 @@ class NCEndToEndSetup { throw results.error == .success ? NKError( errorCode: global.errorInternalError, - errorDescription: "certificate absent" + errorDescription: NSLocalizedString("_e2ee_setup_sign_certificate_", comment: "") ) : results.error } @@ -105,7 +105,7 @@ class NCEndToEndSetup { guard extractedPublicKey == NCEndToEndEncryption.shared().generatedPublicKey else { throw NKError( errorCode: global.errorInternalError, - errorDescription: NSLocalizedString("E2E sign publicKey: the public key is incorrect", comment: "") + errorDescription: NSLocalizedString("_e2ee_setup_extract_publickey_", comment: "") ) } NCPreferences().setEndToEndCertificate(account: self.session.account, certificate: certificate) @@ -137,12 +137,13 @@ class NCEndToEndSetup { /// - 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: "Private key cipher absent" + errorDescription: NSLocalizedString("_e2ee_setup_get_privatekey_", comment: "") ) } @@ -154,7 +155,7 @@ class NCEndToEndSetup { else { throw NKError( errorCode: global.errorInternalError, - errorDescription: "Decrypt private key failed" + errorDescription: NSLocalizedString("_e2ee_setup_passphrase_error_", comment: "") ) } @@ -169,7 +170,7 @@ class NCEndToEndSetup { throw results.error == .success ? NKError( errorCode: global.errorInternalError, - errorDescription: "PublicKey absent" + errorDescription: NSLocalizedString("_e2ee_setup_get_publickey_", comment: "") ) : results.error } @@ -222,7 +223,7 @@ class NCEndToEndSetup { ) else { throw NKError( errorCode: global.errorInternalError, - errorDescription: "Error creating private key cipher" + errorDescription: NSLocalizedString("_e2ee_setup_encript_privatekey_", comment: "") ) } @@ -239,7 +240,7 @@ class NCEndToEndSetup { guard let privateKeyString else { throw NKError( errorCode: global.errorInternalError, - errorDescription: "Private key (plaintext) missing" + errorDescription: NSLocalizedString("_e2ee_setup_store_privatekey_", comment: "") ) } @@ -259,7 +260,7 @@ class NCEndToEndSetup { throw publicKeyResults.error == .success ? NKError( errorCode: global.errorInternalError, - errorDescription: "Public key absent" + errorDescription: NSLocalizedString("_e2ee_setup_get_publickey_", comment: "") ) : publicKeyResults.error } @@ -294,7 +295,7 @@ class NCEndToEndSetup { else { throw NKError( errorCode: global.errorInternalError, - errorDescription: "Verify public key error" + errorDescription: NSLocalizedString("_e2ee_setup_verify_publickey_", comment: "") ) } } @@ -357,11 +358,10 @@ class NCEndToEndSetup { /// - 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: "Failed to generate passphrase" + errorDescription: NSLocalizedString("_e2ee_setup_generate_passphrase_", comment: "") ) } diff --git a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift index af441c9472..06dcee000a 100644 --- a/iOSClient/Settings/E2EE/NCManageE2EEModel.swift +++ b/iOSClient/Settings/E2EE/NCManageE2EEModel.swift @@ -100,7 +100,7 @@ class NCManageE2EE: NSObject, ObservableObject, ViewOnAppearHandling, TOPasscode do { let e2ee = NCEndToEndSetup(controller: controller) try await e2ee.start() - + isEndToEndEnabled = true } catch let error as NKError { if error.errorCode == NSUserCancelledError { return 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"; From d1b4a59ee578f890213aae239e4821a86f7ead59 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 2 Apr 2026 15:26:59 +0200 Subject: [PATCH 7/8] fix Signed-off-by: Marino Faggiana --- iOSClient/Extensions/UIAlertController+Extension.swift | 2 +- iOSClient/Networking/E2EE/NCNetworkingE2EEMarkFolder.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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/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 From d0f1f14554b3361ae1fc0fd9d8d2e4ba77f98eb0 Mon Sep 17 00:00:00 2001 From: Marino Faggiana Date: Thu, 2 Apr 2026 15:36:21 +0200 Subject: [PATCH 8/8] didselect push check e2ee Signed-off-by: Marino Faggiana --- ...ionViewCommon+CollectionViewDelegate.swift | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift index 7fb4387099..2725d002ae 100644 --- a/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift +++ b/iOSClient/Main/Collection Common/NCCollectionViewCommon+CollectionViewDelegate.swift @@ -13,21 +13,35 @@ extension NCCollectionViewCommon: UICollectionViewDelegate { func didSelectMetadata(_ metadata: tableMetadata, withOcIds: Bool) async { let capabilities = await NKCapabilities.shared.getCapabilities(for: session.account) - /* 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_") return } } - */ func downloadFile() async { var downloadRequest: DownloadRequest?