zoukankan      html  css  js  c++  java
  • ios Object Encoding and Decoding with NSSecureCoding Protocol


    Object Encoding and Decoding with NSSecureCoding Protocol

     

    February 27, 2014

    NSCoding is a fantastically simple and convenient way to store data on iOS or Mac OS by turning your model objects directly into a file and then loading it back into memory again, without needing to write any file parsing and serialization logic. To save any object to a data file (assuming that it implements the NSCoding protocol), you can just do this:

    Foo *someFoo = [[Foo alloc] init];
    [NSKeyedArchiver archiveRootObject:someFoo toFile:someFile];
    

    And to load it again later:

    Foo *someFoo = [NSKeyedUnarchiver unarchiveObjectWithFile:someFile];
    

    That’s fine for resources that are compiled into your app (such as nib files, which use NSCoding under the hood), but the problem with using NSCoding for reading and writing user data files is that, because you are encoding whole classes in a file, you are implicitly giving that file (with potentially unknown origin) permission to instantiate classes inside your app.

    Although you cannot store executable code in an NSCoded file (on iOS at least), a hacker could use a specially crafted file to trick your app into instantiating classes that you never intended it to, or do so in a different context to what you intended. Whilst it would be difficult to do any real harm in this way, it could certainly lead to app crashes and/or data loss for users.

    In iOS6, Apple introduced a new protocol built on top of NSCoding called NSSecureCoding. NSSecureCoding is identical to NSCoding, except that when decoding objects you specify both the key and class of the object you are decoding, and if the expected class doesn’t match the class of the object decoded from the file, the NSCoder will throw an exception to tell you that the data has been tampered with.

    Most of the system objects that support NSCoding have been upgraded to support NSSecureCoding, so by enabling secure coding on your NSKeyedUnarchiver, you can make sure that the data file you are loading is safe. You would do that like this:

    // Set up NSKeyedUnarchiver to use secure coding
    NSData *data = [NSData dataWithContentsOfFile:someFile];
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    [unarchiver setRequiresSecureCoding:YES];
     
    // Decode object
    Foo *someFoo = [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];
    

    Note that if you enable secure coding on your NSKeyedUnarchiver, every object stored in the file must conform to NSSecureCoding, otherwise you will get an exception. To tell the framework that your own classes support NSSecureCoding, you have to implement the new decoding logic in your initWithCoder: method, and return YES from the supportsSecureCoding method. There are no changes to the encodeWithCoder: method needed, as the security concerns are around loading, not saving.

    @interface Foo : NSObject 
     
    @property (nonatomic, strong) NSNumber *property1;
    @property (nonatomic, copy) NSArray *property2;
    @property (nonatomic, copy) NSString *property3;
     
    @end
     
    @implementation Foo
     
    + (BOOL)supportsSecureCoding
    {
      return YES;
    }
     
    - (id)initWithCoder:(NSCoder *)coder
    {
      if ((self = [super init]))
      {
        // Decode the property values by key, specifying the expected class
        _property1 = [coder decodeObjectOfClass:[NSNumber class] forKey:@"property1"];
        _property2 = [coder decodeObjectOfClass:[NSArray class] forKey:@"property2"];
        _property3 = [coder decodeObjectOfClass:[NSString class] forKey:@"property3"];
      }
      return self;
    }
     
    - (void)encodeWithCoder:(NSCoder *)coder
    {
      // Encode our ivars using string keys as normal
      [coder encodeObject:_property1 forKey:@"property1"];
      [coder encodeObject:_property2 forKey:@"property2"];
      [coder encodeObject:_property3 forKey:@"property3"];
    }
     
    @end
    

    A few weeks ago, I wrote about how to implement NSCoding automatically by using introspection to determine the properties of your class at runtime.

    This is a great way to add NSCoding support to all of your model objects without having to write repetitive and error-prone initWithCoder:/encodeWithCoder: methods. But the approach we used would not support NSSecureCoding because we do not attempt to validate the types of the objects being loaded.

    So how can we enhance our automatic NSCoding system to support NSSecureCoding right out of the box?

    If you recall, the original implementation worked by using the class_copyPropertyList() and property_getName() runtime functions to generate a list of property names, which we stored in an array:

    // Import the Objective-C runtime headers
    #import <objc/runtime.h> 
     
    - (NSArray *)propertyNames
    {    
      // Get the list of properties
      unsigned int propertyCount;
      objc_property_t *properties = class_copyPropertyList([self class], 
        &propertyCount);
      NSMutableArray *array = [NSMutableArray arrayWithCapacity:propertyCount];
      for (int i = 0; i < propertyCount; i++)
      {
        // Get property name
        objc_property_t property = properties[i];
        const char *propertyName = property_getName(property);
        NSString *key = @(propertyName);
     
        // Add to array
        [array addObject:key];
      }
     
      // Remember to free the list because ARC doesn't do that for us
      free(properties);
     
      return array;
    }
    

    Using KVC (Key-Value Coding), we were then able to set and get all the properties of an object by name and encode/decode them in an NSCoder object.

    To implement NSSecureCoding, we’ll follow the same principle, but instead of just getting the property names, we’ll also need to get their types. Fortunately, the objective C runtime stores detailed information about the types of class properties, so we can easily extract this data along with the names.

    Properties of a class can be either primitive types (such as integers, BOOLs and structs) or objects (such as NSString, NSArray, etc). The KVC valueForKey: and setValue:forKey: methods implement automatic “boxing” of primitive types, meaning that they will turn integers, BOOLs and structs into NSNumber and NSValue objects, respectively. That makes things a bit easier for us because we only have to deal with boxed types (objects), so we can represent all of our property types as classes, instead of having to call different decoding methods for different property types.

    The runtime methods don’t give us the boxed class name for each property though, instead they give us the type encoding – a specially formatted C-string containing the type information (this is the same format returned by the @encode(var); syntax). There’s no automatic way to get the equivalent boxed class for a primitive type, so we’ll need to parse this string and then specify the appropriate class ourselves.

    The format of a type encoding string is documented here.

    The first character represents the primitive type. Objective C uses a unique character for each supported primitive, for example ‘i’ represents an integer, ‘f’ a float, ‘d’ a double, and so on. Objects are represented by ‘@’ (followed by the class name) and then there are other more obscure types such as ‘:’ for selectors, or ‘#’ for classes.

    Struct and union types are represented by expressions wrapped in {…} brackets. Only some of these types are supported by the KVC mechanism, but the ones that are supported are always boxed as NSValue objects, so we can treat any value starting with ‘{‘ the same way.

    If we switch based on the first character of the string, we can handle all the known types:

    Class propertyClass = nil;
    char *typeEncoding = property_copyAttributeValue(property, "T");
    switch (typeEncoding[0])
    {
      case 'c': // Numeric types
      case 'i':
      case 's':
      case 'l':
      case 'q':
      case 'C':
      case 'I':
      case 'S':
      case 'L':
      case 'Q':
      case 'f':
      case 'd':
      case 'B':
      {
        propertyClass = [NSNumber class];
        break;
      }
      case '*': // C-String
      {
        propertyClass = [NSString class];
        break;
      }
      case '@': // Object
      {
        //TODO: get class name
        break;
      }
      case '{': // Struct
      {
        propertyClass = [NSValue class];
        break;
      }
      case '[': // C-Array
      case '(': // Enum
      case '#': // Class
      case ':': // Selector
      case '^': // Pointer
      case 'b': // Bitfield
      case '?': // Unknown type
      default:
      {
        propertyClass = nil; // Not supported by KVC
        break;
      }
    }
    free(typeEncoding);
    

    To handle ‘@’ types, we need to extract the class name. The class name may include protocols (which we don’t really need to worry about), so we’ll split the string to extract just the class name, and then use NSClassFromString to get the class:

    case '@':
    {
      // The objcType for classes will always be at least 3 characters long
      if (strlen(typeEncoding) >= 3)
      {
        // Copy the class name as a C-String
        char *cName = strndup(typeEncoding + 2, strlen(typeEncoding) - 3);
     
        // Convert to an NSString for easier manipulation
        NSString *name = @(cName);
     
        // Strip out and protocols from the end of the class name
        NSRange range = [name rangeOfString:@"<"];
        if (range.location != NSNotFound)
        {
          name = [name substringToIndex:range.location];
        }
     
        // Get class from name, or default to NSObject if no name is found
        propertyClass = NSClassFromString(name) ?: [NSObject class];
        free(cName);
      }
      break;
    }
    

    Finally, we can combine this parsing logic with the propertyNames method logic from our previous implementation to create a method that returns a dictionary of property classes, keyed by property name. Here is the complete implementation:

    - (NSDictionary *)propertyClassesByName
    {
      // Check for a cached value (we use _cmd as the cache key, 
      // which represents @selector(propertyNames))
      NSMutableDictionary *dictionary = objc_getAssociatedObject([self class], _cmd);
      if (dictionary)
      {
          return dictionary;
      }
     
      // Loop through our superclasses until we hit NSObject
      dictionary = [NSMutableDictionary dictionary];
      Class subclass = [self class];
      while (subclass != [NSObject class])
      {
        unsigned int propertyCount;
        objc_property_t *properties = class_copyPropertyList(subclass, 
          &propertyCount);
        for (int i = 0; i < propertyCount; i++)
        {
          // Get property name
          objc_property_t property = properties[i];
          const char *propertyName = property_getName(property);
          NSString *key = @(propertyName);
     
          // Check if there is a backing ivar
          char *ivar = property_copyAttributeValue(property, "V");
          if (ivar)
          {
            // Check if ivar has KVC-compliant name
            NSString *ivarName = @(ivar);
            if ([ivarName isEqualToString:key] || 
              [ivarName isEqualToString:[@"_" stringByAppendingString:key]])
            {
              // Get type
              Class propertyClass = nil;
              char *typeEncoding = property_copyAttributeValue(property, "T");
              switch (typeEncoding[0])
              {
                case 'c': // Numeric types
                case 'i':
                case 's':
                case 'l':
                case 'q':
                case 'C':
                case 'I':
                case 'S':
                case 'L':
                case 'Q':
                case 'f':
                case 'd':
                case 'B':
                {
                  propertyClass = [NSNumber class];
                  break;
                }
                case '*': // C-String
                {
                  propertyClass = [NSString class];
                  break;
                }
                case '@': // Object
                {
                  //TODO: get class name
                  break;
                }
                case '{': // Struct
                {
                  propertyClass = [NSValue class];
                  break;
                }
                case '[': // C-Array
                case '(': // Enum
                case '#': // Class
                case ':': // Selector
                case '^': // Pointer
                case 'b': // Bitfield
                case '?': // Unknown type
                default:
                {
                  propertyClass = nil; // Not supported by KVC
                  break;
                }
              }
              free(typeEncoding);
     
              // If known type, add to dictionary
              if (propertyClass) dictionary[propertyName] = propertyClass;
            }
            free(ivar);
          }
        }
        free(properties);
        subclass = [subclass superclass];
      }
     
      // Cache and return dictionary
      objc_setAssociatedObject([self class], _cmd, dictionary, 
        OBJC_ASSOCIATION_RETAIN_NONATOMIC);
      return dictionary;
    }
    

    That’s the hard part done. Now, to implement NSSecureCoding, we’ll just modify the initWithCoder: method from our previous automatic coding implementation to take the property class into account when parsing. We’ll also need to return YES from the supportsSecureCoding method:

    + (BOOL)supportsSecureCoding
    {
      return YES;
    }
     
    - (id)initWithCoder:(NSCoder *)coder
    {
      if ((self = [super init]))
      {
        // Decode the property values by key, specifying the expected class
        [[self propertyClassesByName] enumerateKeysAndObjectsUsingBlock:(void (^)(NSString *key, Class propertyClass, BOOL *stop)) {
          id object = [aDecoder decodeObjectOfClass:propertyClass forKey:key];
          if (object) [self setValue:object forKey:key];
        }];
      }
      return self;
    }
     
    - (void)encodeWithCoder:(NSCoder *)aCoder
    {
      for (NSString *key in [self propertyClassesByName])
      {
        id object = [self valueForKey:key];
        if (object) [aCoder encodeObject:object forKey:key];
      }
    }
    

    And there you have it: A simple base class for your models that supports NSSecureCoding out of the box. Alternatively, you can use my AutoCoding category that uses this approach to add automatic NSCoding and NSSecureCoding support to any object that doesn’t already implement it.


    Nick Lockwood is the author of iOS Core Animation: Advanced Techniques. Nick also wrote iCarousel, iRate and other Mac and iOS open source projects.
  • 相关阅读:
    mysql中in 做条件匹配 带逗号的字符串 数据异常解决
    [LeetCode] Kth Largest Element in a Stream 数据流中的第K大的元素
    [LeetCode] Binary Search 二分搜索法
    [LeetCode] Search in a Sorted Array of Unknown Size 在未知大小的有序数组中搜索
    [LeetCode] Insert into a Binary Search Tree 二叉搜索树中插入结点
    [LeetCode] Search in a Binary Search Tree 二叉搜索树中搜索
    [LeetCode] Design Circular Deque 设计环形双向队列
    [LeetCode] Design Circular Queue 设计环形队列
    [LeetCode] N-ary Tree Preorder Traversal N叉树的前序遍历
    [LeetCode] 589. N-ary Tree Postorder Traversal N叉树的后序遍历
  • 原文地址:https://www.cnblogs.com/tinkl/p/3667572.html
Copyright © 2011-2022 走看看