├── .gitignore ├── .spi.yml ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── SwiftyProvisioningProfile │ ├── Model │ ├── Certificate.swift │ ├── DeveloperCertificate.swift │ ├── PropertyListDictionaryValue.swift │ └── ProvisioningProfile.swift │ ├── SwiftyCMSDecoder.swift │ ├── SwiftyCertificate.swift │ └── SwiftyProvisioningProfile.swift └── Tests ├── LinuxMain.swift └── SwiftyProvisioningProfileTests ├── Resources └── .gitkeep └── SwiftyProvisioningProfileTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | Tests/SwiftyProvisioningProfileTests/Resources/* 6 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - platform: macos-spm 5 | documentation_targets: [SwiftyProvisioningProfile] 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 James Sherlock https://twitter.com/JamesSherlouk/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftyProvisioningProfile", 8 | platforms: [.macOS(.v10_13)], 9 | products: [ 10 | .library( 11 | name: "SwiftyProvisioningProfile", 12 | targets: ["SwiftyProvisioningProfile"]), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "SwiftyProvisioningProfile", 17 | dependencies: []), 18 | .testTarget( 19 | name: "SwiftyProvisioningProfileTests", 20 | dependencies: ["SwiftyProvisioningProfile"]), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftyProvisioningProfile 2 | 3 | This library provides a way to decode a `.mobileprovision` file into a Swift model. 4 | 5 | #### Installation 6 | 7 | The recommended installation is via Swift Package Manager, you'll want to update your `Package.swift` with a new dependency: 8 | 9 | ```swift 10 | import PackageDescription 11 | 12 | let package = Package( 13 | name: "YourAwesomeSoftware", 14 | dependencies: [ 15 | .package(url: "https://github.com/Sherlouk/SwiftProvisioningProfile.git", from: "1.0.0") 16 | ] 17 | ) 18 | ``` 19 | 20 | There are open issues to handle CocoaPods and Carthage installation, if people want it then I'm willing to support it! 21 | 22 | #### Usage 23 | 24 | ```swift 25 | // 1. Import the library 26 | import SwiftyProvisioningProfile 27 | 28 | // 2. Load your provisioning profile's file data 29 | let profileData = try Data(contentsOf: ...) 30 | 31 | // 3. Parse it 32 | let profile = try ProvisioningProfile.parse(from: profileData) 33 | 34 | // 4. Use it 35 | print(profile.uuid) 36 | ``` 37 | -------------------------------------------------------------------------------- /Sources/SwiftyProvisioningProfile/Model/Certificate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Certificate.swift 3 | // SwiftyProvisioningProfile 4 | // 5 | // Created by Sherlock, James on 20/11/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Certificate: Encodable, Equatable { 11 | 12 | public enum InitError: Error { 13 | case failedToFindValue(key: String) 14 | case failedToCastValue(expected: String, actual: String) 15 | case failedToFindLabel(label: String) 16 | } 17 | 18 | public let notValidBefore: Date 19 | public let notValidAfter: Date 20 | public let serialNumber: String 21 | 22 | public let issuerCommonName: String 23 | public let issuerCountryName: String 24 | public let issuerOrgName: String 25 | public let issuerOrgUnit: String 26 | 27 | public let commonName: String? 28 | public let countryName: String 29 | public let orgName: String? 30 | public let orgUnit: String 31 | 32 | init(results: [CFString: Any], commonName: String?) throws { 33 | self.commonName = commonName 34 | 35 | notValidBefore = try Certificate.getValue(for: kSecOIDX509V1ValidityNotBefore, from: results) 36 | notValidAfter = try Certificate.getValue(for: kSecOIDX509V1ValidityNotAfter, from: results) 37 | serialNumber = try Certificate.getValue(for: kSecOIDX509V1SerialNumber, from: results) 38 | 39 | let issuerName: [[CFString: Any]] = try Certificate.getValue(for: kSecOIDX509V1IssuerName, from: results) 40 | issuerCommonName = try Certificate.getValue(for: kSecOIDCommonName, fromDict: issuerName) 41 | issuerCountryName = try Certificate.getValue(for: kSecOIDCountryName, fromDict: issuerName) 42 | issuerOrgName = try Certificate.getValue(for: kSecOIDOrganizationName, fromDict: issuerName) 43 | issuerOrgUnit = try Certificate.getValue(for: kSecOIDOrganizationalUnitName, fromDict: issuerName) 44 | 45 | let subjectName: [[CFString: Any]] = try Certificate.getValue(for: kSecOIDX509V1SubjectName, from: results) 46 | countryName = try Certificate.getValue(for: kSecOIDCountryName, fromDict: subjectName) 47 | orgName = try? Certificate.getValue(for: kSecOIDOrganizationName, fromDict: subjectName) 48 | orgUnit = try Certificate.getValue(for: kSecOIDOrganizationalUnitName, fromDict: subjectName) 49 | } 50 | 51 | static func getValue(for key: CFString, from values: [CFString: Any]) throws -> T { 52 | let node = values[key] as? [CFString: Any] 53 | 54 | guard let rawValue = node?[kSecPropertyKeyValue] else { 55 | throw InitError.failedToFindValue(key: key as String) 56 | } 57 | 58 | if T.self is Date.Type { 59 | if let value = rawValue as? TimeInterval { 60 | // Force unwrap here is fine as we've validated the type above 61 | return Date(timeIntervalSinceReferenceDate: value) as! T 62 | } 63 | } 64 | 65 | guard let value = rawValue as? T else { 66 | let type = (node?[kSecPropertyKeyType] as? String) ?? String(describing: rawValue) 67 | throw InitError.failedToCastValue(expected: String(describing: T.self), actual: type) 68 | } 69 | 70 | return value 71 | } 72 | 73 | static func getValue(for key: CFString, fromDict values: [[CFString: Any]]) throws -> T { 74 | 75 | guard let results = values.first(where: { ($0[kSecPropertyKeyLabel] as? String) == (key as String) }) else { 76 | throw InitError.failedToFindLabel(label: key as String) 77 | } 78 | 79 | guard let rawValue = results[kSecPropertyKeyValue] else { 80 | throw InitError.failedToFindValue(key: key as String) 81 | } 82 | 83 | guard let value = rawValue as? T else { 84 | let type = (results[kSecPropertyKeyType] as? String) ?? String(describing: rawValue) 85 | throw InitError.failedToCastValue(expected: String(describing: T.self), actual: type) 86 | } 87 | 88 | return value 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Sources/SwiftyProvisioningProfile/Model/DeveloperCertificate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeveloperCertificate.swift 3 | // SwiftyProvisioningProfile 4 | // 5 | // Created by Sherlock, James on 13/05/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct DeveloperCertificate: Codable, Equatable { 11 | 12 | public let data: Data 13 | public let certificate: Certificate? 14 | 15 | // MARK: - Codable 16 | 17 | public init(from decoder: Decoder) throws { 18 | let container = try decoder.singleValueContainer() 19 | data = try container.decode(Data.self) 20 | certificate = try? Certificate.parse(from: data) 21 | } 22 | 23 | public func encode(to encoder: Encoder) throws { 24 | var container = encoder.singleValueContainer() 25 | try container.encode(data) 26 | } 27 | 28 | // MARK: - Convenience 29 | 30 | public var base64Encoded: String { 31 | return data.base64EncodedString() 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/SwiftyProvisioningProfile/Model/PropertyListDictionaryValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entitlement.swift 3 | // SwiftyProvisioningProfile 4 | // 5 | // Created by Sherlock, James on 13/05/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Enum describing a property lists value inside of a dictionary 11 | public enum PropertyListDictionaryValue: Codable, Equatable { 12 | 13 | case string(String) 14 | case bool(Bool) 15 | case array([PropertyListDictionaryValue]) 16 | case unknown 17 | 18 | public init(from decoder: Decoder) throws { 19 | 20 | let container = try decoder.singleValueContainer() 21 | 22 | if let string = try? container.decode(String.self) { 23 | self = .string(string) 24 | } else if let bool = try? container.decode(Bool.self) { 25 | self = .bool(bool) 26 | } else if let array = try? container.decode([PropertyListDictionaryValue].self) { 27 | self = .array(array) 28 | } else { 29 | self = .unknown 30 | } 31 | 32 | } 33 | 34 | public func encode(to encoder: Encoder) throws { 35 | 36 | var container = encoder.singleValueContainer() 37 | 38 | switch self { 39 | case .string(let string): 40 | try container.encode(string) 41 | case .bool(let bool): 42 | try container.encode(bool) 43 | case .array(let array): 44 | try container.encode(array) 45 | case .unknown: 46 | break 47 | } 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SwiftyProvisioningProfile/Model/ProvisioningProfile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by James Sherlock on 10/04/2018. 3 | // 4 | 5 | import Foundation 6 | 7 | /// Structure describing a Provisioning Profile and its contents 8 | public struct ProvisioningProfile: Codable, Equatable { 9 | 10 | enum CodingKeys: String, CodingKey { 11 | case appIdName = "AppIDName" 12 | case applicationIdentifierPrefixs = "ApplicationIdentifierPrefix" 13 | case creationDate = "CreationDate" 14 | case platforms = "Platform" 15 | case developerCertificates = "DeveloperCertificates" 16 | case entitlements = "Entitlements" 17 | case expirationDate = "ExpirationDate" 18 | case name = "Name" 19 | case provisionedDevices = "ProvisionedDevices" 20 | case provisionsAllDevices = "ProvisionsAllDevices" 21 | case teamIdentifiers = "TeamIdentifier" 22 | case teamName = "TeamName" 23 | case timeToLive = "TimeToLive" 24 | case uuid = "UUID" 25 | case version = "Version" 26 | } 27 | 28 | /// The name you gave your App ID in the provisioning portal 29 | public var appIdName: String 30 | 31 | /// The App ID prefix (or Bundle Seed ID) generated when you create a new App ID 32 | public var applicationIdentifierPrefixs: [String] 33 | 34 | /// The date in which this profile was created 35 | public var creationDate: Date 36 | 37 | /// The platforms in which this profile is compatible with 38 | public var platforms: [String] 39 | 40 | /// The array of Base64 encoded developer certificates 41 | public var developerCertificates: [DeveloperCertificate] 42 | 43 | /// The key value pair of entitlements associated with this profile 44 | public var entitlements: [String: PropertyListDictionaryValue] 45 | 46 | /// The date in which this profile will expire 47 | public var expirationDate: Date 48 | 49 | /// The name of the profile you provided in the provisioning portal 50 | public var name: String 51 | 52 | /// An array of device UUIDs that are provisioned on this profile 53 | public var provisionedDevices: [String]? 54 | 55 | /// A key indicating whether the profile provisions all devices. This is present when profile is generated for enterprise distribution. 56 | public var provisionsAllDevices: Bool? 57 | 58 | /// An array of team identifier of which this profile belongs to 59 | public var teamIdentifiers: [String] 60 | 61 | /// The name of the team in which this profile belongs to 62 | public var teamName: String 63 | 64 | /// The number of days that this profile is valid for. Usually one year (365) 65 | public var timeToLive: Int 66 | 67 | /// The profile's unique identifier, usually used to reference the profile from within Xcode 68 | public var uuid: String 69 | 70 | /// The provisioning profiles version number, currently set to 1. 71 | public var version: Int 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/SwiftyProvisioningProfile/SwiftyCMSDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by James Sherlock on 10/04/2018. 3 | // 4 | 5 | import Foundation 6 | 7 | /// Swift wrapper around Apple's Security `CMSDecoder` class 8 | final class SwiftyCMSDecoder { 9 | 10 | var decoder: CMSDecoder 11 | 12 | /// Initialises a new `SwiftyCMSDecoder` which in turn creates a new `CMSDecoder` 13 | init?() { 14 | var newDecoder: CMSDecoder? 15 | CMSDecoderCreate(&newDecoder) 16 | 17 | guard let decoder = newDecoder else { 18 | return nil 19 | } 20 | 21 | self.decoder = decoder 22 | } 23 | 24 | /// Feed raw bytes of the message to be decoded into the decoder. Can be called multiple times. 25 | /// 26 | /// - Parameter data: The raw data you want to have decoded 27 | /// - Returns: Success - `false` upon detection of improperly formatted CMS message. 28 | @discardableResult 29 | func updateMessage(data: NSData) -> Bool { 30 | return CMSDecoderUpdateMessage(decoder, data.bytes, data.length) != errSecUnknownFormat 31 | } 32 | 33 | /// Indicate that no more `updateMessage()` calls are coming; finish decoding the message. 34 | /// 35 | /// - Returns: Success - `false` upon detection of improperly formatted CMS message. 36 | @discardableResult 37 | func finaliseMessage() -> Bool { 38 | return CMSDecoderFinalizeMessage(decoder) != errSecUnknownFormat 39 | } 40 | 41 | /// Obtain the actual message content (payload), if any. If the message was signed with 42 | /// detached content then this will return `nil`. 43 | /// 44 | /// - Warning: This cannot be called until after `finaliseMessage()` is called! 45 | var data: Data? { 46 | var newData: CFData? 47 | CMSDecoderCopyContent(decoder, &newData) 48 | return newData as Data? 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Sources/SwiftyProvisioningProfile/SwiftyCertificate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftyCertificate.swift 3 | // SwiftyProvisioningProfile 4 | // 5 | // Created by Sherlock, James on 20/11/2018. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Certificate { 11 | 12 | enum ParseError: Error { 13 | case failedToCreateCertificate 14 | case failedToCreateTrust 15 | case failedToExtractValues 16 | } 17 | 18 | static func parse(from data: Data) throws -> Certificate { 19 | let certificate = try getSecCertificate(data: data) 20 | 21 | var error: Unmanaged? 22 | let values = SecCertificateCopyValues(certificate, nil, &error) 23 | 24 | if let error = error { 25 | throw error.takeRetainedValue() as Error 26 | } 27 | 28 | guard let valuesDict = values as? [CFString: Any] else { 29 | throw ParseError.failedToExtractValues 30 | } 31 | 32 | var commonName: CFString? 33 | SecCertificateCopyCommonName(certificate, &commonName) 34 | 35 | return try Certificate(results: valuesDict, commonName: commonName as String?) 36 | } 37 | 38 | private static func getSecCertificate(data: Data) throws -> SecCertificate { 39 | guard let certificate = SecCertificateCreateWithData(kCFAllocatorDefault, data as CFData) else { 40 | throw ParseError.failedToCreateCertificate 41 | } 42 | 43 | return certificate 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Sources/SwiftyProvisioningProfile/SwiftyProvisioningProfile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by James Sherlock on 10/04/2018. 3 | // 4 | 5 | import Foundation 6 | 7 | public extension ProvisioningProfile { 8 | 9 | enum ParseError: Error { 10 | case failedToCreateDecoder 11 | case failedToCreateData 12 | } 13 | 14 | /// Create a Provisioning Profile object from the file's Data. 15 | static func parse(from data: Data) throws -> ProvisioningProfile { 16 | 17 | guard let decoder = SwiftyCMSDecoder() else { 18 | throw ParseError.failedToCreateDecoder 19 | } 20 | 21 | decoder.updateMessage(data: data as NSData) 22 | decoder.finaliseMessage() 23 | 24 | guard let data = decoder.data else { 25 | throw ParseError.failedToCreateData 26 | } 27 | 28 | return try PropertyListDecoder().decode(ProvisioningProfile.self, from: data) 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SwiftyProvisioningProfileTests 3 | 4 | XCTMain([ 5 | testCase(SwiftyProvisioningProfileTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /Tests/SwiftyProvisioningProfileTests/Resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sherlouk/SwiftProvisioningProfile/d0a52eefbf1539baa1075e1ad40b12baa5536739/Tests/SwiftyProvisioningProfileTests/Resources/.gitkeep -------------------------------------------------------------------------------- /Tests/SwiftyProvisioningProfileTests/SwiftyProvisioningProfileTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by James Sherlock on 10/04/2018. 3 | // 4 | 5 | import XCTest 6 | @testable import SwiftyProvisioningProfile 7 | 8 | class SwiftyProvisioningProfileTests: XCTestCase { 9 | 10 | lazy var resourceBundle: Bundle? = { 11 | // Note: This will fail if run via Xcode, and depends highly on the projects structure. 12 | // SPM doesn't currently support resources, this is a temporary (and fragile) solution until it does. 13 | let currentBundle = Bundle(for: SwiftyProvisioningProfileTests.self) 14 | return Bundle(path: "\(currentBundle.bundlePath)/../../../../Tests/SwiftyProvisioningProfileTests/Resources") 15 | }() 16 | 17 | lazy var testProfileURLs: [URL] = { 18 | guard let bundle = resourceBundle else { 19 | fatalError("Tests are being run through Xcode, or the project structure no longer matches up") 20 | } 21 | 22 | guard let urls = bundle.urls(forResourcesWithExtension: "mobileprovision", subdirectory: nil) else { 23 | fatalError("No `mobileprovision` files found in `Tests/SwiftyProvisioningProfileTests/Resources`") 24 | } 25 | 26 | return urls 27 | }() 28 | 29 | lazy var testCertificateURLs: [URL] = { 30 | guard let bundle = resourceBundle else { 31 | fatalError("Tests are being run through Xcode, or the project structure no longer matches up") 32 | } 33 | 34 | guard let urls = bundle.urls(forResourcesWithExtension: "cer", subdirectory: nil) else { 35 | fatalError("No `cer` files found in `Tests/SwiftyProvisioningProfileTests/Resources`") 36 | } 37 | 38 | return urls 39 | }() 40 | 41 | func testParseIOS() { 42 | 43 | do { 44 | for url in testProfileURLs { 45 | let data = try Data(contentsOf: url) 46 | let profile = try ProvisioningProfile.parse(from: data) 47 | 48 | print(profile.name) 49 | } 50 | 51 | // TODO: Create or find a simple & usable profile and write actual tests for it 52 | } catch { 53 | XCTFail(String(describing: error)) 54 | } 55 | 56 | } 57 | 58 | func testParseCertificate() { 59 | 60 | do { 61 | for url in testCertificateURLs { 62 | let data = try Data(contentsOf: url) 63 | let certificate = try Certificate.parse(from: data) 64 | 65 | print(certificate) 66 | } 67 | 68 | // TODO: Create or find a simple & usable certificate and write actual tests for it 69 | } catch { 70 | XCTFail(String(describing: error)) 71 | } 72 | 73 | } 74 | 75 | func testParseMAC() { 76 | 77 | // TODO 78 | 79 | } 80 | 81 | static var allTests = [ 82 | ("testParseIOS", testParseIOS), 83 | ("testParseMAC", testParseMAC), 84 | ("testParseCertificate", testParseCertificate), 85 | ] 86 | } 87 | --------------------------------------------------------------------------------