Using SecureCoding for NSKeyedArchiver

Switching to Swift 5 and IOS 12, I need to conform to NSSecureCoding for Archiver.

I tried using Codable, but I need to store an array of [String: Any] dictionaries and could not get it work.

Maybe JSON encoder could be a good option here ?


The class is pretty simple.


let theKey = "theKey"

class MyFavorites : NSObject, NSCoding {

    var myData: [[String: Any]]?

    override init() {
        super.init()
        myData = []
    }

    required init(coder decoder: NSCoder) {

        mesVEs = decoder.decodeObject(forKey: favorisVEKey) as? [[String: Any]]
    }

    func encode(with coder: NSCoder) {
        if let savedData = myData {
            coder.encode(savedData, forKey: theKey)
        }
    }


Archiving and unArchiving are done in a utlity class:


class Util {

    class func dataFilePath() -> String {

        let paths = NSSearchPathForDirectoriesInDomains(
            FileManager.SearchPathDirectory.documentDirectory,
            FileManager.SearchPathDomainMask.userDomainMask, true)
        let documentsDirectory = paths[0] as NSString
        return documentsDirectory.appendingPathComponent("data.archive") as String
    }

     class func loadData() -> MyFavorites {

        let filePath = Util.dataFilePath()
        if (FileManager.default.fileExists(atPath: filePath)) {
            let data = NSMutableData(contentsOfFile: filePath)!
            let unarchiver = NSKeyedUnarchiver(forReadingWith: data as Data)
            let allFavoris = unarchiver.decodeObject(forKey: rootKey) as! MyFavorites // whole dictionary
            unarchiver.finishDecoding()
    
            return allFavoris
        } else {
            return MyVEFavoris()
        }
    }

    class func saveData() {

        let filePath = dataFilePath()
        let favorisToSave = MyFavorites()
        favorisToSave.myData =     //  content read from a stored array of dictionaries

        let data = NSMutableData()
        let archiver = NSKeyedArchiver(forWritingWith: data)
        archiver.encode(favorisToSave, forKey: rootKey)
        archiver.finishEncoding()
        data.write(toFile: filePath, atomically: true)
    }



To make it conform to SecureCoding, I tried to minimize the changes (may be not the best option), so I changed the class as follows:



class MyFavorites : NSObject, NSSecureCoding {

    var myData: [[String: Any]]?    // No change

    static var supportsSecureCoding: Bool {
        return true
    }

    override init() {
        super.init()       // No change here
        self.myData = []
    }

    required init(coder decoder: NSCoder) {
        if let topObject = decoder.decodeObject(of: MyFavorites.self, forKey: theKey) {
            myData = topObject.myData
        } else {
            self.myData = []
        }
    }

     func encode(with coder: NSCoder) {
        coder.encode(self, forKey: theKey)
    }


Now, I am struggling to adapt the calls to Archiver (loadData and SaveData)


     class func loadData() -> MyFavorites {

        let filePath = Util.dataFilePath()
        if (FileManager.default.fileExists(atPath: filePath)) {
            guard let dataObject = NSData(contentsOfFile: filePath) else { return MyFavorites() }
            guard let unarchived = try? NSKeyedUnarchiver.unarchivedObject(ofClass: MyFavorites.self, from: dataObject as! Data)
                    else { return MyFavorites() }
            guard let unarchivedArray = unarchived as? [[String: Any]]
                    else { return MyFavorites() }

            let returnFavorites = MyFavorites()
            returnFavorites.myData = unarchivedArray
            return returnFavorites
        } else {
            return MyFavorites()
        }


And cannot find a way to adapt NSKeyedArchiver to secureCoding

Accepted Reply

I do not think your revised `MyFavorite` is properly conforming to `NSSecureCoding`.


You may need to encode and decode your `myData` securely.

class MyFavorites : NSObject, NSSecureCoding {
    
    var myData: [[String: Any]]?    // No change
    
    static var supportsSecureCoding: Bool {
        return true
    }
    
    override init() {
        super.init()       // No change here
        self.myData = []
    }

    required init(coder decoder: NSCoder) {
        myData = decoder.decodeObject(of: [
                NSArray.self, NSDictionary.self, MyItem.self, NSDate.self, //...
            ], forKey: theKey) as? [[String: Any]] ?? []

    }
    
    func encode(with coder: NSCoder) {
        if let savedData = myData {
            coder.encode(savedData, forKey: theKey)
        }
    }
}

You need to specify all possible classes stored in `myData` as Objective-C bridged type.

(As far as I tried, you have no need to specify NSString, NSNumber or NSData.)


With the above implementation, you can write `loadData()` and `saveData()` easily:

class Util {
    
    static func dataFileURL() -> URL {
        let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let documentsDirectoryURL = urls[0]
        return documentsDirectoryURL.appendingPathComponent("data.archive")
    }

    static func loadData() -> MyFavorites {
        let fileURL = Util.dataFileURL()
        if FileManager.default.fileExists(atPath: fileURL.path) {
            do {
                let data = try Data(contentsOf: fileURL)
                guard let unarchived = try NSKeyedUnarchiver.unarchivedObject(ofClass: MyFavorites.self, from: data) else {
                    return MyFavorites()
                }
                return unarchived
            } catch {
                print(error)
                return MyFavorites()
            }
        } else {
            return MyFavorites()
        }
    }
    
    static func saveData() {
        let fileURL = dataFileURL()
        let favorisToSave = MyFavorites()
        favorisToSave.myData = readContentForMyData() // content read from a stored array of dictionaries

        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: favorisToSave, requiringSecureCoding: true)
            try data.write(to: fileURL)
        } catch {
            print(error)
            //You may need some better error handling...
        }
    }
}

My preferred way to work with file paths recently is using `URL`, so I changed `dataFilePath()` to `dataFileURL()`. And I changed `class func` to `static func`. If you think each method requires to be overridable, please revert it to `class func`.

Replies

I do not think your revised `MyFavorite` is properly conforming to `NSSecureCoding`.


You may need to encode and decode your `myData` securely.

class MyFavorites : NSObject, NSSecureCoding {
    
    var myData: [[String: Any]]?    // No change
    
    static var supportsSecureCoding: Bool {
        return true
    }
    
    override init() {
        super.init()       // No change here
        self.myData = []
    }

    required init(coder decoder: NSCoder) {
        myData = decoder.decodeObject(of: [
                NSArray.self, NSDictionary.self, MyItem.self, NSDate.self, //...
            ], forKey: theKey) as? [[String: Any]] ?? []

    }
    
    func encode(with coder: NSCoder) {
        if let savedData = myData {
            coder.encode(savedData, forKey: theKey)
        }
    }
}

You need to specify all possible classes stored in `myData` as Objective-C bridged type.

(As far as I tried, you have no need to specify NSString, NSNumber or NSData.)


With the above implementation, you can write `loadData()` and `saveData()` easily:

class Util {
    
    static func dataFileURL() -> URL {
        let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let documentsDirectoryURL = urls[0]
        return documentsDirectoryURL.appendingPathComponent("data.archive")
    }

    static func loadData() -> MyFavorites {
        let fileURL = Util.dataFileURL()
        if FileManager.default.fileExists(atPath: fileURL.path) {
            do {
                let data = try Data(contentsOf: fileURL)
                guard let unarchived = try NSKeyedUnarchiver.unarchivedObject(ofClass: MyFavorites.self, from: data) else {
                    return MyFavorites()
                }
                return unarchived
            } catch {
                print(error)
                return MyFavorites()
            }
        } else {
            return MyFavorites()
        }
    }
    
    static func saveData() {
        let fileURL = dataFileURL()
        let favorisToSave = MyFavorites()
        favorisToSave.myData = readContentForMyData() // content read from a stored array of dictionaries

        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: favorisToSave, requiringSecureCoding: true)
            try data.write(to: fileURL)
        } catch {
            print(error)
            //You may need some better error handling...
        }
    }
}

My preferred way to work with file paths recently is using `URL`, so I changed `dataFilePath()` to `dataFileURL()`. And I changed `class func` to `static func`. If you think each method requires to be overridable, please revert it to `class func`.

Many thanks, that works well.


I just get an error message :

Error Domain=NSCocoaErrorDomain Code=4865 "requested key: 'root'" UserInfo={NSDebugDescription=requested key: 'root'}

Error Domain=NSCocoaErrorDomain Code=4865 "requested key: 'root'" UserInfo={NSDebugDescription=requested key: 'root'}


It is a bit a shame that XCode documentation is so poor.

At least, when an API is deprecated, it would be great to have some detail on how to migrate to the new one. But document is getting worse and worse, now a mere reference for API signatures, less and less a man pages.

It is a bit a shame that XCode documentation is so poor.


Strongly agreed. Apple's engineers should use enough time to provide a better documentation.

We need to make many cuts and tries when miigrating deprecated methods, and that actually is leading many mis-interpreted implementations...


Maybe we developers need to send more and more bug reports on documentation.


---

About the errors, can you create a simple code (works on a Command Line Tool project would be better) and show whole code and data enough to reproduce the issue?

I will file a bug against documentation, you're right.


after I read here

https://stackoverflow.com/questions/32904811/swift-only-way-to-prevent-nskeyedunarchiver-decodeobject-crash/37163130


I added @objc at class declaration and that cleared the issue…


Could be documented as well…


EDITED:

filed a suggestion report : # 49 875 853