11import Crypto
22import Foundation
33@_spi ( CMS) import X509
4- import Zip
4+ import ZipArchive
55
66/// A tool that generates pass content bundles.
7+ ///
8+ /// > Warning: You can only sign passes with the same pass type identifier of the certificates used to initialize the ``PassBuilder``.
79public struct PassBuilder : Sendable {
810 private let pemWWDRCertificate : String
911 private let pemCertificate : String
@@ -37,7 +39,7 @@ public struct PassBuilder: Sendable {
3739 self . pemCertificate = pemCertificate
3840 self . pemPrivateKey = pemPrivateKey
3941 self . pemPrivateKeyPassword = pemPrivateKeyPassword
40- self . openSSLURL = URL ( fileURLWithPath : openSSLPath)
42+ self . openSSLURL = URL ( filePath : openSSLPath)
4143 }
4244
4345 /// Generates a signature for a given personalization token.
@@ -50,19 +52,19 @@ public struct PassBuilder: Sendable {
5052 public func signature( for data: Data ) throws -> Data {
5153 // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that.
5254 if let pemPrivateKeyPassword {
53- guard FileManager . default. fileExists ( atPath: self . openSSLURL. path) else {
55+ guard FileManager . default. fileExists ( atPath: self . openSSLURL. path ( ) ) else {
5456 throw WalletPassesError . noOpenSSLExecutable
5557 }
5658
57- let tempDir = FileManager . default. temporaryDirectory. appendingPathComponent ( UUID ( ) . uuidString, isDirectory : true )
59+ let tempDir = FileManager . default. temporaryDirectory. appending ( path : UUID ( ) . uuidString, directoryHint : . isDirectory )
5860 try FileManager . default. createDirectory ( at: tempDir, withIntermediateDirectories: true )
5961 defer { try ? FileManager . default. removeItem ( at: tempDir) }
6062
61- let manifestURL = tempDir. appendingPathComponent ( Self . manifestFileName)
62- let wwdrURL = tempDir. appendingPathComponent ( " wwdr.pem " )
63- let certificateURL = tempDir. appendingPathComponent ( " certificate.pem " )
64- let privateKeyURL = tempDir. appendingPathComponent ( " private.pem " )
65- let signatureURL = tempDir. appendingPathComponent ( Self . signatureFileName)
63+ let manifestURL = tempDir. appending ( path : Self . manifestFileName)
64+ let wwdrURL = tempDir. appending ( path : " wwdr.pem " )
65+ let certificateURL = tempDir. appending ( path : " certificate.pem " )
66+ let privateKeyURL = tempDir. appending ( path : " private.pem " )
67+ let signatureURL = tempDir. appending ( path : Self . signatureFileName)
6668
6769 try data. write ( to: manifestURL)
6870 try self . pemWWDRCertificate. write ( to: wwdrURL, atomically: true , encoding: . utf8)
@@ -74,11 +76,11 @@ public struct PassBuilder: Sendable {
7476 process. executableURL = self . openSSLURL
7577 process. arguments = [
7678 " smime " , " -binary " , " -sign " ,
77- " -certfile " , wwdrURL. path,
78- " -signer " , certificateURL. path,
79- " -inkey " , privateKeyURL. path,
80- " -in " , manifestURL. path,
81- " -out " , signatureURL. path,
79+ " -certfile " , wwdrURL. path ( ) ,
80+ " -signer " , certificateURL. path ( ) ,
81+ " -inkey " , privateKeyURL. path ( ) ,
82+ " -in " , manifestURL. path ( ) ,
83+ " -out " , signatureURL. path ( ) ,
8284 " -outform " , " DER " ,
8385 " -passin " , " pass: \( pemPrivateKeyPassword) " ,
8486 ]
@@ -114,31 +116,25 @@ public struct PassBuilder: Sendable {
114116 sourceFilesDirectoryPath: String ,
115117 personalization: PersonalizationJSON ? = nil
116118 ) throws -> Data {
117- let filesDirectory = URL ( fileURLWithPath: sourceFilesDirectoryPath, isDirectory: true )
118- guard
119- ( try ? filesDirectory. resourceValues ( forKeys: [ . isDirectoryKey] ) . isDirectory) ?? false
120- else {
119+ let filesDirectory = URL ( filePath: sourceFilesDirectoryPath, directoryHint: . isDirectory)
120+ guard ( try ? filesDirectory. resourceValues ( forKeys: [ . isDirectoryKey] ) . isDirectory) ?? false else {
121121 throw WalletPassesError . noSourceFiles
122122 }
123123
124- let tempDir = FileManager . default. temporaryDirectory. appendingPathComponent ( UUID ( ) . uuidString, isDirectory: true )
125- try FileManager . default. copyItem ( at: filesDirectory, to: tempDir)
126- defer { try ? FileManager . default. removeItem ( at: tempDir) }
127-
128- var archiveFiles : [ ArchiveFile ] = [ ]
124+ var archiveFiles : [ String : Data ] = [ : ]
125+ var manifestJSON : [ String : String ] = [ : ]
129126
130127 let passJSON = try self . encoder. encode ( pass)
131- try passJSON . write ( to : tempDir . appendingPathComponent ( " pass.json " ) )
132- archiveFiles . append ( ArchiveFile ( filename : " pass.json " , data : passJSON) )
128+ archiveFiles [ " pass.json " ] = passJSON
129+ manifestJSON [ " pass.json " ] = passJSON. manifestHash
133130
134- // Pass Personalization
135131 if let personalization {
136132 let personalizationJSONData = try self . encoder. encode ( personalization)
137- try personalizationJSONData . write ( to : tempDir . appendingPathComponent ( " personalization.json " ) )
138- archiveFiles . append ( ArchiveFile ( filename : " personalization.json " , data : personalizationJSONData) )
133+ archiveFiles [ " personalization.json " ] = personalizationJSONData
134+ manifestJSON [ " personalization.json " ] = personalizationJSONData. manifestHash
139135 }
140136
141- let sourceFilesPaths = try FileManager . default. subpathsOfDirectory ( atPath: tempDir . path)
137+ let sourceFilesPaths = try FileManager . default. subpathsOfDirectory ( atPath: filesDirectory . path ( ) )
142138
143139 if personalization != nil {
144140 guard
@@ -160,32 +156,30 @@ public struct PassBuilder: Sendable {
160156 throw WalletPassesError . noIcon
161157 }
162158
163- var manifestJSON : [ String : String ] = [ : ]
164-
165159 for relativePath in sourceFilesPaths {
166- let fileURL = URL ( fileURLWithPath: relativePath, relativeTo: tempDir)
167-
168- guard !fileURL. hasDirectoryPath else {
169- continue
170- }
171-
172- guard !( fileURL. lastPathComponent == " .gitkeep " || fileURL. lastPathComponent == " .DS_Store " ) else {
173- continue
174- }
160+ let fileURL = URL ( filePath: relativePath, directoryHint: . checkFileSystem, relativeTo: filesDirectory)
161+ guard !fileURL. hasDirectoryPath else { continue }
162+ if fileURL. lastPathComponent == " .gitkeep " || fileURL. lastPathComponent == " .DS_Store " { continue }
175163
176164 let fileData = try Data ( contentsOf: fileURL)
177-
178- archiveFiles. append ( ArchiveFile ( filename: relativePath, data: fileData) )
179-
180- manifestJSON [ relativePath] = Insecure . SHA1. hash ( data: fileData) . map { " 0 \( String ( $0, radix: 16 ) ) " . suffix ( 2 ) } . joined ( )
165+ archiveFiles [ relativePath] = fileData
166+ manifestJSON [ relativePath] = fileData. manifestHash
181167 }
182168
183169 let manifestData = try self . encoder. encode ( manifestJSON)
184- archiveFiles. append ( ArchiveFile ( filename: Self . manifestFileName, data: manifestData) )
185- try archiveFiles. append ( ArchiveFile ( filename: Self . signatureFileName, data: self . signature ( for: manifestData) ) )
170+ archiveFiles [ Self . manifestFileName] = manifestData
171+ try archiveFiles [ Self . signatureFileName] = self . signature ( for: manifestData)
172+
173+ let writer = ZipArchiveWriter ( )
174+ for (filename, contents) in archiveFiles {
175+ try writer. writeFile ( filename: filename, contents: Array ( contents) )
176+ }
177+ return try Data ( writer. finalizeBuffer ( ) )
178+ }
179+ }
186180
187- let zipFile = tempDir . appendingPathComponent ( " \( UUID ( ) . uuidString ) .pkpass " )
188- try Zip . zipData ( archiveFiles : archiveFiles , zipFilePath : zipFile )
189- return try Data ( contentsOf : zipFile )
181+ extension Data {
182+ fileprivate var manifestHash : String {
183+ Insecure . SHA1 . hash ( data : self ) . map { " 0 \( String ( $0 , radix : 16 ) ) " . suffix ( 2 ) } . joined ( )
190184 }
191185}
0 commit comments