# CryptoKit patterns

Detail that sits behind the main skill: how keys are serialized and stored,
less common primitives, sizes, compliance and migration.

## Contents

- [Key serialization](#key-serialization)
- [Keychain storage](#keychain-storage)
- [AES key wrap](#aes-key-wrap)
- [HKDF on its own](#hkdf-on-its-own)
- [HPKE details](#hpke-details)
- [Post-quantum usage](#post-quantum-usage)
- [The Insecure namespace](#the-insecure-namespace)
- [Sealed box layout](#sealed-box-layout)
- [Signing a digest](#signing-a-digest)
- [Export compliance](#export-compliance)
- [Performance](#performance)
- [Moving from CommonCrypto](#moving-from-commoncrypto)

## Key serialization

| Key | Property / initializer | Use |
|---|---|---|
| NIST private (P256/P384/P521) | `derRepresentation` / `init(derRepresentation:)` | exchange with non-Apple systems |
| | `pemRepresentation` / `init(pemRepresentation:)` | text formats, config files |
| | `x963Representation` / `init(x963Representation:)` | `SecKey` and Keychain interop |
| | `rawRepresentation` / `init(rawRepresentation:)` | the private scalar only |
| NIST public | all of the above, plus `compactRepresentation` (optional, can be `nil`) and `compressedRepresentation` | |
| Curve25519 private and public | `rawRepresentation` / `init(rawRepresentation:)` only | |
| ECDSA signature | `derRepresentation` (interop), `rawRepresentation` (r followed by s) | |

```swift
import CryptoKit
import Foundation

func roundTrip() throws {
    let issuer = P384.Signing.PrivateKey()
    let asPEM = issuer.publicKey.pemRepresentation
    let reloaded = try P384.Signing.PublicKey(pemRepresentation: asPEM)

    let sig = try issuer.signature(for: Data("ticket-42".utf8))
    let wire = sig.derRepresentation
    let back = try P384.Signing.ECDSASignature(derRepresentation: wire)
    let compact = try P384.Signing.ECDSASignature(rawRepresentation: sig.rawRepresentation)
    _ = (reloaded.isValidSignature(back, for: Data("ticket-42".utf8)), compact)
}
```

Rule: DER when the other end is not CryptoKit; raw when both ends are CryptoKit
and size matters.

## Keychain storage

Two strategies, chosen by whether the key has an X9.63 form that `SecKey`
understands.

### NIST keys as SecKey items

```swift
import CryptoKit
import Foundation
import Security

protocol SecKeyStorable {
    init<Bytes: ContiguousBytes>(x963Representation: Bytes) throws
    var x963Representation: Data { get }
}

extension P256.Signing.PrivateKey: SecKeyStorable {}
extension P256.KeyAgreement.PrivateKey: SecKeyStorable {}
extension P384.Signing.PrivateKey: SecKeyStorable {}
extension P384.KeyAgreement.PrivateKey: SecKeyStorable {}
extension P521.Signing.PrivateKey: SecKeyStorable {}
extension P521.KeyAgreement.PrivateKey: SecKeyStorable {}

enum VaultError: Error {
    case conversion(String)
    case status(OSStatus)
}

func storeECKey<K: SecKeyStorable>(_ key: K, label: String) throws {
    let attributes = [
        kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
        kSecAttrKeyClass: kSecAttrKeyClassPrivate
    ] as [String: Any]
    var cfError: Unmanaged<CFError>?
    guard let keyRef = SecKeyCreateWithData(key.x963Representation as CFData, attributes as CFDictionary, &cfError) else {
        throw VaultError.conversion(cfError.map { String(describing: $0.takeRetainedValue()) } ?? "unknown")
    }
    let item = [
        kSecClass: kSecClassKey,
        kSecAttrApplicationLabel: label,
        kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
        kSecUseDataProtectionKeychain: true,
        kSecValueRef: keyRef
    ] as [String: Any]
    let status = SecItemAdd(item as CFDictionary, nil)
    guard status == errSecSuccess else { throw VaultError.status(status) }
}

func loadECKey<K: SecKeyStorable>(label: String) throws -> K? {
    let lookup = [
        kSecClass: kSecClassKey,
        kSecAttrApplicationLabel: label,
        kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
        kSecUseDataProtectionKeychain: true,
        kSecReturnRef: true
    ] as [String: Any]
    var found: CFTypeRef?
    switch SecItemCopyMatching(lookup as CFDictionary, &found) {
    case errSecSuccess:
        guard let ref = found, CFGetTypeID(ref) == SecKeyGetTypeID() else { return nil }
        var failure: Unmanaged<CFError>?
        guard let bytes = SecKeyCopyExternalRepresentation(ref as! SecKey, &failure) as Data? else {
            throw VaultError.conversion("export failed")
        }
        return try K(x963Representation: bytes)
    case errSecItemNotFound:
        return nil
    case let other:
        throw VaultError.status(other)
    }
}
```

The P256, P384 and P521 signing and key-agreement private keys all have the
X9.63 initializer and property, so they conform to the helper protocol with an
empty extension. Reading asks for `kSecReturnRef`, exports the `SecKey` with
`SecKeyCopyExternalRepresentation` and rebuilds the CryptoKit key; a missing
item returns `nil` and every other status throws.

### Everything else as generic passwords

Curve25519 keys and `SymmetricKey` have no X9.63 form. Store their raw bytes as
`kSecClassGenericPassword` items.

```swift
import CryptoKit
import Foundation
import Security

protocol RawKeyStorable {
    init<Bytes: ContiguousBytes>(rawKeyBytes: Bytes) throws
    var rawKeyBytes: SymmetricKey { get }
}

extension Curve25519.Signing.PrivateKey: RawKeyStorable {
    init<Bytes: ContiguousBytes>(rawKeyBytes: Bytes) throws { try self.init(rawRepresentation: rawKeyBytes) }
    var rawKeyBytes: SymmetricKey { SymmetricKey(data: rawRepresentation) }
}

extension Curve25519.KeyAgreement.PrivateKey: RawKeyStorable {
    init<Bytes: ContiguousBytes>(rawKeyBytes: Bytes) throws { try self.init(rawRepresentation: rawKeyBytes) }
    var rawKeyBytes: SymmetricKey { SymmetricKey(data: rawRepresentation) }
}

extension SymmetricKey: RawKeyStorable {
    init<Bytes: ContiguousBytes>(rawKeyBytes: Bytes) throws { self.init(data: rawKeyBytes) }
    var rawKeyBytes: SymmetricKey { self }
}

func storeRawKey<K: RawKeyStorable>(_ key: K, account: String) throws {
    let bytes = key.rawKeyBytes.withUnsafeBytes { Data($0) }
    let item = [
        kSecClass: kSecClassGenericPassword,
        kSecAttrAccount: account,
        kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked,
        kSecUseDataProtectionKeychain: true,
        kSecValueData: bytes
    ] as [String: Any]
    let status = SecItemAdd(item as CFDictionary, nil)
    if status != errSecSuccess { throw NSError(domain: NSOSStatusErrorDomain, code: Int(status)) }
}
```

Secure Enclave keys follow the same generic-password route: store the encrypted
`dataRepresentation` as the password data and rebuild the key with
`init(dataRepresentation:)` after reading it back.

## AES key wrap

RFC 3394 key wrapping encrypts one key (the data key) under another (the key
encryption key). Use it to store or send keys protected by a master key.

```swift
import CryptoKit
import Foundation

func wrapFileKey(_ fileKey: SymmetricKey, under master: SymmetricKey) throws -> Data {
    try AES.KeyWrap.wrap(fileKey, using: master)
}

func unwrapFileKey(_ wrapped: Data, under master: SymmetricKey) throws -> SymmetricKey {
    try AES.KeyWrap.unwrap(wrapped, using: master)
}
```

## HKDF on its own

HKDF (RFC 5869) is also available directly, for key material that did not come
from ECDH.

```swift
import CryptoKit
import Foundation

func deriveSubkeys(from material: SymmetricKey) -> (SymmetricKey, SymmetricKey) {
    let direct = HKDF<SHA256>.deriveKey(
        inputKeyMaterial: material,
        salt: Data("vault-salt".utf8),
        info: Data("vault index key".utf8),
        outputByteCount: 32
    )
    let pseudoKey = HKDF<SHA256>.extract(inputKeyMaterial: material, salt: Data("vault-salt".utf8))
    let expanded = HKDF<SHA256>.expand(pseudoRandomKey: pseudoKey, info: Data("vault blob key".utf8), outputByteCount: 32)
    return (direct, expanded)
}
```

`deriveKey` does both steps at once; `extract` then `expand` gives the
pseudorandom key in between.

## HPKE details

HPKE is RFC 9180. It is available from iOS 17; the X-Wing hybrid suite needs
iOS 26.

```swift
import CryptoKit
import Foundation

func sendToServer(_ report: Data, serverKey: P256.KeyAgreement.PublicKey) throws -> (ciphertext: Data, encapsulated: Data) {
    var hpkeSender = try HPKE.Sender(recipientKey: serverKey, ciphersuite: .P256_SHA256_AES_GCM_256,
                                     info: Data("crash-report v1".utf8))
    return (try hpkeSender.seal(report), hpkeSender.encapsulatedKey)
}

func receiveOnServer(_ ciphertext: Data, encapsulated: Data, key: P256.KeyAgreement.PrivateKey) throws -> Data {
    var hpkeRecipient = try HPKE.Recipient(privateKey: key, ciphersuite: .P256_SHA256_AES_GCM_256,
                                           info: Data("crash-report v1".utf8), encapsulatedKey: encapsulated)
    return try hpkeRecipient.open(ciphertext)
}
```

| Ciphersuite | Minimum iOS | Key encapsulation | Key derivation | Authenticated cipher |
|---|---|---|---|---|
| `.P256_SHA256_AES_GCM_256` | 17 | NIST P-256 | SHA-256 HKDF | 256-bit AES-GCM |
| `.P384_SHA384_AES_GCM_256` | 17 | NIST P-384 | SHA-384 HKDF | 256-bit AES-GCM |
| `.P521_SHA512_AES_GCM_256` | 17 | NIST P-521 | SHA-512 HKDF | 256-bit AES-GCM |
| `.Curve25519_SHA256_ChachaPoly` | 17 | X25519 | SHA-256 HKDF | ChaCha20-Poly1305 |
| `.XWingMLKEM768X25519_SHA256_AES_GCM_256` | 26 | X-Wing hybrid (ML-KEM-768 with X25519) | SHA-256 HKDF | 256-bit AES-GCM |

## Post-quantum usage

Guard ML-KEM, ML-DSA and X-Wing with an iOS 26 availability check unless the
deployment target is 26 or later.

```swift
import CryptoKit
import Foundation

@available(iOS 26.0, *)
func kemHandshake() throws -> Bool {
    let receiverKey = try MLKEM768.PrivateKey()
    let result = try receiverKey.publicKey.encapsulate()
    let transmitted = result.encapsulated
    let recovered = try receiverKey.decapsulate(transmitted)
    return recovered == result.sharedSecret
}

@available(iOS 26.0, *)
func quantumSafeSignature(_ firmware: Data) throws -> Bool {
    let signer = try MLDSA65.PrivateKey()
    let sig = try signer.signature(for: firmware)
    return signer.publicKey.isValidSignature(sig, for: firmware)
}
```

- The sender runs `encapsulate()` on the receiver's public key and transmits
  `encapsulated`. The receiver calls `decapsulate(_:)`.
- Both the sender's `sharedSecret` and the receiver's decapsulated value are a
  `SymmetricKey`, and they are equal.
- The Secure Enclave variants listed in the main skill exist on supported
  hardware.

## The Insecure namespace

`Insecure.MD5` and `Insecure.SHA1` live in the `Insecure` enum so that every
use is visibly flagged at the call site.

| Fine | Never |
|---|---|
| verifying a legacy checksum | hashing passwords |
| cache keys, ETags, content hashes | integrity that security depends on |
| a protocol that requires MD5 or SHA-1 | digital signatures |
| | HMAC authentication |

```swift
import CryptoKit
import Foundation

func legacyETag(for body: Data) -> String {
    Insecure.MD5.hash(data: body).reduce(into: "") { $0 += String(format: "%02x", $1) }
}
```

## Sealed box layout

For both AES-GCM and ChaChaPoly:

| Part | Size |
|---|---|
| nonce | 12 bytes |
| ciphertext | same length as the plaintext |
| tag | 16 bytes |

`combined` is nonce, then ciphertext, then tag. The parts are also exposed as
`.nonce`, `.ciphertext` and `.tag`.

```swift
import CryptoKit
import Foundation

func reassemble(nonce: Data, body: Data, tag: Data, key: SymmetricKey) throws -> Data {
    let sealed = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: nonce), ciphertext: body, tag: tag)
    return try AES.GCM.open(sealed, using: key)
}
```

When the parts arrive already concatenated, use `SealedBox(combined:)` instead.

## Signing a digest

The NIST curves can sign and verify a digest you already computed, which avoids
hashing the data twice.

```swift
import CryptoKit
import Foundation

func signPrehashed(_ file: Data, key: P521.Signing.PrivateKey) throws -> Bool {
    let digest = SHA512.hash(data: file)
    let signature = try key.signature(for: digest)
    return key.publicKey.isValidSignature(signature, for: digest)
}
```

## Export compliance

Apps that use encryption declare it in App Store Connect and set the Boolean
Info.plist key `ITSAppUsesNonExemptEncryption`.

- `false` when the app only uses encryption that Apple's platforms provide (HTTPS
  through `URLSession`, CryptoKit protecting data on the device) plus standard
  authentication such as OAuth, SAML or biometrics.
- `true` when it implements its own encryption protocol, offers a non-standard
  encrypted service, or encrypts data it sends to third-party servers. In that
  case the app needs an export classification (or a compliance review) first.
  Apple's guide on this lives at
  <https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations>.

## Performance

- AES-GCM runs in hardware on Apple silicon and is usually faster.
  ChaChaPoly only wins where AES acceleration is missing, which is rare on
  current Apple devices. Default to AES-GCM.
- Hash big files in pieces instead of loading them whole:

```swift
import CryptoKit
import Foundation

func sha256OfFile(_ fileURL: URL) throws -> SHA256.Digest {
    let reader = try FileHandle(forReadingFrom: fileURL)
    defer { try? reader.close() }
    var hasher = SHA256()
    var finished = false
    while !finished {
        try autoreleasepool {
            if let piece = try reader.read(upToCount: 1 << 20), !piece.isEmpty {
                hasher.update(data: piece)
            } else {
                finished = true
            }
        }
    }
    return hasher.finalize()
}
```

- Relative cost of key generation, cheapest first: `SymmetricKey(size:)`
  (straight from the system random generator), P256, P384, P521, and
  `SecureEnclave.P256.*.PrivateKey()` (a trip to the hardware).
- Generate a key once and keep it; never create one per operation.

## Moving from CommonCrypto

| CommonCrypto | CryptoKit |
|---|---|
| `CC_SHA256` into a manually sized buffer | `SHA256.hash(data:)` |
| `CCHmac(kCCHmacAlgSHA256, ...)` | `HMAC<SHA256>.authenticationCode(for:using:)` |
| `CCCrypt` with AES, a hand-managed IV and padding (dozens of lines) | `AES.GCM.seal` / `AES.GCM.open` |

What changes for the better:

- Encryption is authenticated by default; there is no unauthenticated CBC mode
  to reach for by accident.
- Keys and nonces have their own types.
- Nonces are generated for you.
- No manual buffer sizing or pointer handling.
- Comparisons of MACs and signatures are constant-time.
- The types are `Sendable`.
