Extracting Payload Keys from Profile Manager
May 12, 2016
I’m developing ProfileCreator, an open source configuration profile creation tool. While doing this I’ve been looking at ways to automatically and repeatedly create a list of all available payload keys and their configuration for use in my app.
Apple publishes the Configuration Profile Key Reference at their developer site, but even though it’s being updated, it doesn’t contain all keys and configurations.
Profile Manager, in being Apple’s own MDM product, is often the closest we have to a reference for OS X and iOS configuration profiles as it usually supports all new profile keys on their release. But as Profile Manager’s profile creation is geared towards OTA deployment of profiles it enforces keys which aren’t required if the profile is installed by other means.
Apple Configurator 2 is another application developed by Apple capable of creating configuration profiles (iOS only). But when you start comparing payload keys and options with Profile Manager you see that some payloads have slightly different configuration options.
Neither of these applications have a reference that describe which payloads and keys they support so the only way to find that out is to create a profile, export it and inspect it’s content.
Finding the pieces
Profile Manager is a web app with it’s front end written in JavaScript and the back end in Ruby and PHP, which in turn also uses private Objective-C libraries.
The information in this post comes from inspecting the Profile Manager source in Server 5.1
This post will cover how to find and extract payload keys and their corresponding information from the source code instead of creating and saving profiles through it’s GUI.
I’m also making a python script available that I’ve put together while researching this that is currently able to extract most of the available payload information, but it's not complete.
The bulk of the payload information can be found in the front end code, so we’ll start there.
Front end
This is the path to the JavaScript fronted source code folder:
.../Server.app/Contents/ServerRoot/usr/share/devicemgr/frontend/admin/common/app
Within is a JavaScript file named javascript-packed.js and two CSS-files. The JavaScript file (as the name implies) is packed which is common for JavaScript code to save bandwidth. But this technique also obfuscates the file contents making it harder for humans to read.
To unpack this file, I used one of the many JavaScript “Beautifyer/Unpackers” online which expands and tries to structure the code in a human readable format.
Not only did the code expand from 1838 lines to 97302 lines, and the file size grew from 2965154 bytes to 4648523, it also made it possible for me to find out how the code is structured and find searchable patterns to single out payloads and their respective keys.
KnobSet
“KnobSet” is the name given to payload groups that hold payload-specific property keys for a PayloadType. (There are some rare exceptions).
The variable knobSetProperties in the JavaScript source lists all available KnobSets
This example command only works on versions where the knobSetProperties list doesn’t contain a newline
sed -nE -e 's/.*knobSetProperties:\[([",a-zA-Z]+)\].*/\1/p' javascript-packed.js
Pass the -l/–list flag to my profileManagerKeyExtractor.py script for the same list:
./profileManagerKeyExtractor.py -l
With this list we can single out the relevant parts of the code that contain payload information for the selected KnobSet and parse it to get a structured output.
Gathering the information
To access the payload information for a KnobSet we need the selected KnobSet object name.
Example that extracts the object name for the calDavKnobSets property:
sed -nE 's/.*,Admin.([a-zA-Z]+KnobSet).*profilePropertyName[=:]"calDavKnobSets".*/\1/p' javascript-packed.js
The payload keys available to a specific KnobSet is merged into the object using extend().
The CalDavKnobSet object contains information for keys, value type, default value(s) etc.
Excerpt from the extend method for the CalDavKnobSet:
Admin.CalDavKnobSet = Admin.KnobSet.extend({
accountDescription: SC.Record.attr(String, {
key: "CalDAVAccountDescription",
defaultValue: "_generic_string_My CalDAV Account".loc()
}),
hostName: SC.Record.attr(String, {
key: "CalDAVHostName"
}),
portNumber: SC.Record.attr(Number, {
key: "CalDAVPort",
defaultValue: 8443
}),
principalUrl: SC.Record.attr(String, {
key: "CalDAVPrincipalURL"
}),
...
The same is true for the KnobSetView object which holds key descriptions, available selections, hint strings, default values etc.
Excerpt from the extend method for the CalDavKnobSetView:
Admin.CalDavKnobSetView = Admin.KnobSetView.extend({
contentView: Admin.KnobSetContentView.design({
layout: {
width: Admin.KNOB_WIDTH,
centerX: 0,
height: 420
},
childViews: ["accountDescriptionField", "hostNameField", "principalUrlField", "usernameField", "passwordField", "useSslField"],
accountDescriptionField: Admin.KnobSetTextFieldAbsolute.design({
layout: {
top: 5,
centerX: 0,
width: Admin.KNOB_WIDTH,
height: 70
},
label: "_generic_string_Account Description".loc(),
description: "_generic_string_The display name of the account".loc(),
fieldHint: "_generic_string_optional".loc(),
contentBinding: ".owner.content",
fieldContentValueKey: "accountDescription"
}),
hostNameField: Admin.KnobSetIpPortFieldViewAbsolute.design({
layout: {
top: 75,
centerX: 0,
width: Admin.KNOB_WIDTH,
height: 70
},
label: "_generic_string_Account Hostname and Port"
.loc(),
description: "_generic_string_The CalDAV hostname or IP address and port number".loc(),
fieldHint: "_hint_required".loc(),
contentBinding: ".owner.content",
fieldIpHostContentValueKey: "hostName",
fieldPortContentValueKey: "portNumber"
}),
...
By knowing this, and by knowing the name of the KnobSet object we’re interested in it’s possible by using regular expression to extract the method content and subsequently parse that for the specific information we seek.
Localization
Many of the string values in the source is localized, which means that the actual string you might get from a variable could look like this:
Example from the CalDAV account description:
"_generic_string_The display name of the account".loc()
As you can see, the string ends with the method .loc(). To get the string that’s presented to the user we need to use the string we got as a variable name and extract that variable’s value from the localization file.
For English, the localization file can be found at the following path:
.../Server.app/Contents/ServerRoot/usr/share/devicemgr/frontend/admin/en.lproj/\
app/javascript_localizedStrings.js
Then we just get the value for the _generic_string_The display name of the account variable:
sed -nE 's/"_generic_string_The display name of the account": (.*),$/\1/p' javascript_localizedStrings.js
"The display name of the account"
Or, if we use the same extraction from the french localization file:
"Nom d’affichage du compte"
Back end
The information in the front end code is not complete. For example the PayloadType(s), if the payload allows multiple configurations (Unique) and the Payload Name is stored in ruby model files in the back end source code.
This is the path to the source folder for the back end ruby models:
.../Server.app/Contents/ServerRoot/usr/share/devicemgr/backend/app/models
In this folder are all KnobSets separated into their own model files, so we can use the KnobSet object name to find the file containing a class that matches it and parse that file for info:
Example content of the cal_dav_knob_set.rb file (the file matching object CalDavKnobSet):
class CalDavKnobSet < KnobSet
@@payload_type = "com.apple.caldav.account"
@@payload_subidentifier = "caldav"
@@is_unique = false
@@payload_name = "CalDAV"
Putting it together
With this knowledge, it’s possible to parse the source to extract the payload information.
This is the current output from my script if you request the information for the calDavKnobSet:
$ ./profileManagerKeyExtractor.py -k calDavKnobSets
Payload Name: CalDAV
Payload Type: com.apple.caldav.account
Unique: NO
UserLevel: YES
SystemLevel: NO
Platforms: iOS,OSX
PayloadKey: CalDAVAccountDescription
Title: Account Description
Description: The display name of the account
Type: String
Hint String: optional
DefaultValue: My Calendar Account
PayloadKey: CalDAVHostName
Title: Account Hostname and Port
Description: The CalDAV hostname or IP address and port number
Type: String
PayloadKey: CalDAVPort
Title:
Description:
Type: Number
DefaultValue: 8443
PayloadKey: CalDAVPrincipalURL
Title: Principal URL
Description: The Principal URL for the CalDAV account
Type: String
PayloadKey: CalDAVUsername
Title: Account User name
Description: The CalDAV user name
Type: String
Hint String: Required (OTA)
Set on device (Manual)
PayloadKey: CalDAVPassword
Title: Account Password
Description: The CalDAV password
Type: String
Hint String: Optional (OTA)
Set on device (Manual)
PayloadKey: CalDAVUseSSL
Title: Use SSL
Description: Enable Secure Socket Layer communication with CalDAV server
Type: Boolean
DefaultValue: YES
Final Notes
The method of parsing source code is of course never a good idea as any release might break the parsing. This was just a break from my work on ProfileCreator to see if it would be possible to create a tool that could be used to run on each Profile Manager release and create a diff of what’s changed so new additions would be easy to keep track of.
It was partly successful, and I can now manually get all information I need, but the script currently doesn’t handle arrays or dicts as that would require some complex parsing logic.
Also, some strings that are returned cannot be found in the places I’ve looked and might be resolved in the private frameworks.
I currently need to focus on building an alpha release of the ProfileCreator application, but when that is finished, I will probably revisit this script to improve it further.
If anyone find this interesting please feel free to extend my script or use it as inspiration.