granoff/Strongbox

NSSecureCoding

Closed this issue · 16 comments

I want to store a dictionary that stores a custom enum and a bool. How exactly do I make that conform to NSSecureCoding?

I also keep getting this error:

[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x28283c480 -[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x28283c480

Basic types like bool should store just fine, but a custom enum itself cannot be made to conform to NSSecureCoding.

I suggest creating a class whose members are your bool and an instance of your custom enum. Then make the class conform to NSSecureCoding. You'll have to write encode() and decode() methods that pack and unpack the member variables. For the custom enum, you would encode the member variable's raw value (something like an int or string, yes?) and you could decode that raw value to then create an enum from it.

The error you're seeing means that the value you are trying to encode doesn't conform to NSSecureCoding. For a custom type, you have to write that support yourself by conforming the type to the protocol and implementing encode() and decode().

I've made sure my type conforms to the protocol, but I am still getting the same error. Is there anyway I could send you my code?

Sure. Is the relevant code short enough to post here?

Should be. Thank you in advance for your help.

class PurchaseStorage: NSSecureCoding {
    private var purchased: [IAPProduct: Bool] = [
        IAPProduct.december31Solution: false,
        IAPProduct.january1Solution: false
    ]
    
    static let shared = PurchaseStorage()
    static var supportsSecureCoding: Bool {return true}
    private init() {}
    static let key = "PurchaseStorageKey"
    private let strongBox = Strongbox()
    
    private init(purchased: [IAPProduct: Bool]) {
        self.purchased = purchased
    }
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(purchased, forKey: PurchaseStorage.key)
    }
    
    required convenience init?(coder aDecoder: NSCoder) {
        let purchased = aDecoder.decodeObject(forKey: PurchaseStorage.key) as! [IAPProduct:Bool]
        self.init(purchased: purchased)
    }
    
    func getIsPurchased(product: IAPProduct) -> Bool {
        return purchased[product]!
    }
    
    func setPurchased(product: IAPProduct) {
        purchased[product] = true
        
        if strongBox.archive(purchased, key: PurchaseStorage.key) {
            print("archived")
        } else {
            print("not archived")
        }
    }
    
    func unarchive() {
        if (strongBox.unarchive(objectForKey: PurchaseStorage.key) as? PurchaseStorage) != nil {
            print("unarchived")
        } else {
            print("not unarchived")
        }
    }
}

I have also tried this:

class PurchaseStorage: NSSecureCoding {
    private var purchased: [IAPProduct: Bool] = [
        IAPProduct.december31Solution: true,
        IAPProduct.january1Solution: true
    ]
    
    static var shared = PurchaseStorage()
    static var supportsSecureCoding: Bool {return true}
    private init() {}
    static let key = "PurchaseStorageKey"
    private let strongBox = Strongbox()
    
    private init(purchased: [IAPProduct: Bool]) {
        self.purchased = purchased
    }
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(purchased, forKey: PurchaseStorage.key)
    }
    
    required convenience init?(coder aDecoder: NSCoder) {
        let purchased = aDecoder.decodeObject(forKey: PurchaseStorage.key) as! [IAPProduct:Bool]
        self.init(purchased: purchased)
    }
    
    func getIsPurchased(product: IAPProduct) -> Bool {
        return purchased[product]!
    }
    
    func setPurchased(product: IAPProduct) {
        purchased[product] = true
        
        if strongBox.archive(PurchaseStorage.shared, key: PurchaseStorage.key) {
            print("archived")
        } else {
            print("not archived")
        }
    }
    
    func unarchive() {
        if let shared = strongBox.unarchive(objectForKey: PurchaseStorage.key) as? PurchaseStorage {
            PurchaseStorage.shared = shared
            print("unarchived")
        } else {
            print("not unarchived")
        }
    }
}

What is the definition of IAPProduct look like?

Your implementation of encode() is on the right track, but you will have to be more explicit with your encoding. The enum key of the dictionary still isn't encodable on its own. So you might need to convert purchased into something more suitable for automatic encoding, e.g. [String: Bool] and encode that. You will also need to implement decode() to turn whatever you store back into the type you want, [IAPProduct: Bool].

I know it seems like supporting NSSecureCoding "un-does" a lot of the convenience of enums, but that's the price to pay. Even without using Strongbox, to store your dictionary in just a plist, or in NSUserDefaults (both insecure), you would have to convert to basic types.

Thank you for this information.

The definition of IAPProduct is:

enum IAPProduct: String {
    case december31Solution = "Actual key omitted for privacy reasons"
    case january1Solution = "Actual key omitted for privacy reasons"
}

Just to clarify, when you say,

The enum key of the dictionary still isn't encodable on its own. So you might need to convert purchased into something more suitable for automatic encoding, e.g. [String: Bool] and encode that.

Isn't that the same a getting the raw value of the enum since the raw value is itself a String?

The enum key of the dictionary still isn't encodable on its own.

I'm not trying to encode the enum key of the dictionary. I'm trying to encode the dictionary itself since it stores flag variables about whether or not my in-app purchases have been purchased. The keys in the dictionary are just the cases in my enum. I hope that makes sense.

That's what I thought the enum might look like. Thanks.

Yes, you would need to encode the raw value, a string, because an enum is not encodable by itself. Then to decode, you pull out the string and use it to inflate the enum.

I'm not trying to encode the enum itself, though. I'm just using it in my dictionary. Does that matter?

I'm not trying to encode the enum key of the dictionary. I'm trying to encode the dictionary itself since it stores flag variables about whether or not my in-app purchases have been purchased. The keys in the dictionary are just the cases in my enum. I hope that makes sense.

Yeah, but Swift sees a dictionary of type [IAPProduct: Bool]. IAPProduct is not a basic type that "just suports" secure coding. That the type is used in a dictionary, which is a supported type for coding, doesn't mean you get a free pass for whatever is in the dictionary. Every type the dictionary uses must also support coding. If you are using a custom type, like you are for the key, then that type has to support coding, and you'll have to write that support yourself. It might be easier to change your dictionary type to [String: Bool], in which case you would not have to write any custom support for coding. Or, keep your custom types, but you'll have to write more code and ultimately, you might be storing a dictionary of type [String: Bool] anyway. Ask yourself if having IAPProduct is really useful outside this dictionary use case? How many cases are inside IAPProduct? Just 2? Maybe some static string constants would suffice. Just a thought.

From the README:

Strongbox makes it easy to store and retrieve any Foundation-based object that conforms to NSSecureCoding

"Foundation-based" is the key, meaning: basic types. But, that doesn't preclude using custom types that you make support NSSecureCoding.

"Foundation-based" is the key, meaning: basic types. But, that doesn't preclude using custom types that you make support NSSecureCoding.

Got it. I totally missed the meaning behind that on the first read-through. I will try to rework this and let you know if I run into additional problems.

Great!