Skip to content

Adopting Secure Coding in SecureStoreCache and SecureKeyValueStore

Introduction

With SAP BTP SDK for iOS v26.8, SecureStoreCache and SecureKeyValueStore classes from SAPFoundation adopt Apple's secure archiving/unarchiving APIs, with an option to keep using the older APIs. This is controlled by a new initializer flag requiresSecureCoding which defaults to false.

This guide explains how to use the new requiresSecureCoding flag and the allowedClasses parameter when reading values from these classes.

You Do Not Have To Migrate Now

requiresSecureCoding defaults to false. If you do nothing, your existing NSCoding types and existing code continue to work with no change in behavior.

    // Still perfectly valid
    let cache = SecureStoreCache<MyValue>(secureStore: store)
    let keyValueStore    = SecureKeyValueStore(name: "MyStore")

However, you should plan to migrate eventually. The path below lets you move your types one at a time whenever you are ready.

Requirements

To adopt secure coding with SecureStoreCache and SecureKeyValueStore, three requirements must be met.

  1. Conform your types to NSSecureCoding.

    Every type you persist — and every nested type it contains — must adopt NSSecureCoding instead of NSCoding. See Conforming to NSSecureCoding.

  2. Opt in with the requiresSecureCoding flag.

    Create the SecureStoreCache or SecureKeyValueStore with requiresSecureCoding: true so archiving and unarchiving use Apple's secure APIs. See Opting In To Secure Coding.

  3. Read using the allowedClasses methods.

    While reading values, use the new methods that incorporate an allowedClasses parameter, declaring every class that may appear in the decoded graph. See Read Using The New Methods.

Once these code changes are in place, data written with the older SDK APIs can still be read with the newer secure APIs — no data migration or rewrite is required.

Conforming to NSSecureCoding

NSSecureCoding is a protocol for encoding and decoding in a manner that is robust against object substitution attacks. It is NSCoding plus a static property that declares support. You keep your existing encode(with:) and init?(coder:) — you only add the conformance and the supportsSecureCoding property.

BEFORE (NSCoding):

    class MyValue: NSObject, NSCoding {
        let id: Int
        let name: String
        let isActive: Bool
        let score: Double

        init(id: Int, name: String, isActive: Bool, score: Double) {
            self.id = id
            self.name = name
            self.isActive = isActive
            self.score = score
        }

        required init?(coder: NSCoder) {
            self.id       = coder.decodeInteger(forKey: "id")
            self.name     = coder.decodeObject(forKey: "name") as? String ?? ""
            self.isActive = coder.decodeBool(forKey: "isActive")
            self.score    = coder.decodeDouble(forKey: "score")
        }

        func encode(with coder: NSCoder) {
            coder.encode(id, forKey: "id")
            coder.encode(name, forKey: "name")
            coder.encode(isActive, forKey: "isActive")
            coder.encode(score, forKey: "score")
        }
    }

AFTER (NSSecureCoding):

    class MyValue: NSObject, NSSecureCoding {

        // 1. Declare secure coding support.
        static var supportsSecureCoding: Bool { true }

        let id: Int
        let name: String
        let isActive: Bool
        let score: Double

        init(id: Int, name: String, isActive: Bool, score: Double) {
            self.id = id
            self.name = name
            self.isActive = isActive
            self.score = score
        }

        // 2. encode(with:) is UNCHANGED
        func encode(with coder: NSCoder) {
            coder.encode(id, forKey: "id")
            coder.encode(name, forKey: "name")
            coder.encode(isActive, forKey: "isActive")
            coder.encode(score, forKey: "score")
        }

        // 3. init?(coder:) — primitive types (Int, Bool, Double) are UNCHANGED.
        //    when decoding objects, specify which class you're expecting using the secure api decodeObject(of:forKey: instead of the plain decodeObject(forKey:).
        required init?(coder: NSCoder) {
            self.id       = coder.decodeInteger(forKey: "id")
            self.isActive = coder.decodeBool(forKey: "isActive")
            self.score    = coder.decodeDouble(forKey: "score")
            // For String types, decode it securely against NSString.
            self.name     = coder.decodeObject(of: NSString.self,
                                               forKey: "name") as String? ?? ""
        }
    }
  • If your object holds other objects (a custom sub-object, or an object inside an array/dictionary/set), those types must also conform to NSSecureCoding.

  • When decoding an object or a nested object you must use the secure decode API decodeObject(OfClass:forKey:), e.g.

        let child = coder.decodeObject(of: Child.self, forKey: "child")

and for containers:

        let items = coder.decodeObject( of: [NSArray.self, Child.self], forKey: "items") as? [Child]

Every class that can appear (the container class and its element classes) must be in the list.

Opting In To Secure Coding

Once your type conforms to NSSecureCoding, create the store/cache with the requiresSecureCoding flag set to true:

    // Cache
    let cache = SecureStoreCache<MyValue>(secureStore: store, requiresSecureCoding: true)

    // Key-value store
    let keyValueStore = SecureKeyValueStore(name: "MyStore", requiresSecureCoding: true)

From now on this instance archives and unarchives using the secure APIs.

Using The New Methods For Reading

Writing remains unchanged — continue to call set(value:forKey:) (SecureStoreCache) or put(_:forKey:) (SecureKeyValueStore). The archiver now enforces secure coding under the hood.

Reading requires the use of new methods to declare the expected classes.

    // Cache read
    let value = cache.value(forKey: "myKey", allowedClasses: [MyValue.self])

    let entry = cache.entry(forKey: "myKey", allowedClasses: [MyValue.self])

    // Key-value read
    let value: MyValue? = try keyValueStore.get("myKey", allowedClasses: [MyValue.self])

    let value2: MyValue? = try keyValueStore.get("myKey", defaultValue: fallback, allowedClasses: [MyValue.self])

allowedClasses must contain every class that can appear in the decoded graph — the root type and any nested types, plus container classes (NSArray, NSDictionary, NSSet) when the value is a collection. Example:

    // A cached array of MyValue:
    let list = cache.value(forKey: "list", allowedClasses: [NSArray.self, MyValue.self])

If the allowedClasses does not contain all the class types then there will be failures. The two classes behave differently on failure.

SecureStoreCache returns nil along with an error log which has the description of the underlying error.

SecureKeyValueStore throws SecureStorageError.typeConversionFailed along with an error log which has the description of the underlying error.

Changes to Data Stored in Database

Enabling requiresSecureCoding does not change the shape of the data stored in the database. The value is still stored the same way in the database store. The change affects only the code path used to archive/unarchive it — it now uses the secure APIs that validate the decoded class against allowedClasses.

Consequences:

  • No database migration is required.
  • Archiving/unarchiving simply becomes more secure (tamper-resistant).

Example

Illustration below shows the code BEFORE and AFTER the change to adopt secure coding is done.

BEFORE

NSCoding types with the previous SDK APIs.

    import Foundation
    import SAPFoundation

    class Address: NSObject, NSCoding {
        let street: String
        let city: String

        init(street: String, city: String) {
            self.street = street
            self.city = city
        }

        required init?(coder: NSCoder) {
            self.street = coder.decodeObject(forKey: "street") as? String ?? ""
            self.city   = coder.decodeObject(forKey: "city") as? String ?? ""
        }

        func encode(with coder: NSCoder) {
            coder.encode(street, forKey: "street")
            coder.encode(city, forKey: "city")
        }
    }

    class UserProfile: NSObject, NSCoding {
        let id: Int
        let name: String
        let isActive: Bool
        let address: Address          // <-- nested NSCoding object

        init(id: Int, name: String, isActive: Bool, address: Address) {
            self.id = id
            self.name = name
            self.isActive = isActive
            self.address = address
        }

        required init?(coder: NSCoder) {
            self.id       = coder.decodeInteger(forKey: "id")
            self.name     = coder.decodeObject(forKey: "name") as? String ?? ""
            self.isActive = coder.decodeBool(forKey: "isActive")
            self.address  = coder.decodeObject(forKey: "address") as! Address
        }

        func encode(with coder: NSCoder) {
            coder.encode(id, forKey: "id")
            coder.encode(name, forKey: "name")
            coder.encode(isActive, forKey: "isActive")
            coder.encode(address, forKey: "address")
        }
    }

    //-------------------- Writing / reading with SecureStoreCache--------------------

    let store = SecureDatabaseStore(databaseFileName: "profiles.db")
    try store.open(with: "my-encryption-key")

    // requiresSecureCoding defaults to false — old behavior.
    let cache = SecureStoreCache<UserProfile>(secureStore: store, tableName: "Profiles")
    let profile = UserProfile(id: 42, name: "Ada", isActive: true, address: Address(street: "1 Infinite Loop", city: "Cupertino"))

    // WRITE
    cache.set(value: profile, forKey: "user_42", withCost: 1)

    // READ
    let userProfileValue: UserProfile?      = cache.value(forKey: "user_42")
    let userProfileEntry:  CacheEntry<UserProfile>? = cache.entry(forKey: "user_42")

    //-------------------- Writing / reading with SecureKeyValueStore ----------------

    let keyValueStore = SecureKeyValueStore(name: "ProfileStore")
    try keyValueStore.open(with: "my-encryption-key")

    // WRITE
    try keyValueStore.put(profile, forKey: "user_42")

    // READ
    let userProfile: UserProfile? = try keyValueStore.get("user_42")

    // READ with default
    let fallback = UserProfile(id: 0, name: "guest", isActive: false, address: Address(street: "", city: ""))
    let userProfileOrFallback: UserProfile? = try keyValueStore.get("user_42", defaultValue: fallback)

AFTER

Both the root type and the nested type must conform to NSSecureCoding, and object decodes use the decodeObject(of:forKey:) form.

    import Foundation
    import SAPFoundation

    class Address: NSObject, NSSecureCoding {

        // 1. Declare secure coding support.
        static var supportsSecureCoding: Bool { true }

        let street: String
        let city: String

        init(street: String, city: String) {
            self.street = street
            self.city = city
        }

        // 2. encode(with:) is UNCHANGED.
        func encode(with coder: NSCoder) {
            coder.encode(street, forKey: "street")
            coder.encode(city, forKey: "city")
        }

        // 3. Strings are objects -> decode securely against NSString.
        required init?(coder: NSCoder) {
            self.street = coder.decodeObject(of: NSString.self,
                                             forKey: "street") as String? ?? ""
            self.city   = coder.decodeObject(of: NSString.self,
                                             forKey: "city") as String? ?? ""
        }
    }

    class UserProfile: NSObject, NSSecureCoding {

        static var supportsSecureCoding: Bool { true }

        let id: Int
        let name: String
        let isActive: Bool
        let address: Address          // nested type — also NSSecureCoding now

        init(id: Int, name: String, isActive: Bool, address: Address) {
            self.id = id
            self.name = name
            self.isActive = isActive
            self.address = address
        }

        // encode(with:) is UNCHANGED from the NSCoding version.
        func encode(with coder: NSCoder) {
            coder.encode(id, forKey: "id")
            coder.encode(name, forKey: "name")
            coder.encode(isActive, forKey: "isActive")
            coder.encode(address, forKey: "address")
        }

        required init?(coder: NSCoder) {
            // Primitive types (Int, Bool) are UNCHANGED.
            self.id       = coder.decodeInteger(forKey: "id")
            self.isActive = coder.decodeBool(forKey: "isActive")
            // Objects use the secure decode
            self.name     = coder.decodeObject(of: NSString.self,
                                               forKey: "name") as String? ?? ""
            guard let address = coder.decodeObject(of: Address.self,
                                                   forKey: "address") else {
                return nil
            }
            self.address = address
        }
    }

    //-------------------- Writing / reading with SecureStoreCache--------------------

    let store = SecureDatabaseStore(databaseFileName: "profiles.db")
    try store.open(with: "my-encryption-key")

    let cache = SecureStoreCache<UserProfile>(secureStore: store, tableName: "Profiles", requiresSecureCoding: true)  // <-- Opt in to secure coding.
    let profile = UserProfile(id: 42, name: "Ada", isActive: true, address: Address(street: "1 Infinite Loop", city: "Cupertino"))

    // WRITE — unchanged
    cache.set(value: profile, forKey: "user_42", withCost: 1)

    // READ — must use the allowedClasses overload.
    let userProfileValue: UserProfile? = cache.value(forKey: "user_42", allowedClasses: [UserProfile.self, Address.self])

    let userProfileEntry: CacheEntry<UserProfile>? = cache.entry(forKey: "user_42", allowedClasses: [UserProfile.self, Address.self])

    //-------------------- Writing / reading with SecureKeyValueStore ----------------

    let keyValueStore = SecureKeyValueStore(name: "ProfileStore",  requiresSecureCoding: true) // <-- Opt in to secure coding.
    try keyValueStore.open(with: "my-encryption-key")

    // WRITE
    try keyValueStore.put(profile, forKey: "user_42")

    // READ — use the allowedClasses overload.
    let userProfile: UserProfile? = try keyValueStore.get("user_42", allowedClasses: [UserProfile.self, Address.self])

    let fallback = UserProfile(id: 0, name: "guest", isActive: false, address: Address(street: "", city: ""))
    let userProfileOrFallback: UserProfile? = try keyValueStore.get("user_42", defaultValue: fallback, allowedClasses: [UserProfile.self, Address.self])

    // --- If the value is a collection ------------------------------------------
    // When the stored value is an array/dictionary/set, add the container class too, e.g. a cached [UserProfile]
    let list = cache.value(forKey: "all_users", allowedClasses: [NSArray.self, UserProfile.self, Address.self])
    let listFromKeyValueStore: [UserProfile]? = try kv.get("all_users", allowedClasses: [NSArray.self, UserProfile.self, Address.self])

Conclusion

Adopting secure coding is a deliberate, opt-in step. When you are ready, migrate by following the three requirements stated above. Writing is unchanged and no database migration is needed. The result is the same stored data, archived and unarchived through Apple's secure APIs providing security to your application against object substitution attacks.


Last update: August 12, 2026