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

Commit f1b8b34

Browse files
authored
Update certChecker to use applep12 API
1 parent ecbf5b4 commit f1b8b34

2 files changed

Lines changed: 214 additions & 64 deletions

File tree

Lines changed: 213 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,225 @@
11
import Foundation
22

3-
public final class CertRevokeChecker {
4-
public static func checkRevocation(folderName: String) async -> String {
5-
let certDir = CertificateFileManager.shared.certificatesDirectory.appendingPathComponent(folderName)
6-
let p12URL = certDir.appendingPathComponent("certificate.p12")
7-
let provURL = certDir.appendingPathComponent("profile.mobileprovision")
8-
let passwordURL = certDir.appendingPathComponent("password.txt")
9-
10-
var password = ""
11-
if let pwData = try? Data(contentsOf: passwordURL), let pw = String(data: pwData, encoding: .utf8) {
12-
password = pw
13-
}
14-
15-
guard let p12Data = try? Data(contentsOf: p12URL),
16-
let provData = try? Data(contentsOf: provURL) else {
17-
return "Unknown"
18-
}
19-
20-
guard let url = URL(string: "https://tools.nezushub.vip/cert-ios-checker/api/") else {
21-
return "Unknown"
3+
class CertChecker {
4+
static let baseURL = URL(string: "https://check-p12.applep12.com/")!
5+
6+
static func getToken() async throws -> String {
7+
let (data, _) = try await URLSession.shared.data(from: baseURL)
8+
let html = String(data: data, encoding: .utf8) ?? ""
9+
let pattern = "(?i)<input\\s+name=\"__RequestVerificationToken\"\\s+type=\"hidden\"\\s+value=\"([^\"]+)\""
10+
let regex = try? NSRegularExpression(pattern: pattern)
11+
if let match = regex?.firstMatch(in: html, range: NSRange(location: 0, length: html.utf16.count)),
12+
let range = Range(match.range(at: 1), in: html) {
13+
return String(html[range])
2214
}
23-
24-
var request = URLRequest(url: url)
15+
throw NSError(domain: "Token not found", code: 0)
16+
}
17+
18+
static func submit(token: String, p12Data: Data, p12Filename: String, mpData: Data, mpFilename: String, password: String) async throws -> String {
19+
var request = URLRequest(url: baseURL)
2520
request.httpMethod = "POST"
26-
2721
let boundary = "Boundary-\(UUID().uuidString)"
2822
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
29-
23+
request.setValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64)", forHTTPHeaderField: "User-Agent")
24+
request.setValue(baseURL.absoluteString, forHTTPHeaderField: "Referer")
25+
request.setValue("https://check-p12.applep12.com", forHTTPHeaderField: "Origin")
26+
3027
var body = Data()
31-
32-
// Add p12 file
33-
body.append("--\(boundary)\r\n".data(using: .utf8)!)
34-
body.append("Content-Disposition: form-data; name=\"file\"; filename=\"certificate.p12\"\r\n".data(using: .utf8)!)
35-
body.append("Content-Type: application/octet-stream\r\n\r\n".data(using: .utf8)!)
36-
body.append(p12Data)
37-
body.append("\r\n".data(using: .utf8)!)
38-
39-
// Add mobileprovision file
40-
body.append("--\(boundary)\r\n".data(using: .utf8)!)
41-
body.append("Content-Disposition: form-data; name=\"secondary_file\"; filename=\"profile.mobileprovision\"\r\n".data(using: .utf8)!)
42-
body.append("Content-Type: application/octet-stream\r\n\r\n".data(using: .utf8)!)
43-
body.append(provData)
44-
body.append("\r\n".data(using: .utf8)!)
45-
46-
// Add password
47-
body.append("--\(boundary)\r\n".data(using: .utf8)!)
48-
body.append("Content-Disposition: form-data; name=\"password\"\r\n\r\n".data(using: .utf8)!)
49-
body.append(password.data(using: .utf8)!)
50-
body.append("\r\n".data(using: .utf8)!)
51-
52-
// End boundary
28+
29+
func addPart(name: String, value: String? = nil, filename: String? = nil, contentType: String? = nil, data: Data? = nil) {
30+
body.append("--\(boundary)\r\n".data(using: .utf8)!)
31+
if let filename = filename {
32+
body.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!)
33+
if let contentType = contentType {
34+
body.append("Content-Type: \(contentType)\r\n\r\n".data(using: .utf8)!)
35+
}
36+
} else {
37+
body.append("Content-Disposition: form-data; name=\"\(name)\"\r\n\r\n".data(using: .utf8)!)
38+
}
39+
if let value = value {
40+
body.append(value.data(using: .utf8)!)
41+
} else if let data = data {
42+
body.append(data)
43+
}
44+
body.append("\r\n".data(using: .utf8)!)
45+
}
46+
47+
addPart(name: "P12File", filename: p12Filename, contentType: "application/x-pkcs12", data: p12Data)
48+
addPart(name: "P12PassWord", value: password)
49+
addPart(name: "MobileProvisionFile", filename: mpFilename, contentType: "application/octet-stream", data: mpData)
50+
addPart(name: "__RequestVerificationToken", value: token)
5351
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
54-
52+
5553
request.httpBody = body
56-
57-
do {
58-
let (data, _) = try await URLSession.shared.data(for: request)
59-
if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
60-
let success = json["success"] as? Bool, success,
61-
let dataDict = json["data"] as? [String: Any],
62-
let certificate = dataDict["certificate"] as? [String: Any],
63-
let comparison = certificate["comparison_data"] as? [String: Any],
64-
let match = comparison["certificates_match"] as? Bool, match,
65-
let certStatus = certificate["certificate_status"] as? [String: Any],
66-
let status = certStatus["status"] as? String {
67-
return status
68-
} else {
69-
return "Unknown"
54+
55+
let (data, _) = try await URLSession.shared.data(for: request)
56+
return String(data: data, encoding: .utf8) ?? ""
57+
}
58+
59+
static func parseHTML(html: String) -> [String: Any] {
60+
let divPattern = "(?i)<div\\s+class=\"[^\"]*alert[^\"]*\"[^>]*>(.*?)</div>"
61+
let divRegex = try? NSRegularExpression(pattern: divPattern, options: .dotMatchesLineSeparators)
62+
guard let match = divRegex?.firstMatch(in: html, range: NSRange(location: 0, length: html.utf16.count)),
63+
let range = Range(match.range(at: 1), in: html) else {
64+
return ["error": "No certificate info found in response"]
65+
}
66+
67+
var divContent = String(html[range])
68+
divContent = divContent.replacingOccurrences(of: "<br/>", with: "\n", options: .caseInsensitive)
69+
divContent = divContent.replacingOccurrences(of: "<br />", with: "\n", options: .caseInsensitive)
70+
let tagRegex = try? NSRegularExpression(pattern: "<[^>]+>", options: [])
71+
divContent = tagRegex?.stringByReplacingMatches(in: divContent, range: NSRange(0..<divContent.utf16.count), withTemplate: "") ?? divContent
72+
73+
var lines = divContent.components(separatedBy: .newlines).map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }
74+
75+
let emojiRegex = try? NSRegularExpression(pattern: "[🟢🔴]", options: [])
76+
for i in 0..<lines.count {
77+
lines[i] = emojiRegex?.stringByReplacingMatches(in: lines[i], range: NSRange(0..<lines[i].utf16.count), withTemplate: "") ?? lines[i]
78+
}
79+
80+
var data: [String: Any] = [
81+
"certificate": [String: String](),
82+
"mobileprovision": [String: String](),
83+
"binding_certificate_1": [String: String](),
84+
"permissions": [String: String]()
85+
]
86+
87+
func findIndex(prefixes: [String], start: Int = 0) -> Int? {
88+
for i in start..<lines.count {
89+
for p in prefixes {
90+
if lines[i].lowercased().hasPrefix(p.lowercased()) {
91+
return i
92+
}
93+
}
94+
}
95+
return nil
96+
}
97+
98+
let certIdx = findIndex(prefixes: ["CertName:", "CertName:"])
99+
let mpIdx = findIndex(prefixes: ["MP Name:", "MP Name:"])
100+
let bindingIdx = findIndex(prefixes: ["Binding Certificates:", "Binding Certificates:"], start: mpIdx ?? 0)
101+
let certMatchingIdx = findIndex(prefixes: ["Certificate Matching Status:", "Certificate Matching Status:"], start: bindingIdx ?? 0)
102+
103+
func splitKV(line: String) -> (String, String) {
104+
let separators = [":", ""]
105+
for sep in separators {
106+
if let range = line.range(of: sep) {
107+
let k = String(line[..<range.lowerBound]).trimmingCharacters(in: .whitespaces)
108+
let v = String(line[range.upperBound...]).trimmingCharacters(in: .whitespaces)
109+
return (k, v)
110+
}
111+
}
112+
return (line.trimmingCharacters(in: .whitespaces), "")
113+
}
114+
115+
if let certIdx = certIdx {
116+
let end = mpIdx ?? (bindingIdx ?? lines.count)
117+
for i in certIdx..<end {
118+
let (k, v) = splitKV(line: lines[i])
119+
let lk = k.lowercased()
120+
var cert = data["certificate"] as! [String: String]
121+
if lk.hasPrefix("certname") {
122+
cert["name"] = v
123+
} else if lk.hasPrefix("effective date") {
124+
cert["effective"] = v
125+
} else if lk.hasPrefix("expiration date") {
126+
cert["expiration"] = v
127+
} else if lk.hasPrefix("issuer") {
128+
cert["issuer"] = v
129+
} else if lk.hasPrefix("country") {
130+
cert["country"] = v
131+
} else if lk.hasPrefix("organization") {
132+
cert["organization"] = v
133+
} else if lk.hasPrefix("certificate number (hex)") {
134+
cert["number_hex"] = v
135+
} else if lk.hasPrefix("certificate number (decimal)") {
136+
cert["number_decimal"] = v
137+
} else if lk.hasPrefix("certificate status") {
138+
cert["status"] = v
139+
}
140+
data["certificate"] = cert
141+
}
142+
}
143+
144+
if let mpIdx = mpIdx {
145+
let end = bindingIdx ?? (certMatchingIdx ?? lines.count)
146+
for i in mpIdx..<end {
147+
let (k, v) = splitKV(line: lines[i])
148+
let lk = k.lowercased()
149+
var mp = data["mobileprovision"] as! [String: String]
150+
if lk.hasPrefix("mp name") {
151+
mp["name"] = v
152+
} else if lk.hasPrefix("app id") {
153+
mp["app_id"] = v
154+
} else if lk.hasPrefix("identifier") {
155+
mp["identifier"] = v
156+
} else if lk.hasPrefix("platform") {
157+
mp["platform"] = v
158+
} else if lk.hasPrefix("effective date") {
159+
if mp["effective"] == nil {
160+
mp["effective"] = v
161+
}
162+
} else if lk.hasPrefix("expiration date") {
163+
if mp["expiration"] == nil {
164+
mp["expiration"] = v
165+
}
166+
}
167+
data["mobileprovision"] = mp
70168
}
71-
} catch {
72-
return "Unknown"
73169
}
170+
171+
if let bindingIdx = bindingIdx {
172+
let cert1Idx = findIndex(prefixes: ["Certificate 1:", "Certificate 1:", "Certificate 1"], start: bindingIdx)
173+
if let cert1Idx = cert1Idx {
174+
let cert2Idx = findIndex(prefixes: ["Certificate 2:", "Certificate 2:", "Certificate 2"], start: cert1Idx + 1)
175+
let end = cert2Idx ?? (certMatchingIdx ?? lines.count)
176+
for i in (cert1Idx + 1)..<end {
177+
let (k, v) = splitKV(line: lines[i])
178+
let lk = k.lowercased()
179+
var bc1 = data["binding_certificate_1"] as! [String: String]
180+
if lk.hasPrefix("certificate status") {
181+
bc1["status"] = v
182+
} else if lk.hasPrefix("certificate number (hex)") {
183+
bc1["number_hex"] = v
184+
} else if lk.hasPrefix("certificate number (decimal)") {
185+
bc1["number_decimal"] = v
186+
}
187+
data["binding_certificate_1"] = bc1
188+
}
189+
}
190+
}
191+
192+
let permKeys = [
193+
"Apple Push Notification Service",
194+
"HealthKit",
195+
"VPN",
196+
"Communication Notifications",
197+
"Time-sensitive Notifications"
198+
]
199+
for line in lines {
200+
for pk in permKeys {
201+
if line.hasPrefix(pk) {
202+
let (_, v) = splitKV(line: line)
203+
var perms = data["permissions"] as! [String: String]
204+
perms[pk] = v
205+
data["permissions"] = perms
206+
}
207+
}
208+
}
209+
210+
if let certMatchingIdx = certMatchingIdx {
211+
let (_, v) = splitKV(line: lines[certMatchingIdx])
212+
data["certificate_matching_status"] = v
213+
}
214+
215+
return data
216+
}
217+
218+
static func checkCert(mobileProvision: Data, mobileProvisionFilename: String = "example.mobileprovision",
219+
p12: Data, p12Filename: String = "example.p12",
220+
password: String) async throws -> [String: Any] {
221+
let token = try await getToken()
222+
let html = try await submit(token: token, p12Data: p12, p12Filename: p12Filename, mpData: mobileProvision, mpFilename: mobileProvisionFilename, password: password)
223+
return parseHTML(html: html)
74224
}
75-
}
225+
}

Sources/prostore/views/CertificateView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ struct CertificateView: View {
258258
loadExpiries()
259259
for cert in customCertificates {
260260
Task {
261-
let status = await CertRevokeChecker.checkRevocation(folderName: cert.folderName)
261+
let status = (try? await { let dir = CertificateFileManager.shared.certificatesDirectory.appendingPathComponent(cert.folderName); let p12 = try Data(contentsOf: dir.appendingPathComponent("certificate.p12")); let mp = try Data(contentsOf: dir.appendingPathComponent("profile.mobileprovision")); let pw = (try? String(contentsOf: dir.appendingPathComponent("password.txt"), encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""; return try await CertChecker.checkCert(mobileProvision: mp, mobileProvisionFilename: "profile.mobileprovision", p12: p12, p12Filename: "certificate.p12", password: pw) })().flatMap { ($0["certificate"] as? [String: String])?["status"] ?? ($0["certificate_matching_status"] as? String) } ?? "Unknown"
262262
await MainActor.run {
263263
certStatuses[cert.folderName] = status
264264
}

0 commit comments

Comments
 (0)