Encryption at Rest: Protecting Data on iOS Devices
Implement encryption at rest for iOS applications using Apple's CryptoKit, AES-GCM, and the Secure Enclave, with practical Swift code examples.
Encryption at rest ensures that data stored on a device is unreadable without the proper decryption key, even if an attacker gains physical access to the device. While iOS provides system-level Data Protection that encrypts all files, applications handling highly sensitive data -- financial records, health information, personal documents -- often need an additional layer of application-level encryption. This defense-in-depth approach means that even if system-level protections are bypassed (through a jailbreak, for example), the application's data remains encrypted.
This article covers how to implement application-level encryption at rest using Apple's CryptoKit framework and the Secure Enclave, as integrated into KlivvrStorageKit.
Understanding iOS Data Protection
Before adding application-level encryption, it is important to understand what iOS already provides. Every iOS device has a hardware AES engine that encrypts all flash storage. On top of this, Data Protection uses the user's passcode to derive file-level encryption keys.
// iOS Data Protection levels applied to files
func setFileProtection(_ url: URL, level: FileProtectionType) throws {
try FileManager.default.setAttributes(
[.protectionKey: level],
ofItemAtPath: url.path
)
}
// Available protection levels:
// .complete - Encrypted when locked, inaccessible
// .completeUnlessOpen - Writable if opened while unlocked
// .completeUntilFirstUserAuthentication - Available after first unlock
// .none - No file-level encryption (still has volume encryption)
// Check current protection level
func getFileProtection(_ url: URL) -> FileProtectionType? {
let attributes = try? FileManager.default.attributesOfItem(atPath: url.path)
return attributes?[.protectionKey] as? FileProtectionType
}iOS Data Protection is excellent but has limitations. It protects files at the OS level, but if the device is unlocked (as it is during normal use), files are decryptable. Application-level encryption adds protection that persists even on an unlocked device.
AES-GCM Encryption with CryptoKit
Apple's CryptoKit framework provides modern cryptographic primitives. AES-GCM (Galois/Counter Mode) is the recommended algorithm for authenticated encryption, providing both confidentiality and integrity.
import CryptoKit
final class AESEncryptor {
// Generate a new random encryption key
static func generateKey() -> SymmetricKey {
SymmetricKey(size: .bits256)
}
// Encrypt data with AES-GCM
static func encrypt(data: Data, using key: SymmetricKey) throws -> Data {
let sealedBox = try AES.GCM.seal(data, using: key)
// Combined representation includes nonce + ciphertext + tag
guard let combined = sealedBox.combined else {
throw EncryptionError.sealingFailed
}
return combined
}
// Decrypt data with AES-GCM
static func decrypt(data: Data, using key: SymmetricKey) throws -> Data {
let sealedBox = try AES.GCM.SealedBox(combined: data)
return try AES.GCM.open(sealedBox, using: key)
}
}
// Usage
let key = AESEncryptor.generateKey()
let sensitiveData = "Secret financial data".data(using: .utf8)!
let encrypted = try AESEncryptor.encrypt(data: sensitiveData, using: key)
let decrypted = try AESEncryptor.decrypt(data: encrypted, using: key)
let originalString = String(data: decrypted, encoding: .utf8)
// "Secret financial data"AES-GCM automatically generates a unique nonce (initialization vector) for each encryption operation and appends an authentication tag to detect tampering. The combined representation bundles nonce + ciphertext + tag into a single Data object for easy storage.
Key Management with the Keychain
The encryption key is the most sensitive piece of the system. Storing it securely is as important as the encryption itself. The iOS Keychain, backed by the Secure Enclave on devices with the appropriate hardware, is the right place for encryption keys.
final class KeyManager {
private let keyIdentifier: String
init(keyIdentifier: String = "com.klivvr.storage.encryption-key") {
self.keyIdentifier = keyIdentifier
}
// Retrieve or create the encryption key
func getOrCreateKey() throws -> SymmetricKey {
if let existingKey = try retrieveKey() {
return existingKey
}
let newKey = SymmetricKey(size: .bits256)
try storeKey(newKey)
return newKey
}
// Store key in Keychain
private func storeKey(_ key: SymmetricKey) throws {
let keyData = key.withUnsafeBytes { Data($0) }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: keyIdentifier,
kSecAttrService as String: "com.klivvr.storage",
kSecValueData as String: keyData,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
]
// Delete any existing key first
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeyManagementError.storageFailed(status)
}
}
// Retrieve key from Keychain
private func retrieveKey() throws -> SymmetricKey? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: keyIdentifier,
kSecAttrService as String: "com.klivvr.storage",
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
switch status {
case errSecSuccess:
guard let keyData = result as? Data else { return nil }
return SymmetricKey(data: keyData)
case errSecItemNotFound:
return nil
default:
throw KeyManagementError.retrievalFailed(status)
}
}
// Rotate the encryption key (re-encrypt all data with new key)
func rotateKey(reEncrypt: (SymmetricKey, SymmetricKey) throws -> Void) throws {
let oldKey = try getOrCreateKey()
let newKey = SymmetricKey(size: .bits256)
try reEncrypt(oldKey, newKey)
try storeKey(newKey)
}
}Using kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly ensures the key is available for background operations (like syncing) but is never backed up or transferred to other devices.
Secure Enclave Integration
For the highest security level, encryption keys can be generated and stored in the Secure Enclave. Keys in the Secure Enclave never leave the hardware -- the Enclave performs cryptographic operations internally.
import CryptoKit
import LocalAuthentication
final class SecureEnclaveManager {
// Create a key pair in the Secure Enclave
func createSecureEnclaveKey(requireBiometric: Bool = true) throws -> SecureEnclave.P256.Signing.PrivateKey {
var authContext: LAContext? = nil
if requireBiometric {
authContext = LAContext()
authContext?.localizedReason = "Authenticate to access secure storage"
}
let key: SecureEnclave.P256.Signing.PrivateKey
if let authContext {
let accessControl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet],
nil
)!
key = try SecureEnclave.P256.Signing.PrivateKey(
accessControl: accessControl,
authenticationContext: authContext
)
} else {
key = try SecureEnclave.P256.Signing.PrivateKey()
}
return key
}
// Use Secure Enclave for key agreement (derive shared secret for encryption)
func deriveEncryptionKey(
privateKey: SecureEnclave.P256.KeyAgreement.PrivateKey,
serverPublicKey: P256.KeyAgreement.PublicKey
) throws -> SymmetricKey {
let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: serverPublicKey)
// Derive a symmetric key from the shared secret
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: "KlivvrStorageKit".data(using: .utf8)!,
sharedInfo: Data(),
outputByteCount: 32
)
return symmetricKey
}
}The Secure Enclave is available on devices with an A7 chip or later (iPhone 5s onward). Always check for availability before attempting Secure Enclave operations.
Encrypted Storage Implementation
Bringing encryption, key management, and storage together into a cohesive encrypted storage layer is what KlivvrStorageKit provides.
public final class EncryptedStore {
private let keyManager: KeyManager
private let fileStore: SecureFileStorage
private let encryptor: AESEncryptor.Type = AESEncryptor.self
public init(storeIdentifier: String = "default") throws {
self.keyManager = KeyManager(keyIdentifier: "com.klivvr.storage.\(storeIdentifier)")
self.fileStore = try SecureFileStorage()
}
public func save<T: Codable>(_ value: T, forKey key: String) throws {
let data = try JSONEncoder().encode(value)
let encryptionKey = try keyManager.getOrCreateKey()
let encryptedData = try encryptor.encrypt(data: data, using: encryptionKey)
// Store with integrity metadata
let envelope = StorageEnvelope(
encryptedData: encryptedData,
keyVersion: keyManager.currentKeyVersion,
algorithm: "AES-GCM-256",
createdAt: Date()
)
let envelopeData = try JSONEncoder().encode(envelope)
try fileStore.write(envelopeData, forKey: key)
}
public func load<T: Codable>(_ type: T.Type, forKey key: String) throws -> T? {
guard let envelopeData = try fileStore.read(forKey: key) else {
return nil
}
let envelope = try JSONDecoder().decode(StorageEnvelope.self, from: envelopeData)
let encryptionKey = try keyManager.getOrCreateKey()
let decryptedData = try encryptor.decrypt(data: envelope.encryptedData, using: encryptionKey)
return try JSONDecoder().decode(T.self, from: decryptedData)
}
public func delete(forKey key: String) throws {
try fileStore.delete(forKey: key)
}
}
struct StorageEnvelope: Codable {
let encryptedData: Data
let keyVersion: Int
let algorithm: String
let createdAt: Date
}The storage envelope includes metadata about the encryption algorithm and key version, enabling future key rotation and algorithm upgrades without breaking existing stored data.
Practical Tips
Always use authenticated encryption (AES-GCM, not AES-CBC) to detect tampering. Never derive encryption keys from user passwords without a proper KDF like Argon2 or PBKDF2 with high iteration counts. Implement key rotation from day one -- retrofitting it later is painful. Use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly for encryption keys that background processes need. Test encryption/decryption round-trips in your unit tests to catch serialization issues early. Profile encryption performance on older devices to ensure it does not impact UX.
Conclusion
Encryption at rest provides a critical layer of defense for sensitive data on iOS devices. By combining CryptoKit's AES-GCM encryption with Keychain-based key management and optional Secure Enclave integration, KlivvrStorageKit delivers application-level encryption that protects data even when system-level protections are compromised. The key is not just encrypting data but managing the encryption lifecycle -- key generation, storage, rotation, and retirement -- with the same rigor as the encryption itself.
Related Articles
Building Internal SDKs That Developers Want to Use
Lessons from building KlivvrStorageKit on creating internal SDKs with great developer experience — covering API design, documentation, migration support, and adoption strategies.
iOS Keychain Services: A Comprehensive Guide
Master iOS Keychain Services with this comprehensive guide covering item classes, access control, sharing, migration, and common pitfalls in Swift.
Secure Storage on iOS: Keychain, UserDefaults, and Beyond
Compare iOS storage options including Keychain, UserDefaults, file system, and Core Data, with guidance on choosing the right storage mechanism for sensitive data.