Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit ba26617

Browse files
authored
Refactor CertificatesManager to use Combine
Updated CertificatesManager to use Combine for state management and added a shared instance for easier access.
1 parent 94b2ee8 commit ba26617

1 file changed

Lines changed: 54 additions & 42 deletions

File tree

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
// certificates.swift
1+
// CertificatesManager.swift
22
import Foundation
33
import Security
44
import CryptoKit
5+
import Combine
56

67
public enum CertificateCheckResult {
78
case incorrectPassword
@@ -19,19 +20,27 @@ public enum CertificateError: Error {
1920
case unknown
2021
}
2122

22-
public final class CertificatesManager {
23-
// SHA256 hex from Data
24-
static func sha256Hex(_ d: Data) -> String {
23+
/// CertificatesManager handles cert extraction/checking and exposes the currently selected identity.
24+
/// Replace `SecIdentity` with your own wrapper type if you use a custom model.
25+
public final class CertificatesManager: ObservableObject {
26+
public static let shared = CertificatesManager()
27+
private init() {}
28+
29+
// The currently selected certificate identity (nil if none chosen).
30+
@Published public var selectedCertificate: SecIdentity? = nil
31+
32+
// MARK: - Utility: SHA256 hex
33+
public static func sha256Hex(_ d: Data) -> String {
2534
let digest = SHA256.hash(data: d)
2635
return digest.map { String(format: "%02x", $0) }.joined()
2736
}
28-
29-
// Export public key bytes for a certificate (SecCertificate -> SecKey -> external representation)
37+
38+
// MARK: - Export public key bytes for a SecCertificate
3039
private static func publicKeyData(from cert: SecCertificate) throws -> Data {
3140
guard let secKey = SecCertificateCopyKey(cert) else {
3241
throw CertificateError.certExtractionFailed
3342
}
34-
43+
3544
var cfErr: Unmanaged<CFError>?
3645
guard let keyData = SecKeyCopyExternalRepresentation(secKey, &cfErr) as Data? else {
3746
if let cfError = cfErr?.takeRetainedValue() {
@@ -41,28 +50,27 @@ public final class CertificatesManager {
4150
throw CertificateError.publicKeyExportFailed(-1)
4251
}
4352
}
44-
53+
4554
return keyData
4655
}
47-
48-
// Extract the <plist>...</plist> portion from a .mobileprovision (PKCS7) blob,
49-
// parse it to a dictionary and return SecCertificate objects from DeveloperCertificates.
56+
57+
// MARK: - Extract certificates array from mobileprovision (PKCS7) blob
5058
private static func certificatesFromMobileProvision(_ data: Data) throws -> [SecCertificate] {
5159
let startTag = Data("<plist".utf8)
5260
let endTag = Data("</plist>".utf8)
53-
61+
5462
guard let startRange = data.range(of: startTag),
5563
let endRange = data.range(of: endTag) else {
5664
throw CertificateError.plistExtractionFailed
5765
}
58-
66+
5967
let plistData = data[startRange.lowerBound..<endRange.upperBound]
6068
let parsed = try PropertyListSerialization.propertyList(from: Data(plistData), options: [], format: nil)
61-
69+
6270
guard let dict = parsed as? [String: Any] else {
6371
throw CertificateError.plistExtractionFailed
6472
}
65-
73+
6674
var resultCerts: [SecCertificate] = []
6775
if let devArray = dict["DeveloperCertificates"] as? [Any] {
6876
for item in devArray {
@@ -78,17 +86,17 @@ public final class CertificatesManager {
7886
}
7987
}
8088
}
81-
89+
8290
if resultCerts.isEmpty {
8391
throw CertificateError.noCertsInProvision
8492
}
85-
93+
8694
return resultCerts
8795
}
88-
89-
/// Get the certificate's display name (subject summary)
90-
public static func getCertificateName(mobileProvisionData: Data) -> String? {
91-
// Extract the <plist>...</plist> block from the mobileprovision (PKCS7) blob
96+
97+
// MARK: - Readable display name from mobileprovision
98+
public func getCertificateName(mobileProvisionData: Data) -> String? {
99+
// Extract the <plist>...</plist> block
92100
let startTag = Data("<plist".utf8)
93101
let endTag = Data("</plist>".utf8)
94102
guard let startRange = mobileProvisionData.range(of: startTag),
@@ -99,76 +107,80 @@ public final class CertificatesManager {
99107
let plistDataSlice = mobileProvisionData[startRange.lowerBound..<endRange.upperBound]
100108
let plistData = Data(plistDataSlice)
101109

102-
// Parse plist into a dictionary
103110
guard let parsed = try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil),
104111
let dict = parsed as? [String: Any] else {
105112
return nil
106113
}
107114

108-
// Prefer TeamName if present
109115
if let teamName = dict["TeamName"] as? String, !teamName.isEmpty {
110116
return teamName
111117
}
112-
113-
// Fallback to Name (string)
114118
if let name = dict["Name"] as? String, !name.isEmpty {
115119
return name
116120
}
117-
118121
return nil
119122
}
120-
121-
/// Top-level check: returns result
123+
124+
// MARK: - Top-level check: verify p12 matches one of the embedded certs in mobileprovision
125+
/// Returns .success(.success) if match, .success(.noMatch) if no match, or .failure(Error)
122126
public static func check(p12Data: Data, password: String, mobileProvisionData: Data) -> Result<CertificateCheckResult, Error> {
123127
let options = [kSecImportExportPassphrase as String: password] as CFDictionary
124128
var itemsCF: CFArray?
125-
129+
126130
let importStatus = SecPKCS12Import(p12Data as CFData, options, &itemsCF)
127-
131+
128132
if importStatus == errSecAuthFailed {
129133
return .success(.incorrectPassword)
130134
}
131-
135+
132136
guard importStatus == errSecSuccess, let items = itemsCF as? [[String: Any]], items.count > 0 else {
133137
return .failure(CertificateError.p12ImportFailed(importStatus))
134138
}
135-
139+
136140
guard let first = items.first else {
137141
return .failure(CertificateError.identityExtractionFailed)
138142
}
139-
140-
let identity = first[kSecImportItemIdentity as String] as! SecIdentity
141-
143+
144+
// kSecImportItemIdentity should be present
145+
guard let identityAny = first[kSecImportItemIdentity as String] else {
146+
return .failure(CertificateError.identityExtractionFailed)
147+
}
148+
149+
// Attempt to cast to SecIdentity
150+
guard let identity = identityAny as? SecIdentity else {
151+
return .failure(CertificateError.identityExtractionFailed)
152+
}
153+
142154
var certRef: SecCertificate?
143155
let certStatus = SecIdentityCopyCertificate(identity, &certRef)
144-
156+
145157
guard certStatus == errSecSuccess, let p12Cert = certRef else {
146158
return .failure(CertificateError.certExtractionFailed)
147159
}
148-
160+
149161
do {
150162
let p12PubKeyData = try publicKeyData(from: p12Cert)
151163
let p12Hash = sha256Hex(p12PubKeyData)
152-
164+
153165
let embeddedCerts = try certificatesFromMobileProvision(mobileProvisionData)
154-
166+
155167
for cert in embeddedCerts {
156168
do {
157169
let embPubKeyData = try publicKeyData(from: cert)
158170
let embHash = sha256Hex(embPubKeyData)
159-
171+
160172
if embHash == p12Hash {
161173
return .success(.success)
162174
}
163175
} catch {
176+
// continue checking other embedded certs
164177
continue
165178
}
166179
}
167-
180+
168181
return .success(.noMatch)
169182
} catch {
170183
return .failure(error)
171184
}
172185
}
173-
174186
}

0 commit comments

Comments
 (0)