Question

I have an object of class SGBContainer which has an array named objects that contains objects of class SGBObject. Currently, they each implement NSCoding but not NSSecureCoding. The -initWithCoder: for SGBContainer looks like this:

- (id)initWithCoder:(NSCoder *)aCoder
{
    self = [self init];
    if (self)
    {
        _objects = [aCoder decodeObjectForKey:@"objects"];
    }
}

I want to switch to using NSSecureCoding, and from what I can tell, it would mean changing the above to this:

- (id)initWithCoder:(NSCoder *)aCoder
{
    self = [self init];
    if (self)
    {
        _objects = [aCoder decodeObjectOfClass:[NSArray class] forKey:@"objects"];
    }
}

...which isn't much of an improvement, as the contents of the array will be instantiated whatever their class. How do I make sure that the array only contains objects of class SGBObject without instantiating them?

Was it helpful?

Solution

Although it's not at all clear from the documentation (and just sounds like an oddly ungrammatical method name), this is what -decodeObjectOfClasses:forKey: does. You'd do something like the following:

NSSet *classes = [NSSet setWithObjects:[NSArray class], [SGBObject class], nil];
_objects = [aCoder decodeObjectOfClasses:classes forKey:@"objects"];

(Credit where it's due: See NSSecureCoding trouble with collections of custom class)

OTHER TIPS

There is no direct way of doing this with NSSecureCoding, as NSCoder isn't collection aware. You have to manually sanitize the array to make sure it contains only objects of type SGBObject (which, to be fair, kind of defeats the purpose of NSSecureCoding).

An alternative would be to encode and decode your array yourself, instead of relying on NSCoder for doing so.

Sean D.'s answer is correct to unblock the code, but there is one caveat: the foundation does not guarantee the class types inside of the array, IF they are foundation classes. E.g. the following code demonstrates the problem:

@implementation Serializable
- (id)initWithCoder:(NSCoder *)aDecoder {
  return [super init];
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
}
+ (BOOL)supportsSecureCoding {
  return YES;
}
@end

int main(int argc, const char * argv[]) {

  NSArray *foo = @[ @1, @"Gotcha", [Serializable new] ];
  NSMutableData *archive = [NSMutableData data];
  NSKeyedArchiver *archiver =
      [[NSKeyedArchiver alloc] initForWritingWithMutableData:archive];
  archiver.requiresSecureCoding = YES;
  [archiver encodeObject:foo forKey:@"root"];
  [archiver finishEncoding];

  NSKeyedUnarchiver *unarchiver = 
     [[NSKeyedUnarchiver alloc] initForReadingWithData:archive];
  unarchiver.requiresSecureCoding = YES;
  NSSet *classes = [NSSet setWithArray:@[ [NSArray class], [Serializable class] ]];
  // Should throw, but it does not.
  NSArray *loaded = [unarchiver decodeObjectOfClasses:classes forKey:NSKeyedArchiveRootObjectKey];
  if (loaded.count > 2 && ![loaded[0] isKindOfClass:[Serializable class]]) {
    NSLog(@"Successfully performed object substitution attack.");
    NSLog(@"Class: %@", [loaded[0] class]);  // All 3 objects are
    return 1;
  }
  return 0;
}

Here is Swift 4.2 version:

Let's assume you have you custom class CustomClass. Make sure it conforms NSSecureCoding protocol.

class CustomClass: NSObject, NSCopying, NSSecureCoding {
    var longName: String
    var shortName: String

    init(longName: String, shortName: String) {
        self.longName = longName
        self.shortName = shortName
    }

    // MARK: - NSCoding Protocol

    required convenience init(coder aDecoder: NSCoder) {
        let longName = (aDecoder.decodeObject(forKey: "longName") as? String ?? "")
        let shortName = (aDecoder.decodeObject(forKey: "shortName") as? String ?? "")

        self.init(longName: longName, shortName: shortName)
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(self.longName, forKey: "longName")
        aCoder.encode(self.shortName, forKey: "shortName")
    }

    // MARK: - NSSecureCoding Protocol

    static var supportsSecureCoding: Bool = true

    // MARK: - NSCopying Protocol

    func copy(with zone: NSZone? = nil) -> Any {
        return CustomClass(longName: self.longName, shortName: self.shortName)
    }
}

First you need to convert your objects array to the binary data and archive it:

let userDefaults = UserDefaults.standard
let customObjectsData = try? NSKeyedArchiver.archivedData(withRootObject: customObjectsArray, requiringSecureCoding: true)
userDefaults.set(customObjectsData, forKey: USER_DEFAULTS_DATA_KEY)
userDefaults.synchronize()

After this you are able to read the data from the UserDefaults and unarchive it:

if let customObjectsData = UserDefaults.standard.data(forKey: USER_DEFAULTS_DATA_KEY) {
    if let customObjects = (try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, CustomObject.self], from: customObjectsData)) as? [CustomObject] {
        // your code
    }
}

Try

NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:archivedData];
unarchiver.requiresSecureCoding = YES;
MyClass *myClass = [unarchiver decodeObjectOfClass:[MyClass class] forKey:@"root"];

If you don't set unarchiver.requiresSecureCoding = YES, then no checks will be applied.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top