Unarchiving data without secure encoding with new NSKeyedUnarchiver APIs

I'm trying to replace my method calls to NSKeyedArchiver and NSKeyedUnarchiver that were recently deprecated in the most recent iOS SDK. I'm trying to implement these calls on an object that conforms to the NSCoding protocol, not the NSSecureCoding protocol.


I managed to replace calls to +[NSKeyedArchiver archivedDataWithRootObject:] with calls to +[NSKeyedArchiver archivedDataWithRootObject:requiringSecureCoding:error:], passing false for requiringSecureCoding and verified that my object is successfully encoding.


However, when I attempted to replace calls to +[NSKeyedUnarchiver unarchiveObjectWithData:] with calls to +[NSKeyedUnarchiver unarchivedObjectOfClasses:fromData:error:], there was no error and the call returned nil. What I found especially odd was that I couldn't trigger the object's implemention of the - (void) encodeWithCoder:(NSCoder *) aCoder method from the NSCoding protocol; the log statement I placed there wasn't triggered.


Here's my call to 'unarchivedObjectOfClasses:fromData:error:':


[NSKeyedUnarchiver unarchivedObjectOfClasses:Cookies.class fromData:data error:&error]


and the old call:


[NSKeyedUnarchiver unarchiveObjectWithData:data];


Cookies is a class with 2 properties: one is an enum instance (backed by NSInteger) and the other is a Set of NSStrings


My best guess is that this may be caused by what's here in the method description:

For some reason it appears the image isn't loading. It says "Decodes the root object of the given class from the given archive, previously encoded by NSKeyedArchiver. Enables requiresSecureCoding and sets the decodingFailurePolicy to NSDecodingFailurePolicySetErrorAndReturn. Returns nil if the given data is not valid or cannot be decoded, and sets the error out parameter."


If this is the problem, I'm not sure how to work around this behavior. The only other hint I have about what could be going on is what was said in this forum post (https://forums.developer.apple.com/message/322990#322990) below:

This made me think that there may be analagous behavior in the Objective C APIs, so I tried using the call shown below:


[NSKeyedUnarchiver unarchivedObjectOfClasses:[[NSMutableSet alloc] initWithArray:@[Cookies.class, NSSet.class]] fromData:data error:&error]


That did not work either. Does anyone know how I can rewrite this API to unarchive this class or have any insight as to why I might be experiencing this behavior?

Replies

I haven't had to use the new APIs yet, so I'm not sure exactly what's going on, but I suspect you will need to create the unarchiver manually (alloc/initForReadingFromData:error:) first, then set the value of requiresSecureCoding to NO, then decode the top level object manually.


That used to be what you need to do if you wanted to enforce secure coding —you couldn't use the convenience class method. It looks like this is now turned around, so that the convenience class method gives you forced secure coding, and you use manual creation otherwise.


IIRC there was discussion of the new APIs in one of the WWDC sessions. If you search for that session, there might be information in there about "legacy" non-secure archives.

you will need to create the unarchiver manually … first, then set the value of

requiresSecureCoding
to NO, then decode the top level object manually.

Correct.

IIRC there was discussion of the new APIs in one of the WWDC sessions. If you search for that session, there might be information in there about "legacy" non-secure archives.

Also correct. The session is WWDC 2018 Session 222 Data You Can Trust and this technique is covered at 32:48.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Great. But for clarity, none of this will work in Xcode 9.4.1 with a deployment target of 10.0 - correct?

initForWritingWithMutableData: is deprecated but still the only available init for iOS11 - correct?


And a comment; please, please show code for Objective C especially when deprecating current code and replacing it with something else.

Post not yet marked as solved Up vote reply of PBK Down vote reply of PBK

Great. But for clarity, none of this will work in Xcode 9.4.1 with a deployment target of 10.0 - correct?

Correct. Xcode 9 includes the iOS 11 SDK which doesn’t have the declarations you need. And iOS 10 does not have the implementation so, if you want to maintain support for that release, you’ll need to continue to use your old code as a compatibility path.

And a comment; please, please show code for Objective C especially when deprecating current code and replacing it with something else.

The compiler does a good job of this. Consider the following code:

archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding:false];

If you add that in a project whose deployment target is iOS 10 and then build it with Xcode 10 beta, you’ll get a warning (`initRequiringSecureCoding:' is only available on iOS 11.0 or newer) and a fix-it that transforms the code to this:

if (@available(iOS 11.0, *)) {
    archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding:false];
} else {
    // Fallback on earlier versions
}

You just need to fill in the else branch with your compatibility code, resulting in something like this:

NSKeyedArchiver * archiver;
if (@available(iOS 11.0, *)) {
    archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding:false];
} else {
    archiver = [[NSKeyedArchiver alloc] init];
}

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you very much.

I have since tried to instantiate and decode my data manually with various permutations of NSKeyedUnarchiver methods, but I keep running into issues. Here's one of the ways I rewrote the call to +[NSKeyedUnarchiver unarchiveObjectWithData:]:

NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
[unarchiver setRequiresSecureCoding:NO];
cookies = [[Cookies alloc] initWithCoder:unarchiver];


Here's the initWithCoder: method from my Cookies class:


- (id)initWithCoder:(NSCoder *)coder {
    if (self = [super init]) {
        _version = [coder decodeObjectForKey:keychainVersion];
        _cAccount = [coder decodeObjectForKey:currentAccount];
        _pAccount = [coder decodeObjectForKey:primaryAccount];
        _dict = [coder decodeObjectForKey:dictionary];
        _shouldHaveFirstRunToken = [coder decodeBoolForKey:shouldHaveFirstRunToken];
    }
    return self;
}

I traced this method call with my debugger, and I found that my call to initWithCoder returns an empty Cookies object. That is, all the calls to -[NSKeyedUnarchiver decodeObjectForKey:] and -[NSKeyedUnarchiver decodeBoolForKey:] return nil and assign it to the property variables of Cookies.

I tried using many permutations of NSKeyedUnarchiver methods, but I had a similar experiences with all of them. I tried using -[NSCoding decodeObjectOfClass:forKey:], -[NSKeyedUnarchiver decodeTopLevelObjectForKey:error:], and -[NSKeyedUnarchiver initForReadingFromData:error:]. The ONLY method call I was able to get an error or exception from was -[NSKeyedUnarchiver initForReadingFromData:error:], which gave me an error saying:

Error Domain=NSCocoaErrorDomain Code=4864 "*** -[NSKeyedUnarchiver _initForReadingFromData:error:throwLegacyExceptions:]: incomprehensible archive (0x61, 0x73, 0x64, 0x66, 0x66, 0x61, 0x0, 0x0)" UserInfo={NSDebugDescription=*** -[NSKeyedUnarchiver _initForReadingFromData:error:throwLegacyExceptions:]: incomprehensible archive (0x61, 0x73, 0x64, 0x66, 0x66, 0x61, 0x0, 0x0)}

However, I could not find any applicable information about this error message when checking online resources.


I'm pulling my hair out over this one. Where are the errors for these failed method calls? Where's the documentation or examples on how these methods work? Does anyone know what I should change to successfully unarchive this object?

Note: Something I found odd about this rewriting is that the -[NSKeyedUnarchiver initForReadingWithData:] method is deprecated as of iOS12, but I'm still able to compile it with iOS SDK 12 without any build errors. Why am I able to do this? Does deprecation not mean that support for the API is withdrawn in that SDK?

Additional Note: Furthermore, when I jump to the method definition for -[NSKeyedUnarchiver initForReadingWithData:], I'm taken to the NSKeyedArchiver.h header that I presume is from the iOS 11 SDK. That is, the newly deprecated methods are not marked as such and the comment at the head of the file says:

/* NSKeyedArchiver.h
  Copyright (c) 2001-2017, Apple Inc. All rights reserved.
*/


However, when I jump to the method definition for other methods in NSKeyedUnarchiver, I'm taken to a different NSKeyedArchiver.h header that I presume is from the iOS 12 SDK. I say that because methods I know to be deprecated are marked as such and the comment at the head of the file says:

/* NSKeyedArchiver.h
  Copyright (c) 2001-2018, Apple Inc. All rights reserved.
*/


What can explain this behavior? Why am I ever being shown the iOS11 NSKeyedArchiver.h header if I'm compiling against iOS SDK 12?

I don’t have time to go through your entire post today — sorry, but it’s been a really busy day here on DevForums — but I think the main thing you’re missing is the concept of the root object (

NSKeyedArchiveRootObjectKey
). When you want to replace the unarchiver convenience routines (
+unarchiveObjectWithData
) with an unarchiver object, you have to tell it to decode using the root object. So, once you have an unarchiver you don’t pass it to
-initWithCoder:
, but instead call
-decodeObjectForKey:
, passing in
NSKeyedArchiveRootObjectKey
.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hello.


The following methods were added on iOS 11, but build errors on iOS 11 SDK will occur.

Because there is no entry in the header file. (Objective-C)

Since the methods has been added in iOS 12 SDK beta, it can be built.


+[NSKeyedArchiver archivedDataWithRootObject: requiringSecureCoding: error:]

+[NSKeyedUnarchiver unarchivedObjectOfClasses: fromData: error:]

+[NSKeyedUnarchiver unarchivedObjectOfClass: fromData: error:]


I sent a report to Bug Reporter, but since it is fixed with iOS 12 SDK beta, it seems that iOS 11 SDK will not be modified.

I want to build it with iOS 11 SDK (Xcode 9.4.1), but I am in trouble because I can not use it.


Thanks.

it seems that iOS 11 SDK will not be modified.

That’s not surprising. The iOS 11 SDK only ships as part of Xcode 9 and, given that Xcode 10 is in late beta, you’re unlikely to see any future updates to Xcode 9.

I want to build it with iOS 11 SDK (Xcode 9.4.1), but I am in trouble because I can not use it.

In some cases you can work around issues like this by adding a category with the require methods to one of your own headers. However, that can be risky because the OS does various runtime checks depending on the SDK you link against. If you do this, I recommend that you only do it in the short term; the right medium-term solution is to move to Xcode 10 once it is released.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I am surprised that this problem has been left for about a year.

Moreover, other methods are Deprecated on iOS 12, and you need to use these methods.


In other words, it may be that almost no new methods is used by anyone.

And I think that it will be used only after it becomes Deprecated.



In addition, our Developer Account has been brought to bad condition due to inappropriate review (80% was Fraud Review) by your company's App Review team.

I would like you to do this investigation. Or please tell me the contact address of the department to investigate.

In addition, the objections are completely ignored for 120 days.

It is abnormal that they ignore this for a long time like this. That seems to have hidden it.

In other words, I want you to investigate the fraud of the App Review team.

The root cause is Fraud Review with a probability of 80% or more of the App Review team.

Please tell me the contact address of the department to investigate.


Thanks.

I am surprised that this problem has been left for about a year.

What problem? The absence of these methods from the iOS 11 SDK was a deliberate decision. These methods were introduced for internal clients, which allows us to get some experience with how well they work. Once we were satisfied with their behaviour, they were moved to the public SDK. This is a very common lifecycle for new APIs.

The only odd thing here is that the availability was set to iOS 11. We only do this when we’re satisfied that the implementation in the old OS is a good match for the implementation in the new OS.

Moreover, other methods are Deprecated on iOS 12, and you need to use these methods.

Deprecations are an SDK thing, so you only see the deprecation warnings if you’re using the iOS 12 beta SDK. And if you use that SDK, you have access to the new methods. So, your choices are clear cut:

  • If you want to continue using the old SDK, stick with the old methods and you won’t see any deprecation warnings.

  • If you want to use the new SDK, write conditional code that uses the new methods when they are available.

In addition, our Developer Account …

I’m sorry but I can only help you with technical issues.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hello.


Is "absence" in the iOS 11 SDK was "deliberate decision"?

Currently, I patched the iOS 11 SDK and I build it, but are these "missing" methods not working in the iOS 11 environment?


I can not wait until the official version of the iOS 12 SDK! Because it is due to the fraud work of the App Review team.


Since many developers are in trouble with "cheating" of your company's App Review team, I want you to do something.

More than 80% is inappropriate review.

The developer account will be deleted when asking for the correct review. Therefore, the probability of fraudulent work increases. It deletes the developer's complainant account and silences it. And, it is actually such a state.

It's too bad.


Thanks.

I suspect you are complaining about a non-problem. You can't launch an app using iOS12 because it hasn't been released. Just develop under iOS11 and use the deprecated iOS11 commands. I am certain they will work fine for the next year. Sometime during that year, after iOS12 has been released, update the app using the contingent code.

Well, I have the same problem with you. And I found that:


If you are using a custom object, you'd better conform to the NSSecureCoding protocol, and implement some methods, then call

+[NSKeyedUnarchiver unarchivedObjectOfClass:fromData:error:] method will be ok.


If you conform to the NSCoding protocol, you can only call +[NSKeyedUnarchiver unarchiveObjectWithData:] method.


By the way, +[NSKeyedUnarchiver unarchivedObjectOfClass:fromData:error:] and +[NSKeyedUnarchiver unarchivedObjectOfClasses:fromData:error:] are different methods, you have to difference.


If you have an NSArray object with custom elements, you need to call +[NSKeyedUnarchiver unarchivedObjectOfClasses:fromData:error:] method for unarchiving, otherwise, call +[NSKeyedUnarchiver unarchivedObjectOfClass:fromData:error:] will be fine.

I added this extension to make it a more or less identical API signature:

extension NSKeyedUnarchiver {
    
    static func insecureUnarchivedObject<DecodedObjectType>(
        ofClass cls: DecodedObjectType.Type,
        from data: Data
    ) throws -> DecodedObjectType? where DecodedObjectType : NSObject, DecodedObjectType : NSCoding {
        
        let unarchiver = try NSKeyedUnarchiver.init(forReadingFrom: data)
        unarchiver.requiresSecureCoding = false
        let decodedType = unarchiver.decodeObject(forKey: NSKeyedArchiveRootObjectKey) as? DecodedObjectType

        return decodedType
    }
}