diff --git a/.gitignore b/.gitignore index a482f37..32e80db 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ /.build /Packages /*.xcodeproj -*.mobileprovision +Tests/SwiftyProvisioningProfileTests/Resources/* diff --git a/Sources/SwiftyProvisioningProfile/Model/Certificate.swift b/Sources/SwiftyProvisioningProfile/Model/Certificate.swift new file mode 100644 index 0000000..79ba3e5 --- /dev/null +++ b/Sources/SwiftyProvisioningProfile/Model/Certificate.swift @@ -0,0 +1,48 @@ +// +// Certificate.swift +// SwiftyProvisioningProfile +// +// Created by Sherlock, James on 20/11/2018. +// + +import Foundation + +public struct Certificate: Encodable, Equatable { + + public enum InitError: Error { + case failedToFindValue(key: String) + case failedToCastValue(expected: String, actual: String) + } + + public let notValidBefore: Date + public let notValidAfter: Date + + init(results: [CFString: Any]) throws { + notValidBefore = try Certificate.getValue(for: kSecOIDX509V1ValidityNotBefore, from: results) + notValidAfter = try Certificate.getValue(for: kSecOIDX509V1ValidityNotAfter, from: results) + // TODO: Add more values to this + } + + static func getValue(for key: CFString, from values: [CFString: Any]) throws -> T { + let node = values[key] as? [CFString: Any] + + guard let rawValue = node?[kSecPropertyKeyValue] else { + throw InitError.failedToFindValue(key: key as String) + } + + if T.self is Date.Type { + if let value = rawValue as? TimeInterval { + // Force unwrap here is fine as we've validated the type above + return Date(timeIntervalSinceReferenceDate: value) as! T + } + } + + guard let value = rawValue as? T else { + let type = (node?[kSecPropertyKeyType] as? String) ?? String(describing: rawValue) + throw InitError.failedToCastValue(expected: String(describing: T.self), actual: type) + } + + return value + } + +} diff --git a/Sources/SwiftyProvisioningProfile/Model/DeveloperCertificate.swift b/Sources/SwiftyProvisioningProfile/Model/DeveloperCertificate.swift index f2d7e87..95d9d49 100644 --- a/Sources/SwiftyProvisioningProfile/Model/DeveloperCertificate.swift +++ b/Sources/SwiftyProvisioningProfile/Model/DeveloperCertificate.swift @@ -10,14 +10,14 @@ import Foundation public struct DeveloperCertificate: Codable, Equatable { public let data: Data - public let certificate: SecureCertificate? + public let certificate: Certificate? // MARK: - Codable public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() data = try container.decode(Data.self) - certificate = try? SecureCertificate(base64EncodedData: data) + certificate = try? Certificate.parse(from: data) } public func encode(to encoder: Encoder) throws { diff --git a/Sources/SwiftyProvisioningProfile/Model/SecureCertificate.swift b/Sources/SwiftyProvisioningProfile/Model/SecureCertificate.swift deleted file mode 100644 index df1d118..0000000 --- a/Sources/SwiftyProvisioningProfile/Model/SecureCertificate.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// SecureCertificate.swift -// SwiftyProvisioningProfile -// -// Created by Sherlock, James on 13/05/2018. -// - -import Foundation - -public struct SecureCertificate: CustomStringConvertible, Equatable { - - public enum CertificateError: Error { - case failedToCreate - case failedToObtainSummary - case failedToObtainValues - } - - public let summary: String - public let expiryDate: Date? - - public init(base64EncodedData: Data) throws { - - // Create Certificate - - guard let certificate = SecCertificateCreateWithData(nil, base64EncodedData as CFData) else { - throw CertificateError.failedToCreate - } - - // Error - - var error: Unmanaged? - - func checkError() throws { - if let error = error { - throw error.takeUnretainedValue() - } - } - - // Summary - - guard let summary = SecCertificateCopySubjectSummary(certificate) else { - throw CertificateError.failedToObtainSummary - } - - self.summary = summary as String - - // Values (Expiry) - - let valuesKeys = [ - kSecOIDInvalidityDate - ] as CFArray - - let values = SecCertificateCopyValues(certificate, valuesKeys, &error) - try checkError() - - guard let dictionary = values as? Dictionary else { - throw CertificateError.failedToObtainValues - } - - let expiryDateDictionary = dictionary[kSecOIDInvalidityDate] as? [String: Any] - expiryDate = expiryDateDictionary?["value"] as? Date - - } - - public var description: String { - return "\(summary), Expires: \(expiryDate?.description ?? "No Expiry Date")" - } - -} diff --git a/Sources/SwiftyProvisioningProfile/SwiftyCertificate.swift b/Sources/SwiftyProvisioningProfile/SwiftyCertificate.swift new file mode 100644 index 0000000..cfa2e93 --- /dev/null +++ b/Sources/SwiftyProvisioningProfile/SwiftyCertificate.swift @@ -0,0 +1,44 @@ +// +// SwiftyCertificate.swift +// SwiftyProvisioningProfile +// +// Created by Sherlock, James on 20/11/2018. +// + +import Foundation +import Security + +public extension Certificate { + + public enum ParseError: Error { + case failedToCreateCertificate + case failedToCreateTrust + case failedToExtractValues + } + + public static func parse(from data: Data) throws -> Certificate { + let certificate = try getSecCertificate(data: data) + + var error: Unmanaged? + let values = SecCertificateCopyValues(certificate, nil, &error) + + if let error = error { + throw error.takeRetainedValue() as Error + } + + guard let valuesDict = values as? [CFString: Any] else { + throw ParseError.failedToExtractValues + } + + return try Certificate(results: valuesDict) + } + + private static func getSecCertificate(data: Data) throws -> SecCertificate { + guard let certificate = SecCertificateCreateWithData(kCFAllocatorDefault, data as CFData) else { + throw ParseError.failedToCreateCertificate + } + + return certificate + } + +} diff --git a/Tests/SwiftyProvisioningProfileTests/SwiftyProvisioningProfileTests.swift b/Tests/SwiftyProvisioningProfileTests/SwiftyProvisioningProfileTests.swift index 1f5e980..7ed609e 100644 --- a/Tests/SwiftyProvisioningProfileTests/SwiftyProvisioningProfileTests.swift +++ b/Tests/SwiftyProvisioningProfileTests/SwiftyProvisioningProfileTests.swift @@ -14,7 +14,7 @@ class SwiftyProvisioningProfileTests: XCTestCase { return Bundle(path: "\(currentBundle.bundlePath)/../../../../Tests/SwiftyProvisioningProfileTests/Resources") }() - lazy var testProfileURL: [URL] = { + lazy var testProfileURLs: [URL] = { guard let bundle = resourceBundle else { fatalError("Tests are being run through Xcode, or the project structure no longer matches up") } @@ -26,23 +26,52 @@ class SwiftyProvisioningProfileTests: XCTestCase { return urls }() + lazy var testCertificateURLs: [URL] = { + guard let bundle = resourceBundle else { + fatalError("Tests are being run through Xcode, or the project structure no longer matches up") + } + + guard let urls = bundle.urls(forResourcesWithExtension: "cer", subdirectory: nil) else { + fatalError("No `cer` files found in `Tests/SwiftyProvisioningProfileTests/Resources`") + } + + return urls + }() + func testParseIOS() { do { - for url in testProfileURL { + for url in testProfileURLs { let data = try Data(contentsOf: url) let profile = try ProvisioningProfile.parse(from: data) - print(profile.developerCertificates.flatMap({ $0.certificate?.description }).joined(separator: "\n")) + print(profile.name) } - // TODO: Create or find a simple & usable profile and wrtie actual tests for it + // TODO: Create or find a simple & usable profile and write actual tests for it } catch { XCTFail(String(describing: error)) } } + func testParseCertificate() { + + do { + for url in testCertificateURLs { + let data = try Data(contentsOf: url) + let certificate = try Certificate.parse(from: data) + + print(certificate) + } + + // TODO: Create or find a simple & usable certificate and write actual tests for it + } catch { + XCTFail(String(describing: error)) + } + + } + func testParseMAC() { // TODO @@ -52,5 +81,6 @@ class SwiftyProvisioningProfileTests: XCTestCase { static var allTests = [ ("testParseIOS", testParseIOS), ("testParseMAC", testParseMAC), + ("testParseCertificate", testParseCertificate), ] }