1+ import Foundation
2+ import ZIPFoundation
3+
4+ enum CertType : String {
5+ case signed = " Signed "
6+ case revoked = " Revoked "
7+ }
8+
9+ struct Cert : Identifiable , Hashable {
10+ let id = UUID ( )
11+ let name : String
12+ let downloadURL : URL
13+ let type : CertType
14+ let lastModified : Date ?
15+ }
16+
17+ @MainActor
18+ class OfficialCertificateManager : ObservableObject {
19+ @Published var certs : [ Cert ] = [ ]
20+ @Published var featuredCert : Cert ?
21+ @Published var currentStatus = " "
22+ @Published var isProcessing = false
23+
24+ func loadCerts( ) async {
25+ guard let tokenURL = URL ( string: " https://certapi.loyah.dev/pac " ) else { return }
26+ do {
27+ let ( tokenData, _) = try await URLSession . shared. data ( from: tokenURL)
28+ guard let token = String ( data: tokenData, encoding: . utf8) ? . trimmingCharacters ( in: . whitespacesAndNewlines) , !token. isEmpty else { return }
29+
30+ let signedAPIURL = URL ( string: " https://api.github.com/repos/loyahdev/certificates/contents/certs/signed " ) !
31+ var request = URLRequest ( url: signedAPIURL)
32+ request. setValue ( " Bearer \( token) " , forHTTPHeaderField: " Authorization " )
33+ request. setValue ( " application/vnd.github.v3+json " , forHTTPHeaderField: " Accept " )
34+
35+ let ( signedData, _) = try await URLSession . shared. data ( for: request)
36+ let signedJSON = try JSONSerialization . jsonObject ( with: signedData) as? [ [ String : Any ] ] ?? [ ]
37+ let signedFiles = signedJSON. compactMap { dict -> ( name: String , downloadURL: String , path: String ) ? in
38+ guard let name = dict [ " name " ] as? String , name. hasSuffix ( " .zip " ) ,
39+ let dl = dict [ " download_url " ] as? String else { return nil }
40+ return ( name, dl, " certs/signed/ \( name) " )
41+ }
42+
43+ let revokedAPIURL = URL ( string: " https://api.github.com/repos/loyahdev/certificates/contents/certs/revoked " ) !
44+ request. url = revokedAPIURL
45+ let ( revokedData, _) = try await URLSession . shared. data ( for: request)
46+ let revokedJSON = try JSONSerialization . jsonObject ( with: revokedData) as? [ [ String : Any ] ] ?? [ ]
47+ let revokedFiles = revokedJSON. compactMap { dict -> ( name: String , downloadURL: String , path: String ) ? in
48+ guard let name = dict [ " name " ] as? String , name. hasSuffix ( " .zip " ) ,
49+ let dl = dict [ " download_url " ] as? String else { return nil }
50+ return ( name, dl, " certs/revoked/ \( name) " )
51+ }
52+
53+ let allFiles = signedFiles + revokedFiles
54+
55+ let dates = try await withThrowingTaskGroup ( of: ( String, String, String, Date? ) . self) { group in
56+ for file in allFiles {
57+ let encodedPath = file. path. addingPercentEncoding ( withAllowedCharacters: . urlQueryAllowed) ?? file. path
58+ let commitsURL = URL ( string: " https://api.github.com/repos/loyahdev/certificates/commits?path= \( encodedPath) &per_page=1 " ) !
59+ var req = URLRequest ( url: commitsURL)
60+ req. setValue ( " Bearer \( token) " , forHTTPHeaderField: " Authorization " )
61+ req. setValue ( " application/vnd.github.v3+json " , forHTTPHeaderField: " Accept " )
62+
63+ group. addTask {
64+ do {
65+ let ( data, _) = try await URLSession . shared. data ( for: req)
66+ if let arr = try JSONSerialization . jsonObject ( with: data) as? [ [ String : Any ] ] ,
67+ let first = arr. first,
68+ let commitDict = first [ " commit " ] as? [ String : Any ] ,
69+ let authorDict = commitDict [ " author " ] as? [ String : Any ] ,
70+ let dateStr = authorDict [ " date " ] as? String {
71+ let formatter = ISO8601DateFormatter ( )
72+ let date = formatter. date ( from: dateStr)
73+ return ( file. name, file. downloadURL, file. path, date)
74+ }
75+ } catch {
76+ print ( " Date fetch error for \( file. name) : \( error) " )
77+ }
78+ return ( file. name, file. downloadURL, file. path, nil )
79+ }
80+ }
81+
82+ var results : [ ( String , String , String , Date ? ) ] = [ ]
83+ for try await result in group {
84+ results. append ( result)
85+ }
86+ return results
87+ }
88+
89+ var newCerts : [ Cert ] = [ ]
90+ for (name, dlStr, path, date) in dates {
91+ guard let dlURL = URL ( string: dlStr) else { continue }
92+ let type : CertType = path. contains ( " /signed/ " ) ? . signed : . revoked
93+ let cleanName = String ( name. dropLast ( 4 ) ) // Remove .zip
94+ newCerts. append ( Cert ( name: cleanName, downloadURL: dlURL, type: type, lastModified: date) )
95+ }
96+
97+ // Sort signed and revoked separately by date desc (recent first)
98+ let signedCerts = newCerts. filter { $0. type == . signed } . sorted { ( $0. lastModified ?? . distantPast) > ( $1. lastModified ?? . distantPast) }
99+ let revokedCerts = newCerts. filter { $0. type == . revoked } . sorted { ( $0. lastModified ?? . distantPast) > ( $1. lastModified ?? . distantPast) }
100+ certs = signedCerts + revokedCerts
101+
102+ featuredCert = signedCerts. first ?? revokedCerts. first
103+ } catch {
104+ print ( " Load official certs error: \( error) " )
105+ }
106+ }
107+
108+ func checkCert( _ cert: Cert ) async {
109+ isProcessing = true
110+ currentStatus = " Checking... "
111+
112+ let tempDir = FileManager . default. temporaryDirectory. appendingPathComponent ( UUID ( ) . uuidString)
113+ defer {
114+ try ? FileManager . default. removeItem ( at: tempDir)
115+ }
116+
117+ do {
118+ try FileManager . default. createDirectory ( at: tempDir, withIntermediateDirectories: true , attributes: nil )
119+
120+ let ( zipData, _) = try await URLSession . shared. data ( from: cert. downloadURL)
121+ let tempZipURL = tempDir. appendingPathComponent ( " cert.zip " )
122+ try zipData. write ( to: tempZipURL)
123+
124+ // Unzip using ZIPFoundation
125+ try FileManager . default. unzipItem ( at: tempZipURL, to: tempDir)
126+
127+ // Find the extraction directory (root or single subdir)
128+ let contents = try FileManager . default. contentsOfDirectory ( at: tempDir, includingPropertiesForKeys: nil , options: . skipsHiddenFiles)
129+ var searchDir = tempDir
130+ if contents. count == 1 {
131+ let firstItem = contents [ 0 ]
132+ var isDir : ObjCBool = false
133+ if FileManager . default. fileExists ( atPath: firstItem. path, isDirectory: & isDir) , isDir. boolValue {
134+ searchDir = firstItem
135+ }
136+ }
137+
138+ let fileContents = try FileManager . default. contentsOfDirectory ( at: searchDir, includingPropertiesForKeys: nil , options: . skipsHiddenFiles)
139+ var p12URL : URL ?
140+ var provURL : URL ?
141+ var txtURL : URL ?
142+
143+ for url in fileContents {
144+ let ext = url. pathExtension. lowercased ( )
145+ if ext == " p12 " { p12URL = url }
146+ else if ext == " mobileprovision " { provURL = url }
147+ else if ext == " txt " { txtURL = url }
148+ }
149+
150+ guard let p12U = p12URL, let provU = provURL, let txtU = txtURL else {
151+ throw NSError ( domain: " MissingFiles " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Zip missing p12, provision, or txt " ] )
152+ }
153+
154+ let txtData = try Data ( contentsOf: txtU)
155+ let password = String ( data: txtData, encoding: . utf8) ? . trimmingCharacters ( in: . whitespacesAndNewlines) ?? " "
156+
157+ let p12Data = try Data ( contentsOf: p12U)
158+ let provData = try Data ( contentsOf: provU)
159+
160+ let result = CertificatesManager . check ( p12Data: p12Data, password: password, mobileProvisionData: provData)
161+
162+ switch result {
163+ case . success( . success) :
164+ currentStatus = " Success! "
165+ case . success( . incorrectPassword) :
166+ currentStatus = " Incorrect Password "
167+ case . success( . noMatch) :
168+ currentStatus = " P12 and MobileProvision do not match "
169+ case . failure( let err) :
170+ print ( " Official check error: \( err) " )
171+ currentStatus = " P12 and MobileProvision do not match "
172+ }
173+ } catch {
174+ print ( " Official cert process error: \( error) " )
175+ currentStatus = " Error: Couldn't process zip "
176+ }
177+
178+ isProcessing = false
179+ }
180+ }
0 commit comments