---
name: cryptokit
description: "CryptoKit in Swift: SHA-2, SHA-3, HMAC, AES-GCM, ChaChaPoly, P256, P384, P521, Curve25519 and ML-DSA signatures, ECDH with HKDF, HPKE, ML-KEM, X-Wing, SecureEnclave.P256 signing and ECDH. Use when hashing, encrypting, signing or exchanging keys, or migrating from CommonCrypto. Not for Keychain policy (swift-security)."
metadata:
  source: multi-agent-pipeline
---

# CryptoKit

CryptoKit is Apple's Swift API for hashing, message authentication, symmetric
encryption, signatures, key agreement, HPKE, quantum-secure key encapsulation
and signatures, and keys kept in the Secure Enclave. For new code (Swift 6.3+)
reach for it before CommonCrypto or raw Security framework calls.

| Feature set | Minimum iOS |
|---|---|
| Hashing, HMAC, AES-GCM, ChaChaPoly, P-curves, Curve25519, Secure Enclave P256 | 13 |
| HPKE | 17 |
| SHA-3, ML-KEM, ML-DSA, X-Wing | 26 |

## Hashing

`SHA256`, `SHA384` and `SHA512` exist from iOS 13; `SHA3_256`, `SHA3_384` and
`SHA3_512` from iOS 26. All of them conform to `HashFunction`, so the API is the
same for each.

```swift
import CryptoKit
import Foundation

func fingerprint(_ blob: Data) -> String {
    SHA256.hash(data: blob).map { String(format: "%02x", $0) }.joined()
}

func strongerFingerprint(_ blob: Data) -> Data {
    if #available(iOS 26.0, *) {
        return Data(SHA3_384.hash(data: blob))
    }
    return Data(SHA384.hash(data: blob))
}

func streamingDigest(_ chunks: [Data]) -> SHA512.Digest {
    var running = SHA512()
    for chunk in chunks {
        running.update(data: chunk)
    }
    return running.finalize()
}
```

- Put SHA-3 behind `if #available(iOS 26.0, *)` unless the deployment target is
  already 26.
- Use the incremental `update(data:)` / `finalize()` form for large or
  streamed input.
- Compare digests with `==` on the digest values. Do not turn them into strings
  or arrays for a security-relevant equality check.

## HMAC

An HMAC proves that data came from someone holding a shared symmetric key.

```swift
import CryptoKit
import Foundation

let webhookKey = SymmetricKey(size: .bits256)

func tag(_ body: Data) -> HMAC<SHA256>.MAC {
    HMAC<SHA256>.authenticationCode(for: body, using: webhookKey)
}

func isGenuine(_ body: Data, mac: HMAC<SHA256>.MAC) -> Bool {
    HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: body, using: webhookKey)
}

func tagInPieces(_ parts: [Data]) -> HMAC<SHA256>.MAC {
    var signer = HMAC<SHA256>(key: webhookKey)
    parts.forEach { signer.update(data: $0) }
    return signer.finalize()
}
```

`isValidAuthenticationCode(_:authenticating:using:)` compares in constant time.

## Symmetric encryption

Both ciphers are AEAD: the sealed box bundles a nonce, the ciphertext and an
authentication tag.

- **AES-GCM** is the default. Apple silicon accelerates it in hardware.
- **ChaChaPoly** fits devices without AES acceleration or protocols that
  mandate ChaCha20-Poly1305, such as TLS cipher suites or WireGuard.

```swift
import CryptoKit
import Foundation

func lockNote(_ note: Data, key: SymmetricKey) throws -> Data {
    let box = try AES.GCM.seal(note, using: key)
    guard let packed = box.combined else { throw CryptoKitError.incorrectParameterSize }
    return packed
}

func unlockNote(_ packed: Data, key: SymmetricKey) throws -> Data {
    try AES.GCM.open(AES.GCM.SealedBox(combined: packed), using: key)
}

func lockWithChaCha(_ note: Data, key: SymmetricKey) throws -> Data {
    try ChaChaPoly.seal(note, using: key).combined
}

func unlockWithChaCha(_ packed: Data, key: SymmetricKey) throws -> Data {
    try ChaChaPoly.open(ChaChaPoly.SealedBox(combined: packed), using: key)
}
```

`combined` is optional on `AES.GCM.SealedBox` (it is `nil` when a non-standard
nonce size was used) and non-optional on `ChaChaPoly.SealedBox`.

### Additional authenticated data

Metadata that must travel in the clear but must not be altered goes in the AAD.
It is authenticated, not encrypted.

```swift
import CryptoKit
import Foundation

func sealRecord(_ payload: Data, recordID: String, key: SymmetricKey) throws -> AES.GCM.SealedBox {
    try AES.GCM.seal(payload, using: key, authenticating: Data(recordID.utf8))
}

func openRecord(_ box: AES.GCM.SealedBox, recordID: String, key: SymmetricKey) throws -> Data {
    try AES.GCM.open(box, using: key, authenticating: Data(recordID.utf8))
}
```

### Keys

`SymmetricKey(size: .bits256)` gives AES-256-GCM or ChaChaPoly strength and is
the right default. Wrap existing key bytes with `SymmetricKey(data:)`.

## Signatures

ECDSA uses the NIST curves; Ed25519 comes from `Curve25519`.

```swift
import CryptoKit
import Foundation

func signRelease(_ manifest: Data) throws -> (P256.Signing.PublicKey, P256.Signing.ECDSASignature) {
    let signer = P256.Signing.PrivateKey()
    return (signer.publicKey, try signer.signature(for: manifest))
}

func verifyRelease(_ manifest: Data, key: P256.Signing.PublicKey, sig: P256.Signing.ECDSASignature) -> Bool {
    key.isValidSignature(sig, for: manifest)
}

func edSign(_ message: Data) throws -> (Curve25519.Signing.PublicKey, Data) {
    let signer = Curve25519.Signing.PrivateKey()
    return (signer.publicKey, try signer.signature(for: message))
}
```

P384 and P521 have the same shape as P256. NIST keys serialize as DER, PEM,
X9.63 or raw. Curve25519 keys offer only `rawRepresentation`.

### Choosing a curve

| Curve | Algorithm | Size | Notes |
|---|---|---|---|
| P256 | ECDSA | 256-bit | general default; Secure Enclave capable |
| P384 | ECDSA | 384-bit | higher margin |
| P521 | ECDSA | 521-bit | top NIST level |
| Curve25519 | Ed25519 | 256-bit | fast and simple; no Secure Enclave |

Start with P256. Use Curve25519 when a protocol specifies Ed25519.

## Key agreement

ECDH turns two key pairs into one shared secret. Each side combines its own
private key with the other side's public key, and both arrive at the same value.

```swift
import CryptoKit
import Foundation

func sessionKey(mine: P256.KeyAgreement.PrivateKey, theirs: P256.KeyAgreement.PublicKey) throws -> SymmetricKey {
    let secret = try mine.sharedSecretFromKeyAgreement(with: theirs)
    return secret.hkdfDerivedSymmetricKey(
        using: SHA256.self,
        salt: Data("chat-v2-salt".utf8),
        sharedInfo: Data("chat-v2 message key".utf8),
        outputByteCount: 32
    )
}

func x25519Key(mine: Curve25519.KeyAgreement.PrivateKey, theirs: Curve25519.KeyAgreement.PublicKey) throws -> SymmetricKey {
    try mine.sharedSecretFromKeyAgreement(with: theirs).hkdfDerivedSymmetricKey(
        using: SHA256.self,
        salt: Data(),
        sharedInfo: Data("sync-pairing v1".utf8),
        outputByteCount: 32
    )
}
```

### Derivation

A `SharedSecret` is not a `SymmetricKey` and must always go through a KDF.

| Method | Standard | When |
|---|---|---|
| `hkdfDerivedSymmetricKey` | HKDF, RFC 5869 | default choice |
| `x963DerivedSymmetricKey` | ANSI X9.63 | interoperating with X9.63 systems |

Always pass a non-empty `sharedInfo` that names the protocol and purpose, so
the derived key cannot be reused somewhere else.

## HPKE

HPKE (iOS 17+) is the ready-made way to encrypt to someone's public key.
Prefer it over wiring ECDH, HKDF and an AEAD together by hand.

```swift
import CryptoKit
import Foundation

func encryptForDevice(_ messages: [Data], to device: Curve25519.KeyAgreement.PublicKey) throws -> (Data, [Data]) {
    let context = Data("device-backup v3".utf8)
    var sender = try HPKE.Sender(recipientKey: device, ciphersuite: .Curve25519_SHA256_ChachaPoly, info: context)
    let sealed = try messages.map { try sender.seal($0, authenticating: Data("header".utf8)) }
    return (sender.encapsulatedKey, sealed)
}

func decryptOnDevice(_ sealed: [Data], encapsulated: Data, key: Curve25519.KeyAgreement.PrivateKey) throws -> [Data] {
    let context = Data("device-backup v3".utf8)
    var receiver = try HPKE.Recipient(privateKey: key, ciphersuite: .Curve25519_SHA256_ChachaPoly,
                                      info: context, encapsulatedKey: encapsulated)
    return try sealed.map { try receiver.open($0, authenticating: Data("header".utf8)) }
}
```

- `HPKE.Sender` and `HPKE.Recipient` keep a sequence counter, so they must be
  `var`.
- Ship `encapsulatedKey` together with the ciphertext.
- The recipient must open messages in the same order the sender sealed them.

## Post-quantum CryptoKit (iOS 26+)

| Purpose | Types |
|---|---|
| Key encapsulation | `MLKEM768`, `MLKEM1024` |
| Hybrid HPKE | `XWingMLKEM768X25519` with `.XWingMLKEM768X25519_SHA256_AES_GCM_256` |
| Signatures | `MLDSA65`, `MLDSA87` |
| Secure Enclave | `SecureEnclave.MLKEM768`, `SecureEnclave.MLKEM1024`, `SecureEnclave.MLDSA65`, `SecureEnclave.MLDSA87` |

- During a migration where both classical and quantum resistance matter, use
  the hybrid X-Wing suite.
- Keys, ciphertexts and signatures are far larger than their P256 or
  Curve25519 counterparts. Budget for that in storage and on the wire.

## Secure Enclave

The Secure Enclave generates and holds private keys in hardware; they never
leave it.

- Classical support covers P256 signing and P256 key agreement only.
- On iOS 26 with supported hardware, ML-KEM and ML-DSA keys can live there too.
- Always check `SecureEnclave.isAvailable` and fall back to software keys.

```swift
import CryptoKit
import Foundation
import Security

enum DeviceKeyError: Error {
    case noSecureEnclave
    case accessControlRejected
}

func deviceSigningKey() throws -> Data {
    guard SecureEnclave.isAvailable else { throw DeviceKeyError.noSecureEnclave }
    guard let rule = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
                                                     [.privateKeyUsage, .biometryCurrentSet], nil) else {
        throw DeviceKeyError.accessControlRejected
    }
    let key = try SecureEnclave.P256.Signing.PrivateKey(accessControl: rule)
    _ = key.publicKey
    return key.dataRepresentation
}

func restoreDeviceKey(_ blob: Data) throws -> SecureEnclave.P256.Signing.PrivateKey {
    try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: blob)
}

func enclaveAgreement(with peer: P256.KeyAgreement.PublicKey) throws -> SharedSecret {
    let mine = try SecureEnclave.P256.KeyAgreement.PrivateKey()
    return try mine.sharedSecretFromKeyAgreement(with: peer)
}
```

- The public key of a Secure Enclave signing key is an ordinary
  `P256.Signing.PublicKey`; signing and verifying look the same as in software.
- Gate use behind biometrics or the passcode with a `SecAccessControl` that
  includes `.privateKeyUsage`. Deeper Keychain policy choices belong to
  `swift-security`.
- `dataRepresentation` is an encrypted blob that only the same device's Secure
  Enclave can turn back into a key. Store it in the Keychain and restore it
  with `init(dataRepresentation:)`.

## Common mistakes

- **Using the raw shared secret as a key.** Always derive with HKDF.
- **Fixed or repeated nonces.** Passing something like
  `AES.GCM.Nonce(data: Data(count: 12))` to `seal(_:using:nonce:)` for every
  message breaks GCM. Let CryptoKit pick a random nonce by leaving the argument
  out.
- **Decrypting without the tag check.** Never peel the tag off and decrypt by
  hand. `AES.GCM.open` and `ChaChaPoly.open` verify it for you.
- **Insecure hashes for security.** `Insecure.MD5` and `Insecure.SHA1` exist
  for legacy checksums and protocol interop only.
- **Key bytes in `UserDefaults`.** Keys go in the Keychain; see
  [the Keychain patterns](references/cryptokit-patterns.md#keychain-storage).
- **Secure Enclave without an availability check.** Key creation fails in
  Simulator and on unsupported hardware.

## Review checklist

- [ ] CryptoKit is used instead of CommonCrypto or raw Security calls
- [ ] Hashing uses SHA-256 or stronger; no MD5 or SHA-1 for security
- [ ] HMACs are verified with `isValidAuthenticationCode`
- [ ] Encryption is AES-GCM or ChaChaPoly with 256-bit keys
- [ ] Nonces are random, never hardcoded or reused
- [ ] AAD protects metadata that needs integrity
- [ ] Every `SharedSecret` goes through HKDF
- [ ] `sharedInfo` is non-empty and specific to the context
- [ ] HPKE replaces hand-built ECDH + HKDF + AEAD for recipient encryption on iOS 17+
- [ ] SHA-3 and post-quantum code is behind an iOS 26 availability check
- [ ] `SecureEnclave.isAvailable` is checked
- [ ] Secure Enclave `dataRepresentation` is stored in the Keychain
- [ ] Private keys are never logged, printed or serialized without need
- [ ] Symmetric keys live in the Keychain, not `UserDefaults` or loose files
- [ ] Export compliance (`ITSAppUsesNonExemptEncryption`) has been considered

## References

- [CryptoKit patterns](references/cryptokit-patterns.md) - key serialization, Keychain storage, AES key wrap, HKDF, HPKE ciphersuites, post-quantum usage, the Insecure namespace, sealed box layout, digest signing, export compliance, performance, CommonCrypto migration
- [CryptoKit](https://developer.apple.com/documentation/cryptokit)
- [HPKE](https://developer.apple.com/documentation/cryptokit/hpke)
- [Enhancing your app's privacy and security with quantum-secure workflows](https://developer.apple.com/documentation/cryptokit/enhancing-your-app-s-privacy-and-security-with-quantum-secure-workflows)
- [Performing common cryptographic operations](https://developer.apple.com/documentation/cryptokit/performing-common-cryptographic-operations)
- [Storing CryptoKit keys in the keychain](https://developer.apple.com/documentation/cryptokit/storing-cryptokit-keys-in-the-keychain)
