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.

technical7 min readBy Klivvr Engineering
Share:

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

business

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.

6 min read
technical

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.

8 min read