After several years as iOS developer, I decided revisit some concepts of secure development, and explore some advanced topics, using OWASP as a reference for best practices. In this article, we'll start by securely saving data on device.
Secure Data Storage
At this point, you already know that the UserDefaults API is not the right option to save sensitive data. What data are those? All related to personal user information or any sensitive data that could be traced back to it (name, credentials, health, address, banking, financial information), passwords and so on. This also applies to data in any documents too.
To handle this, we have some options, from the basic to more advanced. Here we go:
Securing using Keychain
The most secure way to store simple data is to use the Keychain, which is a data storage system in iOS that uses an encrypted database protected by hardware encryption. It's the most common and well-known way to store key-value pairs safety, and developers usually takes advantage of popular libraries like KeychainSwift.
But, when exploring protection parameters, consider these options to ensure your data is truly secure using the default api. Take a look at this:
func saveToKeychain(userName: String) {
guard let data = userName.data(using: .utf8) else {
return
}
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
]
SecItemAdd(query as CFDictionary, nil)
}
The use of kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly is the best way to secure sensitive data because it forces the device to guarantee that the user has set a passcode on the device. However, if the user disables it, your data is erased. Alternatively, use kSecAttrAccessibleWhenUnlockedThisDeviceOnly for other cases while keeping your data saved only on the device, not shared with iCloud.
- Because it is not shared with iCloud, keep in mind that if you want this data to be recovered during a device backup, you need to choose other options.
And if you want to use it in another application (that you own) or widget, you can include kSecAttrAccessGroup as String: accessGroup to give access to it (other project settings are required).
We can requires the user’s presence to be saved or retrieved, you can also define the .userPresence flag, which certifies that the user is present by prompting for authentication methods like Face ID, passcode, or Touch ID.
Also, you can include an application-specific password to retrieve this data, different from the user’s passcode.
guard let access = SecAccessControlCreateWithFlags(
nil, // Use the default allocator.
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.userPresence, &error) else {
return
}
Don’t forget to wipe out your data if the user log out or reinstalls your application.
The Keychain is a global storage at the iOS level (though not shared between apps by default). Some developers overlook this aspect of the lifecycle, allowing data to be accessed on the same device by different users. Here’s a simple way to handle this here
FileStorage
For store a document file, like a PDF or other documents, you can use this code to securely save it in file storage. In some cases, you collect those files from the user to send later (or sync them). Below is a sample to securely save the file:
func saveFileDataStorage(fromUrl: URL) {
let fileManager = FileManager.default
do {
let documentsUrl = try fileManager.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil, create: false)
let destinationFileUrl = documentsUrl.appendingPathComponent("myFile.pdf")
if fileManager.fileExists(atPath: destinationFileUrl.path) {
try fileManager.removeItem(at: destinationFileUrl)
}
try fileManager.moveItem(at: fromUrl, to: destinationFileUrl)
let attributes = [FileAttributeKey.protectionKey: FileProtectionType.complete]
try fileManager.setAttributes(attributes, ofItemAtPath: destinationFileUrl.path)
print("File saved successfully at \(destinationFileUrl.path)")
} catch {
print("File Error: \(error.localizedDescription)")
}
}
The most important parameter is FileProtectionType.complete, which marks your file to be accessible only when user has unlocked the device. If the device is locked, the data becomes inaccessible until the next unlock.
A good explanation can be found here and in this Apple article.
Note that the default protection since iOS 7 is Protected Until First User Authentication, which is not the best choice to fully protect your document.
Creating and Secure Private Keys (SecureEnclave and Keychain)
Is possible to use the Keychain to generate a private key (or other types of encryption keys) to secure data in iOS and save it into Secure Enclave, the same chip that stores iOS biometric data (passcode, Touch ID, or Face ID).
Private keys stored in the Secure Enclave are non-extractable, and cryptographic operations (e.g., signing, decryption) are performed inside it.
Additionally, the private key material never leaves the Secure Enclave, reducing the risk of key compromise.
The code below shows the piece of the attribute dictionary that generates the key: more details are available on the github project.
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: account.data(using: .utf8)!,
kSecAttrAccessControl as String: accessControl
]
]
Keep in mind, that this kind of storage is designed only for private keys, and use to be rare necessarily for common develop requirements.
Secure your CoreData database
It's not very common, but perhaps you use CoreData to save more structured data and its relations, and need to secure it. CoreData does not encrypt its data by default.
Start securing the file of your database in persistent container, defining a protection level for it, like below, using FileProtectionType.complete key:
let storeURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("iOSOwaspSec.sqlite")
let description = NSPersistentStoreDescription(url: storeURL)
description.setOption(FileProtectionType.complete as NSObject,
forKey: NSPersistentStoreFileProtectionKey)
container.persistentStoreDescriptions = [description]
If your app needs to access data while in the background or when the device is locked, choose an appropriate protection level. However, you can encrypt the data even when the device is unlocked.
For higher security, consider encrypting the persistent store. This technique uses the SQLCipher library to encrypt the entire database, but with a performance cost. In OWASP we have a link to github project that do this.
But, if you want just to secure the information in field level of your table, without lock all database with performance cost, you can create a ValueTransformer:
@objc(SecureTransformer)
class SecureTransformer: ValueTransformer {
static let key = static var key: SymmetricKey = {
// Retrieve the key from the Keychain or generate a new one
if let storedKeyData = KeychainHelper
.retrieveKey(alias: KeychainHelper.keyAlias) {
return SymmetricKey(data: storedKeyData)
} else {
let newKey = SymmetricKey(size: .bits256)
let keyData = newKey.withUnsafeBytes { Data($0) }
KeychainHelper.storeKey(keyData,
alias: KeychainHelper.keyAlias)
return newKey
}
}()
override class func transformedValueClass() -> AnyClass {
return NSData.self
}
override func transformedValue(_ value: Any?) -> Any? {
guard let stringValue = value as? String else { return nil }
let data = Data(stringValue.utf8)
guard let sealedBox = try? ChaChaPoly.seal(data, using: SecureTransformer.key) else { return nil }
return sealedBox.combined as NSData
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? Data else { return nil }
guard let sealedBox = try? ChaChaPoly.SealedBox(combined: data),
let decryptedData = try? ChaChaPoly.open(sealedBox, using: SecureTransformer.key) else { return nil }
return String(data: decryptedData, encoding: .utf8)
}
}
And as show in the image, set the attribute’s type to Transformable, and Set the Value Transformer to SecureTransformer.
And include the register of your transformer at some point on initialization:
ValueTransformer.setValueTransformer(SecureTransformer(),
forName: NSValueTransformerName("SecureTransformer"))
Now you can secure save a specific item in your CoreDate table, this comes handy and avoid encrypt the entire database.
Conclusion
This article cover some best praticies of OWASP for secure your data in iOS application's.
Check the github project here.
And here’s a challenge for you: Can you change the project to use the private key to securely save an item in Core Data, or maybe perform another cryptographic operation?
Top comments (0)