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

Commit a39ef6c

Browse files
authored
Update to use official loyahdev certificates for v1.4.0
1 parent 5464b80 commit a39ef6c

4 files changed

Lines changed: 341 additions & 45 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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+
}

Sources/prostore/views/AboutView.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// AboutView.swift
12
import SwiftUI
23

34
struct Credit: Identifiable {
@@ -27,6 +28,12 @@ struct AboutView: View {
2728
role: "Developer",
2829
profileURL: URL(string: "https://github.com/SuperGamer474")!,
2930
avatarURL: URL(string: "https://github.com/SuperGamer474.png")!
31+
),
32+
Credit(
33+
name: "loyahdev",
34+
role: "iOS Certificates Source",
35+
profileURL: URL(string: "https://github.com/loyahdev")!,
36+
avatarURL: URL(string: "https://github.com/loyahdev.png")!
3037
)
3138
]
3239

@@ -126,10 +133,16 @@ struct CreditRow: View {
126133
} label: {
127134
Image(systemName: "arrow.up.right.square")
128135
.imageScale(.large)
129-
.foregroundColor(.primary) // Neutral color instead of blue
136+
.foregroundColor(.primary)
130137
}
131138
.buttonStyle(BorderlessButtonStyle())
132139
}
133140
.padding(.vertical, 8)
134141
}
142+
}
143+
144+
struct AboutView_Previews: PreviewProvider {
145+
static var previews: some View {
146+
AboutView()
147+
}
135148
}

0 commit comments

Comments
 (0)