Reading iOS Provisioning Profile in your Swift App
In this short post, I will describe how to a read provisioning profile from iOS mobile app to discover some apps metadata.
The mobile provisioning profile is a file embedded by XCode when you build and package your application. It contains several pieces of information that can be useful for your app. For example, you can get the Apple push environment to use, either sandbox for development apps or production for store or Testflight production builds.
We can help you build your next Swift app in weeks rather than months
Read more about ProcessOne and Swift »
Principles
Getting and reading the mobile provisioning file
The mobile provision file is embedded in the app. You can read it from the main bundle. You first need to read the path of that file from the main bundle:
let profilePath: String? = Bundle.main.path(forResource: "embedded",
ofType: "mobileprovision")
And then you can try reading the file into an string:
let profileString = try? NSString.init(contentsOfFile: profilePath,
encoding: String.Encoding.isoLatin1.rawValue)
Extracting the metadata plist
The provisioning profile contains a plist file that exposes some app metadata. However, the plist string is embedded in a larger binary file. The easiest way to extract the plist is to read only the XML part of the file. This can be done as follows:
// Skip binary part at the start of the mobile provisionning profile
let scanner = Scanner(string: profileString as String)
guard scanner.scanUpTo("<plist", into: nil) != false else { return nil }
// ... and extract plist until end of plist payload (skip the end binary part.
var extractedPlist: NSString?
guard scanner.scanUpTo("</plist>", into: &extractedPlist) != false else { return nil }
guard let plist = extractedPlist?.appending("</plist>").data(using: .isoLatin1) else { return nil }
Parsing the plist string
You can finally parse the plist strong to get the plist content into a MobileProvision struct, using Swift decodable protocol, introduced in Swift 4:
let decoder = PropertyListDecoder()
do {
let provision = try decoder.decode(MobileProvision.self, from: plist)
// Do something with the data
} catch let err {
// Handle parsing error
}
Putting it all together
Here is the full code with the MobileProvision struct implementing the Decodable protocol:
// | |
// MobileProvision.swift | |
// Fluux.io | |
// | |
// Created by Mickaël Rémond on 03/11/2018. | |
// Copyright © 2018 ProcessOne. | |
// Distributed under Apache License v2 | |
// | |
import Foundation | |
/* Decode mobileprovision plist file | |
Usage: | |
1. To get mobileprovision data as embedded in your app: | |
MobileProvision.read() | |
2. To get mobile provision data from a file on disk: | |
MobileProvision.read(from: "my.mobileprovision") | |
*/ | |
struct MobileProvision: Decodable { | |
var name: String | |
var appIDName: String | |
var platform: [String] | |
var isXcodeManaged: Bool? = false | |
var creationDate: Date | |
var expirationDate: Date | |
var entitlements: Entitlements | |
private enum CodingKeys : String, CodingKey { | |
case name = "Name" | |
case appIDName = "AppIDName" | |
case platform = "Platform" | |
case isXcodeManaged = "IsXcodeManaged" | |
case creationDate = "CreationDate" | |
case expirationDate = "ExpirationDate" | |
case entitlements = "Entitlements" | |
} | |
// Sublevel: decode entitlements informations | |
struct Entitlements: Decodable { | |
let keychainAccessGroups: [String] | |
let getTaskAllow: Bool | |
let apsEnvironment: Environment | |
private enum CodingKeys: String, CodingKey { | |
case keychainAccessGroups = "keychain-access-groups" | |
case getTaskAllow = "get-task-allow" | |
case apsEnvironment = "aps-environment" | |
} | |
enum Environment: String, Decodable { | |
case development, production, disabled | |
} | |
init(keychainAccessGroups: Array<String>, getTaskAllow: Bool, apsEnvironment: Environment) { | |
self.keychainAccessGroups = keychainAccessGroups | |
self.getTaskAllow = getTaskAllow | |
self.apsEnvironment = apsEnvironment | |
} | |
init(from decoder: Decoder) throws { | |
let container = try decoder.container(keyedBy: CodingKeys.self) | |
let keychainAccessGroups: [String] = (try? container.decode([String].self, forKey: .keychainAccessGroups)) ?? [] | |
let getTaskAllow: Bool = (try? container.decode(Bool.self, forKey: .getTaskAllow)) ?? false | |
let apsEnvironment: Environment = (try? container.decode(Environment.self, forKey: .apsEnvironment)) ?? .disabled | |
self.init(keychainAccessGroups: keychainAccessGroups, getTaskAllow: getTaskAllow, apsEnvironment: apsEnvironment) | |
} | |
} | |
} | |
// Factory methods | |
extension MobileProvision { | |
// Read mobileprovision file embedded in app. | |
static func read() -> MobileProvision? { | |
let profilePath: String? = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision") | |
guard let path = profilePath else { return nil } | |
return read(from: path) | |
} | |
// Read a .mobileprovision file on disk | |
static func read(from profilePath: String) -> MobileProvision? { | |
guard let plistDataString = try? NSString.init(contentsOfFile: profilePath, | |
encoding: String.Encoding.isoLatin1.rawValue) else { return nil } | |
// Skip binary part at the start of the mobile provisionning profile | |
let scanner = Scanner(string: plistDataString as String) | |
guard scanner.scanUpTo("<plist", into: nil) != false else { return nil } | |
// ... and extract plist until end of plist payload (skip the end binary part. | |
var extractedPlist: NSString? | |
guard scanner.scanUpTo("</plist>", into: &extractedPlist) != false else { return nil } | |
guard let plist = extractedPlist?.appending("</plist>").data(using: .isoLatin1) else { return nil } | |
let decoder = PropertyListDecoder() | |
do { | |
let provision = try decoder.decode(MobileProvision.self, from: plist) | |
return provision | |
} catch { | |
// TODO: log / handle error | |
return nil | |
} | |
} | |
} |
MobileProvision.swift hosted by GitHub
You can use the code from your mobile app as follows:
var sandbox: Bool
if let provision = MobileProvision.read() {
print("We need to use Sandbox")
sandbox = provision.entitlements.apsEnvironment == .development ? true : false
}
That’s it. Your application is now able to discover if it should use APNS in development or production mode automatically, without having to rely on defining a DEBUG
build environment variable.