CryptoKit: Encryption, Signing, and Post-Quantum Cryptography
You are shipping user data over the wire, storing tokens on disk, and verifying server payloads — yet the cryptographic
primitives powering all of that are often buried behind third-party wrappers or, worse, raw CommonCrypto C calls.
Apple’s CryptoKit framework gives you a Swift-native,
misuse-resistant API for hashing, encryption, signing, and key agreement — and as of iOS 26, it adds post-quantum
algorithms so your app is ready for the day quantum computers break classical key exchange.
This post covers CryptoKit’s full surface area: hashing, symmetric encryption, asymmetric keys, HMAC, Secure Enclave integration, and the new ML-KEM / ML-DSA post-quantum primitives. We will not cover TLS configuration or network-layer certificate pinning — those are covered in App Security Best Practices.
Contents
- The Problem
- Hashing with SHA-256 and SHA-512
- Symmetric Encryption with AES-GCM
- HMAC: Message Authentication
- Asymmetric Keys: P-256 and Curve25519
- Secure Enclave Keys
- Post-Quantum Cryptography: ML-KEM and ML-DSA
- Performance Considerations
- When to Use (and When Not To)
- Summary
The Problem
Imagine your app manages a Pixar movie vault — users store unreleased plot synopses, character designs, and voice-over scripts on their devices. You need to hash passwords before sending them to your server, encrypt local drafts so a stolen device does not leak the next Toy Story screenplay, and verify that asset bundles downloaded from your CDN have not been tampered with.
Without CryptoKit, you might reach for CommonCrypto:
import CommonCrypto
func legacySHA256(data: Data) -> Data {
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes { buffer in
_ = CC_SHA256(buffer.baseAddress, CC_LONG(data.count), &hash)
}
return Data(hash)
}
This works, but it is C-bridged, requires manual buffer management, and gives you zero compile-time safety against mixing up digest lengths. CryptoKit replaces all of this with value types, generics, and protocols that make incorrect usage a compiler error rather than a runtime vulnerability.
Hashing with SHA-256 and SHA-512
Hashing is the most common cryptographic operation — you need it for integrity checks, fingerprinting, and password
derivation. CryptoKit provides SHA256 and
SHA512 as zero-configuration structs.
import CryptoKit
import Foundation
struct MovieVault {
/// Computes a SHA-256 fingerprint for a movie script's content.
static func fingerprint(for script: String) -> String {
let data = Data(script.utf8)
let digest = SHA256.hash(data: data)
return digest.compactMap { String(format: "%02x", $0) }.joined()
}
/// Verifies that a downloaded asset matches the expected hash.
static func verify(asset data: Data, expectedHash: String) -> Bool {
let digest = SHA512.hash(data: data)
let hex = digest.compactMap { String(format: "%02x", $0) }.joined()
return hex == expectedHash
}
}
let hash = MovieVault.fingerprint(for: "Woody finds a new kid named Bonnie.")
// "a1f3c8..." — deterministic 64-character hex string
Both SHA256.hash(data:) and SHA512.hash(data:) return a Digest value type that conforms to Sequence, Hashable,
and ContiguousBytes. For incremental hashing of large files — say, a full Pixar render frame sequence — use the
streaming API:
func hashLargeFile(at url: URL) throws -> SHA256Digest {
var hasher = SHA256()
let handle = try FileHandle(forReadingFrom: url)
defer { handle.closeFile() }
while true {
let chunk = handle.readData(ofLength: 1024 * 1024) // 1 MB chunks
if chunk.isEmpty { break }
hasher.update(data: chunk)
}
return hasher.finalize()
}
The streaming approach keeps memory constant regardless of file size — critical when hashing multi-gigabyte asset bundles.
Symmetric Encryption with AES-GCM
When you need to encrypt data with a shared secret — local storage encryption, encrypting payloads between your app and
your own server, or protecting cached voice-over recordings — AES-GCM is the standard choice. CryptoKit wraps it in
AES.GCM, which handles nonce generation and
authentication tagging for you.
struct ScriptEncryptor {
private let key: SymmetricKey
init() {
// Generate a 256-bit key. Store this in the Keychain, not in code.
self.key = SymmetricKey(size: .bits256)
}
/// Encrypts a movie script. Returns a combined sealed box (nonce + ciphertext + tag).
func encrypt(script: String) throws -> Data {
let plaintext = Data(script.utf8)
let sealedBox = try AES.GCM.seal(plaintext, using: key)
guard let combined = sealedBox.combined else {
throw EncryptionError.sealingFailed
}
return combined
}
/// Decrypts a previously sealed script.
func decrypt(data: Data) throws -> String {
let sealedBox = try AES.GCM.SealedBox(combined: data)
let decryptedData = try AES.GCM.open(sealedBox, using: key)
guard let script = String(data: decryptedData, encoding: .utf8) else {
throw EncryptionError.decodingFailed
}
return script
}
}
enum EncryptionError: Error {
case sealingFailed
case decodingFailed
}
Warning: Never hardcode
SymmetricKeyvalues in your source code. Generate them at runtime and persist them in the Keychain. A key embedded in your binary can be extracted withstringsor a disassembler in minutes.
The combined property of SealedBox concatenates the 12-byte nonce, ciphertext, and 16-byte authentication tag into a
single Data blob — convenient for storage or transmission. AES-GCM is an authenticated encryption scheme, meaning
open(_:using:) will throw CryptoKitError.authenticationFailure if any byte has been tampered with.
ChaChaPoly as an Alternative
CryptoKit also exposes ChaChaPoly, which uses
ChaCha20-Poly1305. The API is identical to AES-GCM:
let sealedBox = try ChaChaPoly.seal(plaintext, using: key)
let decrypted = try ChaChaPoly.open(sealedBox, using: key)
ChaChaPoly performs better on devices without AES hardware acceleration (older Apple Watch models), but on modern Apple Silicon every device has AES-NI, so AES-GCM is the default choice for iOS.
HMAC: Message Authentication
When you download a Pixar asset bundle from your CDN, you need to verify the server actually produced it — not a man-in-the-middle. HMAC generates a keyed hash that proves both integrity and authenticity.
struct AssetVerifier {
private let signingKey: SymmetricKey
init(sharedSecret: Data) {
self.signingKey = SymmetricKey(data: sharedSecret)
}
/// Creates an HMAC authentication code for an asset bundle.
func authenticationCode(for bundle: Data) -> Data {
let code = HMAC<SHA256>.authenticationCode(
for: bundle, using: signingKey
)
return Data(code)
}
/// Validates that a bundle matches the provided HMAC.
func validate(bundle: Data, against mac: Data) -> Bool {
HMAC<SHA256>.isValidAuthenticationCode(
mac, authenticating: bundle, using: signingKey
)
}
}
HMAC is generic over any HashFunction — swap in SHA512 or SHA384 by changing the type parameter. The
isValidAuthenticationCode method uses constant-time comparison internally, preventing timing attacks.
Tip: Use HMAC when both parties share a secret key. If you need third-party verifiability (anyone can check the signature without the key), use asymmetric signing instead.
Asymmetric Keys: P-256 and Curve25519
Asymmetric cryptography is essential for key agreement (establishing a shared secret over an insecure channel) and digital signatures (proving a message came from a specific sender). CryptoKit supports both NIST P-256 and Curve25519 families.
Digital Signatures with P-256
Suppose your Pixar asset pipeline signs each render job manifest so the rendering farm can verify it came from a trusted coordinator:
struct RenderManifestSigner {
private let privateKey: P256.Signing.PrivateKey
init() {
self.privateKey = P256.Signing.PrivateKey()
}
var publicKey: P256.Signing.PublicKey {
privateKey.publicKey
}
func sign(manifest: Data) throws -> P256.Signing.ECDSASignature {
try privateKey.signature(for: manifest)
}
static func verify(
manifest: Data,
signature: P256.Signing.ECDSASignature,
publicKey: P256.Signing.PublicKey
) -> Bool {
publicKey.isValidSignature(signature, for: manifest)
}
}
Key Agreement with Curve25519
When two devices need to establish a shared secret — say, a director’s iPad sharing review notes with an animator’s Mac
— use Curve25519.KeyAgreement:
struct SecureChannel {
/// Derives a symmetric key from two Curve25519 key pairs using HKDF.
static func deriveSharedKey(
myPrivateKey: Curve25519.KeyAgreement.PrivateKey,
theirPublicKey: Curve25519.KeyAgreement.PublicKey
) throws -> SymmetricKey {
let sharedSecret = try myPrivateKey.sharedSecretFromKeyAgreement(
with: theirPublicKey
)
// Derive a 256-bit key using HKDF with a protocol-specific salt
return sharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: Data("PixarSecureReview".utf8),
sharedInfo: Data(),
outputByteCount: 32
)
}
}
// Both sides generate key pairs, exchange public keys, derive the same shared key
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let aliceShared = try SecureChannel.deriveSharedKey(
myPrivateKey: aliceKey,
theirPublicKey: bobKey.publicKey
)
let bobShared = try SecureChannel.deriveSharedKey(
myPrivateKey: bobKey,
theirPublicKey: aliceKey.publicKey
)
// aliceShared == bobShared — both sides derive the identical key
The HKDF step is critical — raw shared secrets should never be used directly as encryption keys. The salt parameter
should be unique to your protocol to prevent cross-protocol attacks.
Apple Docs:
Curve25519— CryptoKit
Secure Enclave Keys
The Secure Enclave is a hardware coprocessor that stores private keys in a way that even a compromised kernel cannot
extract them. CryptoKit provides
SecureEnclave.P256 for signing and key agreement
operations that never expose the raw key material.
struct VaultAuthenticator {
private let enclaveKey: SecureEnclave.P256.Signing.PrivateKey
init() throws {
// Check hardware availability first
guard SecureEnclave.isAvailable else {
throw VaultError.secureEnclaveUnavailable
}
self.enclaveKey = try SecureEnclave.P256.Signing.PrivateKey()
}
var publicKey: P256.Signing.PublicKey {
enclaveKey.publicKey
}
/// Signs a vault access request. The private key never leaves the Secure Enclave.
func signAccessRequest(_ request: Data) throws -> P256.Signing.ECDSASignature {
try enclaveKey.signature(for: request)
}
}
enum VaultError: Error {
case secureEnclaveUnavailable
}
Secure Enclave keys are bound to the device — they cannot be exported, backed up, or transferred. This makes them ideal for device attestation, biometric-gated signing, and any scenario where key theft must be impossible even if the file system is compromised.
Note:
SecureEnclave.isAvailablereturnsfalseon the iOS Simulator. Always provide a software-key fallback for development and testing, but log a warning when the fallback is active in production builds.
Protecting Keys with Access Control
You can bind Secure Enclave keys to biometric authentication by passing a LAContext access control:
import LocalAuthentication
func createBiometricProtectedKey() throws -> SecureEnclave.P256.Signing.PrivateKey {
let context = LAContext()
context.touchIDAuthenticationAllowableReuseDuration = 10 // seconds
let accessControl = SecureEnclave.AccessControl(
protection: .afterFirstUnlockThisDeviceOnly,
flags: [.biometryCurrentSet]
)
return try SecureEnclave.P256.Signing.PrivateKey(
accessControl: accessControl,
authenticationContext: context
)
}
With .biometryCurrentSet, the key is invalidated if the user adds or removes a fingerprint or Face ID appearance,
preventing an attacker from enrolling their own biometric and then using the key.
Post-Quantum Cryptography: ML-KEM and ML-DSA
Classical asymmetric algorithms like P-256 and Curve25519 rely on the hardness of elliptic-curve discrete logarithm problems. A sufficiently powerful quantum computer could break these using Shor’s algorithm. iOS 26 introduces post-quantum algorithms based on NIST FIPS 203 and FIPS 204:
- ML-KEM (Module-Lattice Key Encapsulation Mechanism) — quantum-resistant key agreement
- ML-DSA (Module-Lattice Digital Signature Algorithm) — quantum-resistant signatures
Note: ML-KEM and ML-DSA require iOS 26 / macOS 26. These APIs are available starting with Xcode 26 beta 1.
ML-KEM: Quantum-Safe Key Exchange
ML-KEM replaces classical Diffie-Hellman key agreement. The encapsulation/decapsulation pattern differs from traditional key agreement:
@available(iOS 26, *)
struct QuantumSafeChannel {
/// Server generates a key pair and publishes the encapsulation key.
static func generateKeyPair() -> MLKEM1024.DecapsulationKey {
let decapsulationKey = MLKEM1024.DecapsulationKey()
return decapsulationKey
}
/// Client encapsulates: produces a shared secret and a ciphertext to send back.
static func encapsulate(
to encapsulationKey: MLKEM1024.EncapsulationKey
) -> MLKEM1024.EncapsulationResult {
encapsulationKey.encapsulate()
}
/// Server decapsulates: recovers the same shared secret from the ciphertext.
static func decapsulate(
ciphertext: Data,
using decapsulationKey: MLKEM1024.DecapsulationKey
) throws -> SymmetricKey {
try decapsulationKey.decapsulate(ciphertext)
}
}
ML-KEM-1024 provides NIST Security Level 5 (equivalent to AES-256). CryptoKit also offers MLKEM768 for Level 3 when
smaller key sizes matter. The encapsulated ciphertext is roughly 1.5 KB — larger than a Curve25519 public key, but a
negligible cost for the quantum resistance it provides.
ML-DSA: Quantum-Safe Signatures
ML-DSA replaces ECDSA for scenarios where signatures must remain secure for decades — think legal documents, long-lived certificates, or archival integrity proofs:
@available(iOS 26, *)
struct ArchivalSigner {
private let signingKey: MLDSA65.SigningKey
init() {
self.signingKey = MLDSA65.SigningKey()
}
var verifyingKey: MLDSA65.VerifyingKey {
signingKey.verifyingKey
}
func sign(document: Data) throws -> MLDSA65.Signature {
try signingKey.signature(for: document)
}
static func verify(
document: Data,
signature: MLDSA65.Signature,
verifyingKey: MLDSA65.VerifyingKey
) -> Bool {
verifyingKey.isValidSignature(signature, for: document)
}
}
ML-DSA-65 (FIPS 204 Security Level 3) produces signatures of roughly 3.3 KB — substantially larger than ECDSA’s ~72 bytes. For bandwidth-constrained scenarios, evaluate whether quantum resistance is worth the overhead today or whether a hybrid approach (classical + post-quantum) is more appropriate.
Hybrid Approach: Defense in Depth
Apple recommends a hybrid strategy during the transition period — use both classical and post-quantum algorithms, so you remain secure even if one is broken:
@available(iOS 26, *)
struct HybridKeyExchange {
/// Performs both classical Curve25519 and quantum-safe ML-KEM key exchange,
/// then combines both shared secrets via HKDF.
static func deriveHybridKey(
classicalPrivate: Curve25519.KeyAgreement.PrivateKey,
classicalPublicTheirs: Curve25519.KeyAgreement.PublicKey,
mlkemEncapsulationKey: MLKEM768.EncapsulationKey
) throws -> SymmetricKey {
// Classical key agreement
let classicalSecret = try classicalPrivate.sharedSecretFromKeyAgreement(
with: classicalPublicTheirs
)
// Post-quantum encapsulation
let result = mlkemEncapsulationKey.encapsulate()
// Combine both secrets using HKDF
var combinedInput = Data()
classicalSecret.withUnsafeBytes { combinedInput.append(contentsOf: $0) }
result.sharedSecret.withUnsafeBytes { combinedInput.append(contentsOf: $0) }
return HKDF<SHA256>.deriveKey(
inputKeyMaterial: SymmetricKey(data: combinedInput),
salt: Data("PixarHybridKEX-v1".utf8),
info: Data(),
outputByteCount: 32
)
}
}
This way, even if ML-KEM is found to have a classical weakness, the Curve25519 component keeps you safe — and if a quantum computer breaks Curve25519, ML-KEM has you covered.
Performance Considerations
CryptoKit operations are hardware-accelerated on Apple Silicon. Here are the relative costs to keep in mind:
| Operation | Relative Cost | Notes |
|---|---|---|
| SHA-256 hash (1 KB) | ~1 us | Effectively free for small payloads |
| AES-GCM encrypt (1 MB) | ~0.5 ms | Hardware AES-NI on all modern devices |
| P-256 sign | ~1 ms | Elliptic curve math is the bottleneck |
| P-256 verify | ~1 ms | Slightly faster than signing |
| Secure Enclave sign | ~30-50 ms | Round-trip to the coprocessor dominates |
| ML-KEM-768 encapsulate | ~0.3 ms | Lattice operations are fast |
| ML-DSA-65 sign | ~2 ms | Larger key/signature sizes add overhead |
The Secure Enclave round-trip is the most expensive operation by far. If you need to sign many items in rapid succession, batch the data and sign once, or sign a Merkle root rather than individual items.
For large-file encryption, streaming AES-GCM through 1 MB chunks keeps memory pressure low. CryptoKit does not provide a streaming encryption API directly, so you will need to manage chunking yourself with per-chunk nonces derived from a counter.
Tip: Profile with Instruments’ Crypto template (available in Xcode 26) to identify whether cryptographic operations are a real bottleneck before optimizing. In most apps, network latency dwarfs CryptoKit computation time by orders of magnitude.
When to Use (and When Not To)
| Scenario | Recommendation |
|---|---|
| Hashing passwords before sending to server | Use SHA-256 for fingerprinting, but prefer server-side bcrypt/Argon2 for storage |
| Encrypting local data at rest | AES-GCM with a Keychain-stored SymmetricKey is the standard approach |
| Verifying downloaded assets | HMAC if you share a secret with your server; asymmetric signatures otherwise |
| Device attestation / biometric gating | Secure Enclave P-256 keys with LAContext access control |
| Long-lived signatures (10+ year validity) | Consider ML-DSA now for forward security against quantum threats |
| TLS / HTTPS configuration | Do not use CryptoKit — rely on URLSession and ATS |
| Password hashing on-device | Do not use raw SHA-256. Use a KDF like HKDF or delegate to the server |
Apple Docs:
CryptoKit— Apple Developer Documentation
Summary
- SHA-256 / SHA-512 provide fast, deterministic hashing for integrity checks and fingerprinting. Use the streaming API for large files.
- AES-GCM is the go-to symmetric encryption scheme — it handles nonce generation and authentication tagging automatically. Store keys in the Keychain, never in code.
- HMAC authenticates messages with a shared key using constant-time comparison. Use it for server-to-client payload verification.
- P-256 and Curve25519 cover signing and key agreement. Combine key agreement with HKDF to derive encryption keys.
- Secure Enclave keys never leave the hardware coprocessor, making them ideal for device-bound authentication and biometric-gated operations.
- ML-KEM and ML-DSA (iOS 26) bring post-quantum cryptography to Apple platforms. Adopt a hybrid classical + post-quantum approach during the transition period.
CryptoKit handles the cryptographic primitives, but keys still need a secure home. Head over to Keychain Storage to learn how to persist symmetric keys and credentials properly, or check out Passkeys Authentication to see how Apple is replacing passwords entirely.