Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Nextcloud.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@
AFCE353927E5DE0500FEA6C2 /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* Shareable.swift */; };
D575039F27146F93008DC9DC /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7A0D1342591FBC5008F8A13 /* String+Extension.swift */; };
D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */; };
F30E77E92EAB716900B1EFAB /* CertificatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30E77E82EAB716900B1EFAB /* CertificatePicker.swift */; };
F30E77EC2EAB7C9B00B1EFAB /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30E77EB2EAB7C9800B1EFAB /* DocumentPicker.swift */; };
F30E77EF2EAF9BCD00B1EFAB /* CertificatePickerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F30E77EE2EAF9BC700B1EFAB /* CertificatePickerModel.swift */; };
F310B1EF2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F310B1EE2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift */; };
F314F1142A30E2DE00BC7FAB /* View+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E8A390295DC5E0006CB2D0 /* View+Extension.swift */; };
F321DA8A2B71205A00DDA0E6 /* NCTrashSelectTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F321DA892B71205A00DDA0E6 /* NCTrashSelectTabBar.swift */; };
Expand Down Expand Up @@ -1380,6 +1383,9 @@
C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
C04E2F202A17BB4D001BAD85 /* NextcloudIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCActivityTableViewCell.swift; sourceTree = "<group>"; };
F30E77E82EAB716900B1EFAB /* CertificatePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificatePicker.swift; sourceTree = "<group>"; };
F30E77EB2EAB7C9800B1EFAB /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = "<group>"; };
F30E77EE2EAF9BC700B1EFAB /* CertificatePickerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificatePickerModel.swift; sourceTree = "<group>"; };
F310B1EE2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCViewerMedia+VisionKit.swift"; sourceTree = "<group>"; };
F321DA892B71205A00DDA0E6 /* NCTrashSelectTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTrashSelectTabBar.swift; sourceTree = "<group>"; };
F32FADA82D1176DE007035E2 /* UIButton+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extension.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2257,6 +2263,16 @@
path = Tests;
sourceTree = "<group>";
};
F30E77EA2EAB7C1700B1EFAB /* CertificatePicker */ = {
isa = PBXGroup;
children = (
F30E77EE2EAF9BC700B1EFAB /* CertificatePickerModel.swift */,
F30E77EB2EAB7C9800B1EFAB /* DocumentPicker.swift */,
F30E77E82EAB716900B1EFAB /* CertificatePicker.swift */,
);
path = CertificatePicker;
sourceTree = "<group>";
};
F3374A7F2D64AB40002A38F9 /* Components */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3393,6 +3409,7 @@
F7725A5D251F33BB00D125E0 /* Files */,
F757CC8929E82D0500F31428 /* Groupfolders */,
F7BFFA621A24D7300044ED85 /* Login */,
F30E77EA2EAB7C1700B1EFAB /* CertificatePicker */,
F7EC9CB921185F2000F1C5CE /* Media */,
371B5A2F23D0B04B00FAFAE9 /* Menu */,
F7CB68942541670D0050EC94 /* More */,
Expand Down Expand Up @@ -4762,6 +4779,7 @@
F3F442EE2DDE292D00FD701F /* NCMetadataPermissions.swift in Sources */,
F3374A812D64AB9F002A38F9 /* StatusInfo.swift in Sources */,
AF7E504E27A2D8FF00B5E4AF /* UIBarButton+Extension.swift in Sources */,
F30E77E92EAB716900B1EFAB /* CertificatePicker.swift in Sources */,
AA8D31682D41224800FE2775 /* NCShareToggleCell.swift in Sources */,
F7A846DE2BB01ACB0024816F /* NCTrashCellProtocol.swift in Sources */,
F799DF852C4B7E56003410B5 /* NCSectionHeader.swift in Sources */,
Expand Down Expand Up @@ -4841,6 +4859,7 @@
F75D90212D2BE26F003E740B /* NCRecommendationsCell.swift in Sources */,
F7E98C1627E0D0FC001F9F19 /* NCManageDatabase+Video.swift in Sources */,
F7F4F11227ECDC52008676F9 /* UIFont+Extension.swift in Sources */,
F30E77EF2EAF9BCD00B1EFAB /* CertificatePickerModel.swift in Sources */,
F76882222C0DD1E7001CF441 /* NCCapabilitiesView.swift in Sources */,
F3CA337D2D0B2B6C00672333 /* AlbumModel.swift in Sources */,
AF93471A27E2361E002537EE /* NCShareHeader.swift in Sources */,
Expand Down Expand Up @@ -4926,6 +4945,7 @@
F7A03E332D426115007AA677 /* NCMoreNavigationController.swift in Sources */,
F7E402312BA891EB007E5609 /* NCTrash+SelectTabBarDelegate.swift in Sources */,
F70753EB2542A99800972D44 /* NCViewerMediaPage.swift in Sources */,
F30E77EC2EAB7C9B00B1EFAB /* DocumentPicker.swift in Sources */,
F7817CF829801A3500FFBC65 /* Data+Extension.swift in Sources */,
F749B651297B0F2400087535 /* NCManageDatabase+Avatar.swift in Sources */,
F7FAFD3A28BFA948000777FE /* NCNotification+Menu.swift in Sources */,
Expand Down
8 changes: 6 additions & 2 deletions Nextcloud.xcodeproj/xcshareddata/xcschemes/Nextcloud.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "NO"
enableThreadSanitizer = "YES"
codeCoverageEnabled = "YES">
disableMainThreadChecker = "YES"
codeCoverageEnabled = "YES"
disablePerformanceAntipatternChecker = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
Expand Down Expand Up @@ -119,12 +121,14 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
disableMainThreadChecker = "YES"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "NO">
allowLocationSimulation = "NO"
disablePerformanceAntipatternChecker = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
Expand Down
4 changes: 0 additions & 4 deletions iOSClient/Account/NCAccount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ class NCAccount: NSObject {
await changeAccount(account, userProfile: userProfile, controller: controller)
nkLog(debug: "NCAccount changed user profile to \(userProfile.userId).")

NCPreferences().setClientCertificate(account: account, p12Data: NCNetworking.shared.p12Data, p12Password: NCNetworking.shared.p12Password)

if let controller {
controller.account = account
nkLog(debug: "Dismissing login provider view controller...")
Expand Down Expand Up @@ -95,8 +93,6 @@ class NCAccount: NSObject {
if let userProfile {
await database.setAccountUserProfileAsync(account: account, userProfile: userProfile)
}
// Networking Certificate
NCNetworking.shared.activeAccountCertificate(account: account)
// Subscribing Push Notification
await NCPushNotification.shared.subscribingNextcloudServerPushNotification(account: tblAccount.account, urlBase: tblAccount.urlBase)
// Start the service
Expand Down
108 changes: 108 additions & 0 deletions iOSClient/CertificatePicker/CertificatePicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2025 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import SwiftUI
import UniformTypeIdentifiers

struct CertificatePicker: View {
@State private var model = CertificatePickerModel()
@State private var showingPicker = false
@State private var fileName: String = ""
@State private var pickedURL: URL?
@State private var password: String = ""

let urlBase: String
weak var delegate: CertificatePickerDelegate?

@Environment(\.dismiss) private var dismiss

var body: some View {
NavigationStack {
VStack {
Form {
Section(header: Text(String(format: NSLocalizedString("_no_client_cert_found_", comment: ""), urlBase)), footer: Text("_no_client_cert_found_desc_")) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("_cert_title_")
.font(.headline)
if !fileName.isEmpty {
Text(fileName)
.font(.subheadline)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
} else {
Text("No file selected")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
Spacer()
Button("_upload_") {
showingPicker = true
}
.buttonStyle(.bordered)
.foregroundStyle(Color(NCBrandColor.shared.customer))
}
}

Section(footer: Text("_no_client_cert_found_desc_password_")) {
SecureField("_password_", text: $password)
.textContentType(.password)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.submitLabel(.done)
.onSubmit {
if let url = pickedURL {
model.handleCertificate(fileUrl: url, urlBase: urlBase, password: password)
}
}
}
}
}
.onAppear {
model.delegate = delegate
}
.navigationTitle(NSLocalizedString("_cert_navigation_title_", comment: ""))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
dismiss()
} label: {
Image(systemName: "xmark")
}
}
ToolbarItem(placement: .confirmationAction) {
Button {
if let url = pickedURL {
model.handleCertificate(fileUrl: url, urlBase: urlBase, password: password)
}
} label: {
Image(systemName: "checkmark")
}
.keyboardShortcut(.defaultAction)
.disabled(pickedURL == nil || password.isEmpty)
.tint(Color(NCBrandColor.shared.customer))
}
}
.sheet(isPresented: $showingPicker) {
DocumentPicker(contentTypes: [UTType.pkcs12]) { urls in
if let url = urls.first {
pickedURL = url
fileName = url.lastPathComponent
}
}
}
.alert(NSLocalizedString("_client_cert_wrong_password_", comment: ""), isPresented: $model.isWrongPassword) {}
.onChange(of: model.isCertImportedSuccessfully) { _, newValue in
if newValue { dismiss() }
}
}
}
}

#Preview {
CertificatePicker(urlBase: "test.com")
}
74 changes: 74 additions & 0 deletions iOSClient/CertificatePicker/CertificatePickerModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2025 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

protocol CertificatePickerDelegate: AnyObject {
func certificatePickerDidImportIdentity(_ picker: CertificatePickerModel, for urlBase: String)
}

@Observable class CertificatePickerModel: NSObject, UIDocumentPickerDelegate {
var isWrongPassword = false
var isCertImportedSuccessfully = false
@ObservationIgnored weak var delegate: CertificatePickerDelegate?

func handleCertificate(fileUrl: URL, urlBase: String, password: String) {
if fileUrl.startAccessingSecurityScopedResource() {
defer {
fileUrl.stopAccessingSecurityScopedResource()
}

if let identity = getIdentityFromP12(from: fileUrl, password: password) {
let urlWithoutScheme = urlBase.replacingOccurrences(of: "https://", with: "").replacingOccurrences(of: "http://", with: "")
let label = "client_identity_\(urlWithoutScheme)"
storeIdentityInKeychain(identity: identity, label: label)
delegate?.certificatePickerDidImportIdentity(self, for: urlBase)
isCertImportedSuccessfully = true
} else {
isWrongPassword = true
}
}
}

func getIdentityFromP12(from url: URL, password: String) -> SecIdentity? {
guard let p12Data = try? Data(contentsOf: url) else { return nil }

let options = [kSecImportExportPassphrase as String: password]
var items: CFArray?
let status = SecPKCS12Import(p12Data as CFData, options as CFDictionary, &items)

if status == errSecSuccess,
let array = items as? [[String: Any]] {
// swiftlint:disable force_cast
if let identity = array.first?[kSecImportItemIdentity as String] as! SecIdentity? {
// swiftlint:enable force_cast
return identity
}
}
return nil
}

func storeIdentityInKeychain(identity: SecIdentity, label: String) {
let addQuery: [String: Any] = [
kSecValueRef as String: identity,
kSecClass as String: kSecClassIdentity,
kSecAttrLabel as String: label,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]

let classes = [kSecClassIdentity, kSecClassCertificate, kSecClassKey]
for secClass in classes {
let deleteQuery: [String: Any] = [
kSecClass as String: secClass,
kSecAttrLabel as String: label,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
]
let status = SecItemDelete(deleteQuery as CFDictionary)
print("Deleting \(secClass): \(status)")
}

let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
print("Add status: \(addStatus)")

}

}
36 changes: 36 additions & 0 deletions iOSClient/CertificatePicker/DocumentPicker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2025 Milen Pivchev
// SPDX-License-Identifier: GPL-3.0-or-later

import SwiftUI
import UniformTypeIdentifiers

struct DocumentPicker: UIViewControllerRepresentable {
var contentTypes: [UTType]
var onPickURLs: ([URL]) -> Void

func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let picker = UIDocumentPickerViewController(forOpeningContentTypes: contentTypes)
picker.delegate = context.coordinator
picker.allowsMultipleSelection = false
return picker
}

func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {}

func makeCoordinator() -> Coordinator {
Coordinator(onPickURLs: onPickURLs)
}

final class Coordinator: NSObject, UIDocumentPickerDelegate {
let onPickURLs: ([URL]) -> Void

init(onPickURLs: @escaping ([URL]) -> Void) {
self.onPickURLs = onPickURLs
}

func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
onPickURLs(urls)
}
}
}
Loading