Use when encrypting data, signing payloads, verifying signatures, generating keys, using Secure Enclave, migrating from CommonCrypto, or adopting quantum-secure cryptography. Covers CryptoKit design philosophy, AES-GCM, ECDSA, ECDH, Secure Enclave keys, HPKE, ML-KEM, ML-DSA, and cross-platform interop with Swift Crypto.
Authenticated encryption, digital signatures, key agreement, Secure Enclave key management, and quantum-secure cryptography for iOS apps.
Use when you need to:
"How do I encrypt user data with AES-GCM?" "How do I sign a payload and verify it on my server?" "How do I use the Secure Enclave to protect a signing key?" "I'm using CommonCrypto — should I migrate to CryptoKit?" "How do I do ECDH key agreement for end-to-end encryption?" "How do I make my app quantum-secure?" "My server can't verify signatures from my iOS app" "What's the difference between P256 and Curve25519?"
Signs you're making this harder or less secure than it needs to be:
AES.GCM.open and signature verification throw on failure. Force-unwrapping masks authentication failures and turns security errors into crashes. Always use try/catch.digraph need_cryptokit {
"What are you\nprotecting?" [shape=diamond];
"Data in transit\nvia HTTPS?" [shape=diamond];
"Data at rest\non device?" [shape=diamond];
"Credentials or\ntokens?" [shape=diamond];
"CloudKit or\niCloud?" [shape=diamond];
"Custom crypto\nneeded?" [shape=diamond];
"Use URLSession + TLS\n(system handles crypto)" [shape=box];
"Use Data Protection\n(.completeFileProtection)" [shape=box];
"Use Keychain\n(axiom-keychain skill)" [shape=box];
"Use CloudKit encryption\n(encryptedValues)" [shape=box];
"YES — Use CryptoKit" [shape=box, style=bold];
"What are you\nprotecting?" -> "Data in transit\nvia HTTPS?" [label="network"];
"What are you\nprotecting?" -> "Data at rest\non device?" [label="storage"];
"What are you\nprotecting?" -> "Credentials or\ntokens?" [label="secrets"];
"What are you\nprotecting?" -> "CloudKit or\niCloud?" [label="sync"];
"What are you\nprotecting?" -> "Custom crypto\nneeded?" [label="signatures, key exchange,\nE2E encryption"];
"Data in transit\nvia HTTPS?" -> "Use URLSession + TLS\n(system handles crypto)" [label="standard HTTPS"];
"Data in transit\nvia HTTPS?" -> "Custom crypto\nneeded?" [label="need E2E beyond TLS"];
"Data at rest\non device?" -> "Use Data Protection\n(.completeFileProtection)" [label="file-level"];
"Data at rest\non device?" -> "Custom crypto\nneeded?" [label="field-level encryption"];
"Credentials or\ntokens?" -> "Use Keychain\n(axiom-keychain skill)" [label="yes"];
"CloudKit or\niCloud?" -> "Use CloudKit encryption\n(encryptedValues)" [label="yes"];
"Custom crypto\nneeded?" -> "YES — Use CryptoKit" [label="yes"];
}
From WWDC 2019-709: "We strongly recommend you rely on higher level system frameworks when you can." CryptoKit is for when system frameworks don't cover your use case — custom signatures, field-level encryption, key agreement, interop with non-Apple systems.
The Secure Enclave is a hardware security module built into Apple devices. Keys generated in the SE never leave the hardware — not even Apple can extract them.
| Scenario | Use SE? | Why |
|---|---|---|
| Signing API requests | Yes | Key can't be extracted from device |
| Biometric-gated decryption | Yes | SE ties key to Face ID/Touch ID |
| End-to-end encryption key | Yes (key agreement) | Private key hardware-bound |
| Encrypting data for another device | No | Key must be exportable |
| Server-shared symmetric key | No | SE only does asymmetric (P256, ML-KEM, ML-DSA) |
| Cross-device sync | No | SE keys are device-bound |
import CryptoKit
import LocalAuthentication
guard SecureEnclave.isAvailable else {
let softwareKey = P256.Signing.PrivateKey()
return
}
let context = LAContext()
context.localizedReason = "Authenticate to sign transaction"
guard let accessControl = SecAccessControlCreateWithFlags(
nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet], nil
) else {
throw CryptoKitError.underlyingCoreCryptoError(error: 0)
}
let seKey = try SecureEnclave.P256.Signing.PrivateKey(
compactRepresentable: false,
accessControl: accessControl,
authenticationContext: .init(context)
)
let signature = try seKey.signature(for: data)
let publicKeyDER = seKey.publicKey.derRepresentation
seKey.dataRepresentation is a wrapped blob, not raw key material. It contains an encrypted reference that only this specific Secure Enclave hardware can unwrap. Exporting it to another device produces an unusable blob.
dataRepresentation in Keychain. Restore with:let restoredKey = try SecureEnclave.P256.Signing.PrivateKey(
dataRepresentation: savedKeyData, authenticationContext: .init(context)
)
SecureEnclave.P256.Signing and SecureEnclave.P256.KeyAgreementSecureEnclave.MLKEM768, SecureEnclave.MLKEM1024, SecureEnclave.MLDSA65, SecureEnclave.MLDSA87SecureEnclave.isAvailable returns false. Always provide a software fallback for testing.import CryptoKit
let data = "Hello, world".data(using: .utf8)!
let digest = SHA256.hash(data: data)
guard digest == SHA256.hash(data: data) else { /* tampering detected */ }
Available: SHA256, SHA384, SHA512. Use Insecure.MD5/Insecure.SHA1 only for legacy protocol compatibility, never for security.
let key = SymmetricKey(size: .bits256)
let mac = HMAC<SHA256>.authenticationCode(for: data, using: key)
let isValid = HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: data, using: key)
Constant-time comparison built in — safe against timing attacks. Use HMAC when both parties share a symmetric key.
let key = SymmetricKey(size: .bits256)
let plaintext = "Secret message".data(using: .utf8)!
let sealedBox = try AES.GCM.seal(plaintext, using: key)
let combined = sealedBox.combined!
let restoredBox = try AES.GCM.SealedBox(combined: combined)
let decrypted = try AES.GCM.open(restoredBox, using: key)
Authenticated encryption — tampering triggers CryptoKitError.authenticationFailure. No separate HMAC step needed. Nonce is auto-generated and prepended to combined.
let privateKey = P256.Signing.PrivateKey()
let signature = try privateKey.signature(for: data)
let isValid = privateKey.publicKey.isValidSignature(signature, for: data)
Curves: P256 (secp256r1, most common), P384, P521, Curve25519 (Ed25519, fastest). Use P256 for server interop. Use Curve25519 when you control both sides.
let alicePrivate = P256.KeyAgreement.PrivateKey()
let bobPrivate = P256.KeyAgreement.PrivateKey()
let sharedSecret = try alicePrivate.sharedSecretFromKeyAgreement(
with: bobPrivate.publicKey
)
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: "my-app-v1".data(using: .utf8)!,
sharedInfo: Data(),
outputByteCount: 32
)
Never use the raw shared secret directly — always derive a symmetric key via HKDF. The raw secret has biased bits that weaken encryption.
HPKE (Hybrid Public Key Encryption) combines key agreement and symmetric encryption in one step. Preferred over manual ECDH + AES-GCM for new protocols. Classical ciphersuites (P256, Curve25519) available iOS 17+. The quantum-secure XWing ciphersuite and ML-KEM/ML-DSA require iOS 26+.
let recipientPrivate = P256.KeyAgreement.PrivateKey()
var sender = try HPKE.Sender(
recipientKey: recipientPrivate.publicKey,
ciphersuite: .P256_SHA256_AES_GCM_256,
info: "my-app-message-v1".data(using: .utf8)!
)
let ciphertext = try sender.seal(plaintext)
var recipient = try HPKE.Recipient(
privateKey: recipientPrivate,
ciphersuite: .P256_SHA256_AES_GCM_256,
info: "my-app-message-v1".data(using: .utf8)!,
encapsulatedKey: sender.encapsulatedKey
)
let decrypted = try recipient.open(ciphertext)
From WWDC 2025-314: harvest-now-decrypt-later is not theoretical. Nation-state actors are recording encrypted traffic today to decrypt with future quantum computers. Data with sensitivity beyond 10 years needs post-quantum protection now.
Same HPKE API, different ciphersuite. Combines ML-KEM (quantum-safe) with X25519 (classical) for hybrid security — Apple's recommendation:
let recipientPrivate = XWingMLKEM768X25519.PrivateKey()
var sender = try HPKE.Sender(
recipientKey: recipientPrivate.publicKey,
ciphersuite: .XWingMLKEM768X25519_SHA256_AES_GCM_256,
info: "my-app-pq-v1".data(using: .utf8)!
)
let ciphertext = try sender.seal(plaintext)
var recipient = try HPKE.Recipient(
privateKey: recipientPrivate,
ciphersuite: .XWingMLKEM768X25519_SHA256_AES_GCM_256,
info: "my-app-pq-v1".data(using: .utf8)!,
encapsulatedKey: sender.encapsulatedKey
)
let decrypted = try recipient.open(ciphertext)
Hybrid constructions (classical + post-quantum) ensure security even if one algorithm is broken. The XWing construction pairs ML-KEM768 with X25519 so a classical break alone or a quantum break alone cannot compromise the exchange.
let kemPrivate = try MLKEM768.PrivateKey()
let (sharedSecret, encapsulation) = try kemPrivate.publicKey.encapsulate()
let derivedSecret = try kemPrivate.decapsulate(encapsulation)
let dsaPrivate = try MLDSA65.PrivateKey()
let signature = try dsaPrivate.signature(for: data)
let isValid = dsaPrivate.publicKey.isValidSignature(signature, for: data)
Use ML-KEM for key encapsulation when building custom protocols. Use ML-DSA for signatures when P256/Ed25519 won't survive quantum analysis. Prefer HPKE with hybrid ciphersuites over raw ML-KEM for most applications.
On Linux/server: import Crypto (apple/swift-crypto) provides the same API minus Secure Enclave and Keychain. For shared code:
#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
CryptoKit's ECDSA signatures use raw format (r || s concatenation, 64 bytes for P256). Most non-Apple platforms (OpenSSL, Java, .NET) expect DER format (ASN.1 encoded, variable length 70-72 bytes for P256).
let signature = try privateKey.signature(for: data)
let raw = signature.rawRepresentation
let der = signature.derRepresentation
When receiving signatures from a server:
let fromDER = try P256.Signing.ECDSASignature(derRepresentation: serverDER)
let fromRaw = try P256.Signing.ECDSASignature(rawRepresentation: serverRaw)
let publicKey = privateKey.publicKey
publicKey.derRepresentation
publicKey.pemRepresentation
publicKey.x963Representation
DER for most platforms, PEM for config files and REST APIs, X9.63 for compact JavaScript interop.
CryptoKit uses secp256r1 (P-256, prime256v1) — the NIST standard curve used by TLS, government systems, and enterprise software.
Bitcoin and Ethereum use secp256k1 (Koblitz curve) — a different curve entirely. These are not interoperable. A P-256 signature cannot be verified with a secp256k1 verifier. If you need secp256k1 for blockchain interop, use a dedicated library (libsecp256k1 wrapper), not CryptoKit.
Secure Enclave keys are device-bound. dataRepresentation is a wrapped blob that only the originating hardware can unwrap. This means SE keys enable crypto-shredding by design — destroying the device or reinstalling the app makes all SE-encrypted data permanently irrecoverable. Plan key lifecycle accordingly. Store recovery paths (server-escrowed backup keys, multi-device key distribution) if data must survive device loss.
| Rationalization | Why It Fails | Time Cost |
|---|---|---|
| "CommonCrypto works fine, no need to migrate" | Buffer overflows, no authenticated encryption, manual IV management. One wrong buffer size = silent data corruption or security vulnerability. | 2-4 hours debugging subtle encryption failures that CryptoKit prevents by design |
| "I'll add authentication to AES-CBC later" | Without authentication, ciphertext is malleable. Attackers modify data without detection. "Later" means after the vulnerability ships. | 4-8 hours incident response when tampered data is discovered in production |
| "Nonces don't need to be random for my use case" | Nonce reuse with AES-GCM leaks plaintext via XOR of ciphertexts. There is no use case where fixed nonces are safe with GCM. | Catastrophic — full plaintext recovery of all messages encrypted with the reused nonce |
| "Secure Enclave is overkill for this" | Software keys can be extracted from jailbroken devices and backups. SE keys cannot. If the key protects money, health data, or identity, SE is not overkill. | 0 extra development time (API is nearly identical to software P256) |
| "Quantum computing is decades away" | Harvest-now-decrypt-later means data recorded today will be decrypted when quantum computers arrive. Apple already ships quantum-secure TLS in iOS 26. | 0 if you use iOS 26 defaults. 1-2 hours for custom protocol migration. |
| "I'll just use my own encryption scheme" | Professional cryptographers spend years designing protocols. Timing side channels, padding oracles, nonce misuse are invisible without expert review. | Weeks to months of security audit + remediation |
| "DER vs raw doesn't matter, it's the same signature" | Wrong encoding = server verification failure. The math is correct but the encoding is wrong. | 2-4 hours debugging interop that one .derRepresentation call prevents |
Context: Feature deadline approaching. Developer needs to encrypt user data before persisting it.
Pressure: "Don't overthink the crypto. AES-CBC, CommonCrypto, whatever gets it done by end of day."
Reality: AES-CBC without authentication is vulnerable to padding oracle attacks. CommonCrypto requires manual buffer management where one wrong size creates silent data corruption. The migration from CCCrypt to AES.GCM.seal is 5-10 lines of code — less code than the CommonCrypto version — and eliminates an entire vulnerability class.
Correct action: Replace CCCrypt(kCCEncrypt, kCCAlgorithmAES, ...) with AES.GCM.seal(plaintext, using: key). Map existing key material to SymmetricKey(data:). If migrating existing CBC-encrypted data, add a read path that decrypts old format and re-encrypts on first access.
Push-back template: "AES-GCM via CryptoKit is actually fewer lines than CommonCrypto and provides authenticated encryption for free. The CBC code has a vulnerability class that GCM eliminates. Switching takes 10 minutes, not hours."
Context: Building an E2E encrypted feature that must work on iOS and Android. Developer proposes storing signing keys in Keychain (software) to keep parity with Android's software keystore.
Pressure: "Android doesn't have a Secure Enclave equivalent with the same guarantees. Let's keep it simple and consistent across platforms."
Reality: Android has hardware-backed Keystore (StrongBox) with similar guarantees. Even if the Android side uses software keys, that doesn't mean the iOS side should. SE protection is free on iOS — the API is nearly identical to software P256. Use SE on iOS, hardware Keystore on Android, software fallback where hardware is unavailable.
Correct action: Use SecureEnclave.P256.Signing.PrivateKey with a fallback to P256.Signing.PrivateKey. The public key and signature formats are identical — the server doesn't know or care which generated them.
Push-back template: "The SE API is the same as software P256 — no extra complexity. The server verifies the same public key format either way. We get hardware protection on devices that support it and graceful fallback on devices that don't. Both platforms can use their best available hardware."
Context: Designing a new E2E encrypted messaging protocol. Developer proposes classical ECDH + AES-GCM.
Pressure: "Quantum computers are at least a decade away. We're over-engineering this."
Reality: Harvest-now-decrypt-later means adversaries record encrypted traffic today and decrypt it when quantum computers arrive. For ephemeral data (session tokens), classical crypto is fine. For messages with long-term sensitivity (health records, financial data, private communications), post-quantum protection is warranted now. Apple already shipped PQ3 for iMessage and quantum-secure TLS in iOS 26.
Correct action: Use HPKE.Ciphersuite.XWingMLKEM768X25519_SHA256_AES_GCM_256 instead of .P256_SHA256_AES_GCM_256. The code change is one ciphersuite constant — same API, same complexity, hybrid classical + quantum-safe protection.
Push-back template: "The code change is literally one ciphersuite constant. Apple already did this for iMessage (PQ3) and iOS 26 TLS. One line of code removes the harvest-now-decrypt-later risk for data that's still sensitive in 10 years."
Before shipping any custom cryptography:
Algorithm Selection:
Key Management:
Nonce/IV Handling:
Interop (if communicating with non-Apple platforms):
Secure Enclave (if used):
SecureEnclave.isAvailable checked with software fallbackdataRepresentation stored in Keychain for persistenceWWDC: 2019-709, 2025-314
Docs: /cryptokit, /cryptokit/secureenclave, /security/certificate_key_and_trust_services
Skills: axiom-cryptokit-ref, axiom-keychain