diff --git a/Sources/WalletOrders/OrderBuilder.swift b/Sources/WalletOrders/OrderBuilder.swift index 1afc02e..51ac28a 100644 --- a/Sources/WalletOrders/OrderBuilder.swift +++ b/Sources/WalletOrders/OrderBuilder.swift @@ -38,35 +38,6 @@ public struct OrderBuilder: Sendable { self.openSSLURL = URL(fileURLWithPath: openSSLPath) } - private static func sourceFiles(in directory: URL) throws -> [String: Data] { - var files: [String: Data] = [:] - - let paths = try FileManager.default.subpathsOfDirectory(atPath: directory.path) - - for relativePath in paths { - let file = URL(fileURLWithPath: relativePath, relativeTo: directory) - guard !file.hasDirectoryPath else { - continue - } - - guard !(file.lastPathComponent == ".gitkeep" || file.lastPathComponent == ".DS_Store") else { - continue - } - - files[relativePath] = try Data(contentsOf: file) - } - - return files - } - - private func manifest(for sourceFiles: [String: Data]) throws -> Data { - let manifest = sourceFiles.mapValues { data in - SHA256.hash(data: data).map { "0\(String($0, radix: 16))".suffix(2) }.joined() - } - - return try self.encoder.encode(manifest) - } - private func signature(for manifest: Data) throws -> Data { // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that. if let pemPrivateKeyPassword { @@ -74,15 +45,15 @@ public struct OrderBuilder: Sendable { throw WalletOrdersError.noOpenSSLExecutable } - let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: dir) } + let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } - let manifestURL = dir.appendingPathComponent(Self.manifestFileName) - let wwdrURL = dir.appendingPathComponent("wwdr.pem") - let certificateURL = dir.appendingPathComponent("certificate.pem") - let privateKeyURL = dir.appendingPathComponent("private.pem") - let signatureURL = dir.appendingPathComponent(Self.signatureFileName) + let manifestURL = tempDir.appendingPathComponent(Self.manifestFileName) + let wwdrURL = tempDir.appendingPathComponent("wwdr.pem") + let certificateURL = tempDir.appendingPathComponent("certificate.pem") + let privateKeyURL = tempDir.appendingPathComponent("private.pem") + let signatureURL = tempDir.appendingPathComponent(Self.signatureFileName) try manifest.write(to: manifestURL) try self.pemWWDRCertificate.write(to: wwdrURL, atomically: true, encoding: .utf8) @@ -90,7 +61,7 @@ public struct OrderBuilder: Sendable { try self.pemPrivateKey.write(to: privateKeyURL, atomically: true, encoding: .utf8) let process = Process() - process.currentDirectoryURL = dir + process.currentDirectoryURL = tempDir process.executableURL = self.openSSLURL process.arguments = [ "smime", "-binary", "-sign", @@ -149,16 +120,32 @@ public struct OrderBuilder: Sendable { try orderJSON.write(to: tempDir.appendingPathComponent("order.json")) archiveFiles.append(ArchiveFile(filename: "order.json", data: orderJSON)) - let sourceFiles = try Self.sourceFiles(in: tempDir) + let sourceFilesPaths = try FileManager.default.subpathsOfDirectory(atPath: tempDir.path) + + var manifestJSON: [String: String] = [:] - let manifest = try self.manifest(for: sourceFiles) - archiveFiles.append(ArchiveFile(filename: Self.manifestFileName, data: manifest)) - try archiveFiles.append(ArchiveFile(filename: Self.signatureFileName, data: self.signature(for: manifest))) + for relativePath in sourceFilesPaths { + let fileURL = URL(fileURLWithPath: relativePath, relativeTo: tempDir) + + guard !fileURL.hasDirectoryPath else { + continue + } - for file in sourceFiles { - archiveFiles.append(ArchiveFile(filename: file.key, data: file.value)) + guard !(fileURL.lastPathComponent == ".gitkeep" || fileURL.lastPathComponent == ".DS_Store") else { + continue + } + + let fileData = try Data(contentsOf: fileURL) + + archiveFiles.append(ArchiveFile(filename: relativePath, data: fileData)) + + manifestJSON[relativePath] = SHA256.hash(data: fileData).map { "0\(String($0, radix: 16))".suffix(2) }.joined() } + let manifestData = try self.encoder.encode(manifestJSON) + archiveFiles.append(ArchiveFile(filename: Self.manifestFileName, data: manifestData)) + try archiveFiles.append(ArchiveFile(filename: Self.signatureFileName, data: self.signature(for: manifestData))) + let zipFile = tempDir.appendingPathComponent("\(UUID().uuidString).order") try Zip.zipData(archiveFiles: archiveFiles, zipFilePath: zipFile) return try Data(contentsOf: zipFile) diff --git a/Sources/WalletPasses/PassBuilder.swift b/Sources/WalletPasses/PassBuilder.swift index 7cb30cd..1357bb2 100644 --- a/Sources/WalletPasses/PassBuilder.swift +++ b/Sources/WalletPasses/PassBuilder.swift @@ -38,84 +38,35 @@ public struct PassBuilder: Sendable { self.openSSLURL = URL(fileURLWithPath: openSSLPath) } - private static func sourceFiles(in directory: URL, isPersonalized: Bool = false) throws -> [String: Data] { - var files: [String: Data] = [:] - - let paths = try FileManager.default.subpathsOfDirectory(atPath: directory.path) - - if isPersonalized { - guard - paths.contains("personalizationLogo.png") - || paths.contains("personalizationLogo@1x.png") - || paths.contains("personalizationLogo@2x.png") - || paths.contains("personalizationLogo@3x.png") - else { - throw WalletPassesError.noPersonalizationLogo - } - } - - guard - paths.contains("icon.png") - || paths.contains("icon@1x.png") - || paths.contains("icon@2x.png") - || paths.contains("icon@3x.png") - else { - throw WalletPassesError.noIcon - } - - for relativePath in paths { - let file = URL(fileURLWithPath: relativePath, relativeTo: directory) - guard !file.hasDirectoryPath else { - continue - } - - guard !(file.lastPathComponent == ".gitkeep" || file.lastPathComponent == ".DS_Store") else { - continue - } - - files[relativePath] = try Data(contentsOf: file) - } - - return files - } - - private func manifest(for sourceFiles: [String: Data]) throws -> Data { - let manifest = sourceFiles.mapValues { data in - Insecure.SHA1.hash(data: data).map { "0\(String($0, radix: 16))".suffix(2) }.joined() - } - - return try self.encoder.encode(manifest) - } - - /// Generates a signature for a given manifest or personalization token. + /// Generates a signature for a given personalization token. /// - /// - Parameter manifest: The manifest or personalization token data to sign. + /// - Parameter data: The personalization token data to sign. /// /// - Returns: The generated signature as `Data`. - public func signature(for manifest: Data) throws -> Data { + public func signature(for data: Data) throws -> Data { // Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that. if let pemPrivateKeyPassword { guard FileManager.default.fileExists(atPath: self.openSSLURL.path) else { throw WalletPassesError.noOpenSSLExecutable } - let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: dir) } + let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } - let manifestURL = dir.appendingPathComponent(Self.manifestFileName) - let wwdrURL = dir.appendingPathComponent("wwdr.pem") - let certificateURL = dir.appendingPathComponent("certificate.pem") - let privateKeyURL = dir.appendingPathComponent("private.pem") - let signatureURL = dir.appendingPathComponent(Self.signatureFileName) + let manifestURL = tempDir.appendingPathComponent(Self.manifestFileName) + let wwdrURL = tempDir.appendingPathComponent("wwdr.pem") + let certificateURL = tempDir.appendingPathComponent("certificate.pem") + let privateKeyURL = tempDir.appendingPathComponent("private.pem") + let signatureURL = tempDir.appendingPathComponent(Self.signatureFileName) - try manifest.write(to: manifestURL) + try data.write(to: manifestURL) try self.pemWWDRCertificate.write(to: wwdrURL, atomically: true, encoding: .utf8) try self.pemCertificate.write(to: certificateURL, atomically: true, encoding: .utf8) try self.pemPrivateKey.write(to: privateKeyURL, atomically: true, encoding: .utf8) let process = Process() - process.currentDirectoryURL = dir + process.currentDirectoryURL = tempDir process.executableURL = self.openSSLURL process.arguments = [ "smime", "-binary", "-sign", @@ -133,7 +84,7 @@ public struct PassBuilder: Sendable { return try Data(contentsOf: signatureURL) } else { let signature = try CMS.sign( - manifest, + data, signatureAlgorithm: .sha256WithRSAEncryption, additionalIntermediateCertificates: [ Certificate(pemEncoded: self.pemWWDRCertificate) @@ -183,16 +134,52 @@ public struct PassBuilder: Sendable { archiveFiles.append(ArchiveFile(filename: "personalization.json", data: personalizationJSONData)) } - let sourceFiles = try Self.sourceFiles(in: tempDir, isPersonalized: personalization != nil) + let sourceFilesPaths = try FileManager.default.subpathsOfDirectory(atPath: tempDir.path) + + if personalization != nil { + guard + sourceFilesPaths.contains("personalizationLogo.png") + || sourceFilesPaths.contains("personalizationLogo@1x.png") + || sourceFilesPaths.contains("personalizationLogo@2x.png") + || sourceFilesPaths.contains("personalizationLogo@3x.png") + else { + throw WalletPassesError.noPersonalizationLogo + } + } + + guard + sourceFilesPaths.contains("icon.png") + || sourceFilesPaths.contains("icon@1x.png") + || sourceFilesPaths.contains("icon@2x.png") + || sourceFilesPaths.contains("icon@3x.png") + else { + throw WalletPassesError.noIcon + } + + var manifestJSON: [String: String] = [:] + + for relativePath in sourceFilesPaths { + let fileURL = URL(fileURLWithPath: relativePath, relativeTo: tempDir) + + guard !fileURL.hasDirectoryPath else { + continue + } + + guard !(fileURL.lastPathComponent == ".gitkeep" || fileURL.lastPathComponent == ".DS_Store") else { + continue + } + + let fileData = try Data(contentsOf: fileURL) - let manifest = try self.manifest(for: sourceFiles) - archiveFiles.append(ArchiveFile(filename: Self.manifestFileName, data: manifest)) - try archiveFiles.append(ArchiveFile(filename: Self.signatureFileName, data: self.signature(for: manifest))) + archiveFiles.append(ArchiveFile(filename: relativePath, data: fileData)) - for file in sourceFiles { - archiveFiles.append(ArchiveFile(filename: file.key, data: file.value)) + manifestJSON[relativePath] = Insecure.SHA1.hash(data: fileData).map { "0\(String($0, radix: 16))".suffix(2) }.joined() } + let manifestData = try self.encoder.encode(manifestJSON) + archiveFiles.append(ArchiveFile(filename: Self.manifestFileName, data: manifestData)) + try archiveFiles.append(ArchiveFile(filename: Self.signatureFileName, data: self.signature(for: manifestData))) + let zipFile = tempDir.appendingPathComponent("\(UUID().uuidString).pkpass") try Zip.zipData(archiveFiles: archiveFiles, zipFilePath: zipFile) return try Data(contentsOf: zipFile)