How to use NSSecureCoding in Swift

Issue #334

NSSecureCoding has been around since iOS 6 and has had some API changes in iOS 12

A protocol that enables encoding and decoding in a manner that is robust against object substitution attacks.

https://developer.apple.com/documentation/foundation/nscoder/2292924-decodeobject

If the coder responds true to requiresSecureCoding, then the coder calls failWithError(_:) in either of the following cases:
The class indicated by cls doesn’t implement NSSecureCoding.
The unarchived class doesn’t match cls, nor do any of its superclasses.

If the coder doesn’t require secure coding, it ignores the cls parameter and does not check the decoded object.

The class must subclass from NSObject and conform to NSSecureCoding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Note: NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool = true

func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: "id")
aCoder.encode(text, forKey: "text")
aCoder.encode(date, forKey: "date")
}

required init?(coder aDecoder: NSCoder) {
guard
let id = aDecoder.decodeObject(of: [NSString.self], forKey: "id") as? String,
let text = aDecoder.decodeObject(of: [NSString.self], forKey: "text") as? String,
let date = aDecoder.decodeObject(of: [NSDate.self], forKey: "date") as? Date
else {
return nil
}

self.id = id
self.text = text
self.date = date
}

let id: String
var text = "untitled"
var date: Date = Date()

override init() {
id = UUID().uuidString
super.init()
}
}

First, we need to serialize to Data, then use EasyStash for easy persistency

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
do {
let securedItems = items.map({ SecuredClientLoggerItem(item: $0) })
if #available(iOS 11.0, *) {
let data = try NSKeyedArchiver.archivedData(
withRootObject: securedItems,
requiringSecureCoding: true
)

try data.write(to: fileUrl)
} else {
_ = NSKeyedArchiver.archiveRootObject(
securedItems,
toFile: fileUrl.path
)
}
} catch {
print(error)
}

Then we can use unarchiveTopLevelObjectWithData to unarchive array

1
2
3
4
5
6
7
do {
let data = try Data(contentsOf: fileUrl)
let notes = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [Note]
// notes is of type [Note]?
} catch {
print(error)
}

Note that for UUID, NSCoding seems to convert to UUID instead of String

1
2
3
4
let id = aDecoder.decodeObject(
of: [NSUUID.self],
forKey: "id"
) as? UUID,

Comments