Get the EXPIRATION date of a Provisioning Profile at Run-time?
Asked Answered
B

6

9

I have an app that I routinely pass out to testers via the ad-hoc distribution method. Some of these testers are 'on the ball' and know enough about provisioning profiles and the quarterly expirations and can (if I forget) give me a nudge to rebuild a new version for them to test.

However some of the users always seem to get to the point where it stops running and then bitch and moan about it - despite them probably dismissing the iOS level reminder.

My question is can I programatically get hold of the expiry date at runtime and do my own 'in-app' alerts or system notifications to remind them to pull down the newer version?

Brainstorm answered 23/9, 2013 at 14:1 Comment(0)
E
4

Swift version:

// Returns `nil` if it fails
private func getProvisioningProfileExpirationDateAsString() -> String? {
    guard
        let profilePath = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision"),
        let profileData = try? Data(contentsOf: URL(fileURLWithPath: profilePath)),
        // Note: We use `NSString` instead of `String`, because it makes it easier working with regex, ranges, substring etc.
        let profileNSString = NSString(data: profileData, encoding: String.Encoding.ascii.rawValue)
        else {
        print("WARNING: Could not find or read `embedded.mobileprovision`. If running on Simulator, there are no provisioning profiles.")
        return nil
    }


    // NOTE: We have the `[\\W]*?` check to make sure that variations in number of tabs or new lines in the future does not influence the result.
    guard let regex = try? NSRegularExpression(pattern: "<key>ExpirationDate</key>[\\W]*?<date>(.*?)</date>", options: []) else {
        print("Warning: Could not create regex.")
        return nil
    }

    let regExMatches = regex.matches(in: profileNSString as String, options: [], range: NSRange(location: 0, length: profileNSString.length))

    // NOTE: range `0` corresponds to the full regex match, so to get the first capture group, we use range `1`
    guard let rangeOfCapturedGroupForDate = regExMatches.first?.range(at: 1) else {
        print("Warning: Could not find regex match or capture group.")
        return nil
    }

    let dateAsString = profileNSString.substring(with: rangeOfCapturedGroupForDate)
    return dateAsString
}
Eolic answered 20/9, 2018 at 16:8 Comment(0)
P
8

You are looking for something like

"<key>ExpirationDate</key><date>2014-12-06T00:26:10Z</date>" in [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"]

But getting there is not easy! This code could be improved, parts of it were based on other stackoverflow posts. Note: Another option would be to load everything between item plist and /plist into ...a plist (dictionary). But since we're already there, we just find the sibling by hand.

- (NSString*) getExpiry{

    NSString *profilePath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
    // Check provisioning profile existence
    if (profilePath)
    {
        // Get hex representation
        NSData *profileData = [NSData dataWithContentsOfFile:profilePath];
        NSString *profileString = [NSString stringWithFormat:@"%@", profileData];

        // Remove brackets at beginning and end
        profileString = [profileString stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:@""];
        profileString = [profileString stringByReplacingCharactersInRange:NSMakeRange(profileString.length - 1, 1) withString:@""];

        // Remove spaces
        profileString = [profileString stringByReplacingOccurrencesOfString:@" " withString:@""];


        // Convert hex values to readable characters
        NSMutableString *profileText = [NSMutableString new];
        for (int i = 0; i < profileString.length; i += 2)
        {
            NSString *hexChar = [profileString substringWithRange:NSMakeRange(i, 2)];
            int value = 0;
            sscanf([hexChar cStringUsingEncoding:NSASCIIStringEncoding], "%x", &value);
            [profileText appendFormat:@"%c", (char)value];
        }

        // Remove whitespaces and new lines characters
        NSArray *profileWords = [profileText componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

        //There must be a better word to search through this as a structure! Need 'date' sibling to <key>ExpirationDate</key>, or use regex
        BOOL sibling = false;
        for (NSString* word in profileWords){
            if ([word isEqualToString:@"<key>ExpirationDate</key>"]){
                NSLog(@"Got to the key, now need the date!");
                sibling = true;
            }
            if (sibling && ([word rangeOfString:@"<date>"].location != NSNotFound)) {
                NSLog(@"Found it, you win!");
                NSLog(@"Expires: %@",word);
                return word;
            }
        }

    }

    return @"";
}
Pleadings answered 8/1, 2014 at 20:56 Comment(2)
Its returning nil for profilePath on the first line itself. I am using xcode 8.2.1 and using this objective-c code with swift.Jadotville
Are you running the code in the simulator? It's not signed, so there will be no embedded.mobileprovision file when run on the simulator. Try running the code on a physical device (code must be signed and the embedded.mobileprovision file should, in theory be there.Willowwillowy
E
4

Swift version:

// Returns `nil` if it fails
private func getProvisioningProfileExpirationDateAsString() -> String? {
    guard
        let profilePath = Bundle.main.path(forResource: "embedded", ofType: "mobileprovision"),
        let profileData = try? Data(contentsOf: URL(fileURLWithPath: profilePath)),
        // Note: We use `NSString` instead of `String`, because it makes it easier working with regex, ranges, substring etc.
        let profileNSString = NSString(data: profileData, encoding: String.Encoding.ascii.rawValue)
        else {
        print("WARNING: Could not find or read `embedded.mobileprovision`. If running on Simulator, there are no provisioning profiles.")
        return nil
    }


    // NOTE: We have the `[\\W]*?` check to make sure that variations in number of tabs or new lines in the future does not influence the result.
    guard let regex = try? NSRegularExpression(pattern: "<key>ExpirationDate</key>[\\W]*?<date>(.*?)</date>", options: []) else {
        print("Warning: Could not create regex.")
        return nil
    }

    let regExMatches = regex.matches(in: profileNSString as String, options: [], range: NSRange(location: 0, length: profileNSString.length))

    // NOTE: range `0` corresponds to the full regex match, so to get the first capture group, we use range `1`
    guard let rangeOfCapturedGroupForDate = regExMatches.first?.range(at: 1) else {
        print("Warning: Could not find regex match or capture group.")
        return nil
    }

    let dateAsString = profileNSString.substring(with: rangeOfCapturedGroupForDate)
    return dateAsString
}
Eolic answered 20/9, 2018 at 16:8 Comment(0)
O
2

In iOS 13, it doesn't work. The description of NSData changes from

{0x30822755 06092a86 4886f70d 010702a0 ... 889bfcbe ed768bac }

to

{length = ..., bytes = 0x30822755 06092a86 4886f70d 010702a0 ... 889bfcbe ed768bac }

Try this code:

NSData *data = ....;
NSUInteger dataLength = [data length];
NSMutableString *string =[NSMutableString stringWithCapacity:dataLength*2];

const unsigned char *dataBytes = [data bytes];
for (NSInteger idx = 0; idx < dataLength; ++idx)
{
    [string appendFormat:@"%02x", dataBytes[idx]];
}
Ousley answered 27/11, 2019 at 16:55 Comment(0)
S
1

In iOS 13, to simplify Ttnka's suggestion, you can use profileData.debugDescription to create the profileString (from Objective-C code posted initially):

NSString *profileString = [NSString stringWithFormat:@"%@", profileData.debugDescription];

Reference: https://developer.apple.com/forums/thread/119111

Skew answered 27/1, 2021 at 19:50 Comment(0)
D
0

Combining @Pierre-Francoys Brousseau's and @Ttnka's answers:

- (NSString *) getAppExpiry
{

    NSString *profilePath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
    // Check provisioning profile existence
    if (profilePath)
    {
        // Get hex representation
        NSData *profileData = [NSData dataWithContentsOfFile:profilePath];
        NSString *profileString;
        
        if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 13.0)
        {
            NSUInteger dataLength = [profileData length];
            NSMutableString *string =[NSMutableString stringWithCapacity:dataLength*2];

            const unsigned char *dataBytes = [profileData bytes];
            for (NSInteger idx = 0; idx < dataLength; ++idx)
            {
                [string appendFormat:@"%02x", dataBytes[idx]];
            }
            
            profileString = string;

        }
        else
        {
            profileString = [NSString stringWithFormat:@"%@", profileData];
            
            // Remove brackets at beginning and end
            profileString = [profileString stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:@""];
            profileString = [profileString stringByReplacingCharactersInRange:NSMakeRange(profileString.length - 1, 1) withString:@""];

            // Remove spaces
            profileString = [profileString stringByReplacingOccurrencesOfString:@" " withString:@""];
        }

        // Convert hex values to readable characters
        NSMutableString *profileText = [NSMutableString new];
        for (int i = 0; i < profileString.length; i += 2)
        {
            NSString *hexChar = [profileString substringWithRange:NSMakeRange(i, 2)];
            int value = 0;
            sscanf([hexChar cStringUsingEncoding:NSASCIIStringEncoding], "%x", &value);
            [profileText appendFormat:@"%c", (char)value];
        }
        
        // Remove whitespaces and newline characters
        NSArray *profileWords = [profileText componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
        
        // There must be a better word to search through this as a structure! Need 'date' sibling to <key>ExpirationDate</key>
        BOOL sibling = false;
        for (NSString* word in profileWords){
            if ([word isEqualToString:@"<key>ExpirationDate</key>"]){
                NSLog(@"Got to the key, now need the date!");
                sibling = true;
            }
            if (sibling && ([word rangeOfString:@"<date>"].location != NSNotFound)) {
                NSLog(@"Found it, you win!");
                NSLog(@"Expires: %@", word);
                return word;
            }
        }
    }
    else
        NSLog(@"profile path is nil");
    
    NSLog(@"Could not find app expiry date");
    
    return @"";
}
Duplicate answered 6/7, 2021 at 2:37 Comment(0)
M
0

Here's a simple solution that I've made. It uses ranges to extract the <date> fields. Since there are two, CreationDate and ExpirationDate, you simply just need to adjust the index you want to look for. Just simply make a URL object with your filePath.

func getExpirationDate(for filePath: URL) -> String? {
    guard let profileData = try? Data(contentsOf: filePath),
          let profileString = String(data: profileData, encoding: .ascii) else {
        return nil
    }
    
    // Index 0 - Creation Date
    // Index 1 - Expiration Date
    let startRange = profileString.ranges(of: "<date>")[1]
    let endRange = profileString.ranges(of: "</date>")[1]
    
    let startIndex = profileString.index(startRange.upperBound, offsetBy: 0)
    let endIndex = profileString.index(endRange.lowerBound, offsetBy: 0)

    let expirationDate = profileString[startIndex..<endIndex]
    
    return String(expirationDate)
}
Maure answered 22/2 at 13:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.