11// AboutView.swift
22import SwiftUI
3+ import Security
34
45struct Credit : Identifiable {
56 var id = UUID ( )
@@ -9,7 +10,126 @@ struct Credit: Identifiable {
910 var avatarURL : URL
1011}
1112
13+ @MainActor
14+ final class SigningInfoProvider : ObservableObject {
15+ @Published var certCommonName : String = " Unknown "
16+ @Published var certExpiry : Date ? = nil
17+
18+ @Published var provName : String = " Unknown "
19+ @Published var provExpiry : Date ? = nil
20+
21+ @Published var errorMessage : String ? = nil
22+
23+ init ( ) {
24+ Task { await fetchAll ( ) }
25+ }
26+
27+ func fetchAll( ) async {
28+ fetchSigningCertificate ( )
29+ fetchEmbeddedProvision ( )
30+ }
31+
32+ // MARK: - Certificate (from code signature)
33+ private func fetchSigningCertificate( ) {
34+ var code : SecCode ?
35+ let status = SecCodeCopySelf ( [ ] , & code)
36+ guard status == errSecSuccess, let codeUnwrapped = code else {
37+ self . errorMessage = " Could not read code object "
38+ return
39+ }
40+
41+ var signingInfoRef : CFDictionary ?
42+ let infoStatus = SecCodeCopySigningInformation ( codeUnwrapped, SecCSFlags ( rawValue: kSecCSSigningInformation) , & signingInfoRef)
43+ if infoStatus != errSecSuccess {
44+ // Try with empty flags if the constant isn't available
45+ let _ = SecCodeCopySigningInformation ( codeUnwrapped, [ ] , & signingInfoRef)
46+ }
47+
48+ guard let signingInfo = signingInfoRef as? [ String : Any ] ,
49+ let certs = signingInfo [ kSecCodeInfoCertificates as String ] as? [ SecCertificate ] ,
50+ let firstCert = certs. first
51+ else {
52+ self . errorMessage = " No certificate in signing info "
53+ return
54+ }
55+
56+ // Common name / subject summary
57+ if let name = SecCertificateCopySubjectSummary ( firstCert) as String ? {
58+ self . certCommonName = name
59+ }
60+
61+ // NotAfter (expiry)
62+ var valuesRef : CFDictionary ?
63+ let oids = [ kSecOIDX509V1ValidityNotAfter] as CFArray
64+ if SecCertificateCopyValues ( firstCert, oids, & valuesRef) == errSecSuccess,
65+ let values = valuesRef as? [ String : Any ] ,
66+ let notAfterEntry = values [ kSecOIDX509V1ValidityNotAfter as String ] as? [ String : Any ] ,
67+ let expiry = notAfterEntry [ kSecPropertyKeyValue as String ] as? Date {
68+ self . certExpiry = expiry
69+ } else {
70+ // fallback: try to parse summary data (rare)
71+ self . certExpiry = nil
72+ }
73+ }
74+
75+ // MARK: - embedded.mobileprovision (from bundle)
76+ private func fetchEmbeddedProvision( ) {
77+ guard let provPath = Bundle . main. path ( forResource: " embedded " , ofType: " mobileprovision " ) else {
78+ // Not found (e.g. App Store app or simulator). Keep defaults.
79+ return
80+ }
81+
82+ do {
83+ let data = try Data ( contentsOf: URL ( fileURLWithPath: provPath) )
84+ guard let str = String ( data: data, encoding: . utf8) else { return }
85+
86+ // The mobileprovision is a CMS envelope containing a plist. Extract plist XML segment.
87+ if let startRange = str. range ( of: " <?xml " ) ,
88+ let endRange = str. range ( of: " </plist> " ) {
89+ let plistString = String ( str [ startRange. lowerBound... endRange. upperBound] )
90+ if let plistData = plistString. data ( using: . utf8) {
91+ let plist = try PropertyListSerialization . propertyList ( from: plistData, options: [ ] , format: nil )
92+ if let dict = plist as? [ String : Any ] {
93+ if let name = dict [ " Name " ] as? String {
94+ self . provName = name
95+ }
96+ if let expiry = dict [ " ExpirationDate " ] as? Date {
97+ self . provExpiry = expiry
98+ } else if let expiryStr = dict [ " ExpirationDate " ] as? String {
99+ // Some formats might return a string — try ISO8601
100+ let df = ISO8601DateFormatter ( )
101+ if let d = df. date ( from: expiryStr) {
102+ self . provExpiry = d
103+ }
104+ }
105+ }
106+ }
107+ } else {
108+ // if no plist detected, try scanning bytes for plist start/end bytes
109+ // (some profiles are binary or slightly different) — try a Data approach
110+ if let plistRangeStart = data. range ( of: Data ( " <?xml " . utf8) ) ,
111+ let plistRangeEnd = data. range ( of: Data ( " </plist> " . utf8) ) {
112+ let plistData = data [ plistRangeStart. lowerBound... plistRangeEnd. upperBound]
113+ let plist = try PropertyListSerialization . propertyList ( from: plistData, options: [ ] , format: nil )
114+ if let dict = plist as? [ String : Any ] {
115+ if let name = dict [ " Name " ] as? String {
116+ self . provName = name
117+ }
118+ if let expiry = dict [ " ExpirationDate " ] as? Date {
119+ self . provExpiry = expiry
120+ }
121+ }
122+ }
123+ }
124+ } catch {
125+ self . errorMessage = " Failed to read embedded.mobileprovision: \( error. localizedDescription) "
126+ }
127+ }
128+ }
129+
12130struct AboutView : View {
131+ @StateObject private var signingInfo = SigningInfoProvider ( )
132+
13133 private let credits : [ Credit ] = [
14134 Credit (
15135 name: " SuperGamer474 " ,
@@ -45,6 +165,17 @@ struct AboutView: View {
45165 Bundle . main. object ( forInfoDictionaryKey: " CFBundleShortVersionString " ) as? String ?? " 1.0 "
46166 }
47167
168+ private var buildString : String {
169+ Bundle . main. object ( forInfoDictionaryKey: " CFBundleVersion " ) as? String ?? " 1 "
170+ }
171+
172+ private var dateFormatter : DateFormatter {
173+ let df = DateFormatter ( )
174+ df. dateStyle = . medium
175+ df. timeStyle = . short
176+ return df
177+ }
178+
48179 var body : some View {
49180 NavigationStack {
50181 List {
@@ -75,9 +206,82 @@ struct AboutView: View {
75206 . font ( . title2)
76207 . fontWeight ( . semibold)
77208
78- Text ( " Version \( versionString) " )
79- . font ( . footnote)
80- . foregroundColor ( . secondary)
209+ VStack ( spacing: 2 ) {
210+ Text ( " Version \( versionString) ( \( buildString) ) " )
211+ . font ( . footnote)
212+ . foregroundColor ( . secondary)
213+
214+ if signingInfo. certCommonName != " Unknown " || signingInfo. certExpiry != nil || signingInfo. provName != " Unknown " || signingInfo. provExpiry != nil {
215+ // Show certificate + provisioning info
216+ VStack ( spacing: 2 ) {
217+ Divider ( )
218+ HStack {
219+ VStack ( alignment: . leading, spacing: 4 ) {
220+ Text ( " Signing certificate " )
221+ . font ( . caption)
222+ . foregroundColor ( . secondary)
223+ Text ( signingInfo. certCommonName)
224+ . font ( . caption2)
225+ . lineLimit ( 1 )
226+ if let certExpiry = signingInfo. certExpiry {
227+ Text ( " Expires: \( dateFormatter. string ( from: certExpiry) ) " )
228+ . font ( . caption2)
229+ . foregroundColor ( . secondary)
230+ } else {
231+ Text ( " Expires: Unknown " )
232+ . font ( . caption2)
233+ . foregroundColor ( . secondary)
234+ }
235+ }
236+ Spacer ( )
237+ }
238+ . padding ( . top, 6 )
239+
240+ HStack {
241+ VStack ( alignment: . leading, spacing: 4 ) {
242+ Text ( " Provisioning profile " )
243+ . font ( . caption)
244+ . foregroundColor ( . secondary)
245+ Text ( signingInfo. provName)
246+ . font ( . caption2)
247+ . lineLimit ( 1 )
248+ if let provExpiry = signingInfo. provExpiry {
249+ Text ( " Expires: \( dateFormatter. string ( from: provExpiry) ) " )
250+ . font ( . caption2)
251+ . foregroundColor ( . secondary)
252+ } else {
253+ Text ( " Expires: Unknown " )
254+ . font ( . caption2)
255+ . foregroundColor ( . secondary)
256+ }
257+ }
258+ Spacer ( )
259+ }
260+
261+ // Show a match / mismatch indicator
262+ HStack {
263+ if let certExpiry = signingInfo. certExpiry, let provExpiry = signingInfo. provExpiry {
264+ if Calendar . current. compare ( certExpiry, to: provExpiry, toGranularity: . second) == . orderedSame {
265+ Label ( " Cert and provision match ✅ " , systemImage: " checkmark.shield " )
266+ . font ( . caption2)
267+ . foregroundColor ( . green)
268+ } else {
269+ Label ( " Cert / provision dates differ ⚠️ " , systemImage: " exclamationmark.triangle " )
270+ . font ( . caption2)
271+ . foregroundColor ( . yellow)
272+ }
273+ } else {
274+ // Can't fully compare
275+ Label ( " Comparison unavailable " , systemImage: " questionmark.circle " )
276+ . font ( . caption2)
277+ . foregroundColor ( . secondary)
278+ }
279+ Spacer ( )
280+ }
281+ . padding ( . top, 6 )
282+ }
283+ }
284+ }
81285 }
82286 . frame ( maxWidth: . infinity)
83287 . padding ( . vertical, 20 )
@@ -88,8 +292,17 @@ struct AboutView: View {
88292 CreditRow ( credit: c)
89293 }
90294 }
295+
296+ if let err = signingInfo. errorMessage {
297+ Section ( header: Text ( " Debug " ) ) {
298+ Text ( err)
299+ . font ( . footnote)
300+ . foregroundColor ( . red)
301+ }
302+ }
91303 }
92304 . listStyle ( InsetGroupedListStyle ( ) )
305+ . navigationTitle ( " About " )
93306 }
94307 }
95308}
@@ -144,5 +357,4 @@ struct AboutView_Previews: PreviewProvider {
144357 static var previews : some View {
145358 AboutView ( )
146359 }
147-
148360}
0 commit comments