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.
-
Conform your types to
NSSecureCoding.Every type you persist — and every nested type it contains — must adopt
NSSecureCodinginstead ofNSCoding. See Conforming to NSSecureCoding. -
Opt in with the
requiresSecureCodingflag.Create the
SecureStoreCacheorSecureKeyValueStorewithrequiresSecureCoding: trueso archiving and unarchiving use Apple's secure APIs. See Opting In To Secure Coding. -
Read using the
allowedClassesmethods.While reading values, use the new methods that incorporate an
allowedClassesparameter, 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.