README.md
| 1 | # PQC Secure Enclave SDK |
| 2 | |
| 3 |  |
| 4 |  |
| 5 |  |
| 6 |  |
| 7 |  |
| 8 |  |
| 9 | |
| 10 | **Quantum-safe on-device AI.** A clean Python SDK for storing AI model weights, LoRA adapters, tokenizers, and API credentials in **device secure enclaves** using **ML-KEM-768** key encapsulation + **AES-256-GCM** encryption. Pluggable backends for Apple Secure Enclave, Android StrongBox, and Qualcomm QSEE let you ship quantum-resistant on-device AI today - without waiting for the platform vendors to finish their PQC rollouts. |
| 11 | |
| 12 | ## The Problem |
| 13 | |
| 14 | Your phone runs AI inference constantly: autocomplete, voice recognition, image classification, on-device LLMs. The model weights and API credentials those features rely on sit in device storage for **years** - Apple Neural Engine, Qualcomm AI Engine, and MediaTek APU models typically persist across OS upgrades. Today they are protected by classical cryptography baked into the secure element. |
| 15 | |
| 16 | This is the **HNDL threat model** (Harvest Now, Decrypt Later) applied to on-device AI: |
| 17 | |
| 18 | - An attacker who exfiltrates encrypted weight files today - from backups, compromised cloud sync, supply-chain tooling, or forensic device imaging - can store them indefinitely. |
| 19 | - When a cryptographically relevant quantum computer arrives, every RSA/ECDSA-wrapped symmetric key is retroactively broken and the plaintext weights fall out. |
| 20 | - For proprietary fine-tunes, biometric templates, and long-lived OAuth refresh tokens, "eventually decrypted" is functionally equivalent to "decrypted". |
| 21 | |
| 22 | ## The Solution |
| 23 | |
| 24 | Wrap every on-device AI artifact in a PQC-protected envelope: |
| 25 | |
| 26 | - **ML-KEM-768** (FIPS 203, NIST PQC) for the session key that the enclave unwraps. |
| 27 | - **AES-256-GCM** (FIPS 197) for the artifact body. Key is 32 bytes, nonce 12 bytes, tag 16 bytes. |
| 28 | - **SHA3-256** content hash authenticated via AES-GCM AAD - any metadata tampering breaks decryption. |
| 29 | - **ML-DSA** (FIPS 204) signatures for device attestations that commit to what was stored. |
| 30 | - Pluggable backends: `iOSEnclaveBackend`, `AndroidEnclaveBackend`, `QSEEBackend`, plus `InMemoryEnclaveBackend` for tests. |
| 31 | |
| 32 | ## Installation |
| 33 | |
| 34 | ```bash |
| 35 | pip install pqc-enclave-sdk |
| 36 | ``` |
| 37 | |
| 38 | Development: |
| 39 | |
| 40 | ```bash |
| 41 | pip install -e ".[dev]" |
| 42 | ``` |
| 43 | |
| 44 | ## Quick Start |
| 45 | |
| 46 | ```python |
| 47 | from pqc_enclave_sdk import ( |
| 48 | ArtifactKind, |
| 49 | EnclaveVault, |
| 50 | InMemoryEnclaveBackend, |
| 51 | ) |
| 52 | |
| 53 | backend = InMemoryEnclaveBackend(device_id="iphone-alice", device_model="iphone-15-pro") |
| 54 | vault = EnclaveVault(backend=backend) |
| 55 | |
| 56 | vault.unlock() |
| 57 | vault.put_artifact( |
| 58 | name="llama-3.2-1b-int4", |
| 59 | kind=ArtifactKind.MODEL_WEIGHTS, |
| 60 | content=weights_bytes, |
| 61 | version="1.0.0", |
| 62 | app_bundle_id="com.example.localllm", |
| 63 | ) |
| 64 | vault.save() |
| 65 | vault.lock() |
| 66 | |
| 67 | # Later, in the same process or another app launch: |
| 68 | vault.unlock() |
| 69 | weights = vault.get_artifact("llama-3.2-1b-int4").content |
| 70 | ``` |
| 71 | |
| 72 | ## Architecture |
| 73 | |
| 74 | ``` |
| 75 | Your App EnclaveVault EnclaveBackend Device Secure Enclave |
| 76 | -------- ------------ -------------- --------------------- |
| 77 | | | | | |
| 78 | | put_artifact(bytes) | | | |
| 79 | | ---------------------> | | | |
| 80 | | | 1. derive session key | | |
| 81 | | | via ML-KEM-768 | | |
| 82 | | | 2. AES-256-GCM encrypt | | |
| 83 | | | with content-hash AAD| | |
| 84 | | | 3. store_session_key ------------------------------>| |
| 85 | | | | wraps w/ hardware KEK | |
| 86 | | | 4. save_artifacts | | |
| 87 | | | ----------------------> | | |
| 88 | | | | persists ciphertext | |
| 89 | | | | to Keychain/Keystore | |
| 90 | | | | | |
| 91 | | get_artifact(name) | | | |
| 92 | | ---------------------> | | | |
| 93 | | | 5. load_session_key --------------------------------| |
| 94 | | | (unwrap inside SEP) | |
| 95 | | | 6. AES-256-GCM decrypt | | |
| 96 | | <--- plaintext | | | |
| 97 | ``` |
| 98 | |
| 99 | ## Artifact Kinds |
| 100 | |
| 101 | | Kind | Purpose | |
| 102 | |---|---| |
| 103 | | `MODEL_WEIGHTS` | Full model weight tensors (INT4 / INT8 / FP16 on-device checkpoints). | |
| 104 | | `LORA_ADAPTER` | Low-rank fine-tune adapters; smaller but sensitive for proprietary tunes. | |
| 105 | | `TOKENIZER` | Tokenizer vocab + merges; lower-sensitivity but integrity-critical. | |
| 106 | | `CREDENTIAL` | API keys, OAuth tokens, auth bearer tokens. | |
| 107 | | `BIOMETRIC_TEMPLATE` | Encoded face / fingerprint templates. Highest sensitivity. | |
| 108 | | `INFERENCE_CACHE` | KV-cache blobs from prior conversations. | |
| 109 | | `SAFETY_MODEL` | Jailbreak classifier / content-safety adapter. | |
| 110 | | `OTHER` | Everything else. | |
| 111 | |
| 112 | ## Cryptography |
| 113 | |
| 114 | | Primitive | Role | Standard | |
| 115 | |---|---|---| |
| 116 | | **ML-KEM-768** | Session-key encapsulation to the enclave's PQC public key | FIPS 203 | |
| 117 | | **AES-256-GCM** | Symmetric encryption of every artifact body | FIPS 197 / SP 800-38D | |
| 118 | | **SHA3-256** | Content hash + canonical AAD hashing | FIPS 202 | |
| 119 | | **ML-DSA-65 / 87** | Signatures over DeviceAttestations | FIPS 204 | |
| 120 | |
| 121 | The AES-GCM AAD covers the full artifact metadata plus the content hash plus the key id - any metadata swap or cross-artifact key reuse is detected on decrypt. |
| 122 | |
| 123 | ## Threat Model |
| 124 | |
| 125 | | Threat | Mitigation | |
| 126 | |---|---| |
| 127 | | **Device theft** (attacker has the phone) | Symmetric key never leaves the enclave. Access control requires biometrics / device unlock. | |
| 128 | | **HNDL on stored weights** (exfiltrated encrypted blobs today, decrypted post-CRQC) | ML-KEM-768 session-key encapsulation; AES-256-GCM (Grover-adjusted 128-bit security). | |
| 129 | | **Rogue app reading another app's artifacts** | `AccessPolicy.allowed_bundle_ids` filters callers; OS Keychain / Keystore access-control flags enforce at the kernel level. | |
| 130 | | **Stale session key** (long-lived re-use) | `DEFAULT_SESSION_TTL = 3600`; `is_unlocked` re-checks expiration on every call. | |
| 131 | | **Post-quantum forgery of attestation** | `DeviceAttester` signs with ML-DSA, not ECDSA. | |
| 132 | | **Artifact swap** (attacker substitutes one encrypted blob for another) | AAD includes `artifact_id` and content hash; decryption of a swapped blob against the wrong metadata fails. | |
| 133 | | **Downgrade to classical crypto** | Algorithm is baked into the AAD; a rewrite requires access to the PQC session key. | |
| 134 | |
| 135 | ## Backend Integration Guides |
| 136 | |
| 137 | ### iOS Secure Enclave (CryptoKit sketch) |
| 138 | |
| 139 | ```swift |
| 140 | import CryptoKit |
| 141 | |
| 142 | // 1. Generate a non-extractable SEP key at app install. |
| 143 | let sepKey = try SecureEnclave.P256.KeyAgreement.PrivateKey( |
| 144 | accessControl: SecAccessControlCreateWithFlags( |
| 145 | nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, |
| 146 | [.privateKeyUsage, .biometryCurrentSet], nil)! |
| 147 | ) |
| 148 | |
| 149 | // 2. On unlock, receive the 32-byte AES-GCM key from the Python SDK |
| 150 | // (ideally via an ML-KEM-768 ciphertext the SEP decapsulates). Wrap it |
| 151 | // with the SEP key and write the sealed blob to the Keychain: |
| 152 | let sealedBox = try AES.GCM.seal(sessionKey, using: sepSymmetricKey) |
| 153 | SecItemAdd([ |
| 154 | kSecClass: kSecClassGenericPassword, |
| 155 | kSecAttrService: "com.dyber.pqc.enclave", |
| 156 | kSecAttrTokenID: kSecAttrTokenIDSecureEnclave, |
| 157 | kSecValueData: sealedBox.combined!, |
| 158 | ] as CFDictionary, nil) |
| 159 | ``` |
| 160 | |
| 161 | ### Android StrongBox (Kotlin sketch) |
| 162 | |
| 163 | ```kotlin |
| 164 | val spec = KeyGenParameterSpec.Builder( |
| 165 | "com.dyber.pqc.enclave.session", |
| 166 | KeyProperties.PURPOSE_WRAP_KEY or KeyProperties.PURPOSE_ENCRYPT |
| 167 | ).setBlockModes(KeyProperties.BLOCK_MODE_GCM) |
| 168 | .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) |
| 169 | .setIsStrongBoxBacked(true) // Titan M / Knox Vault |
| 170 | .setUserAuthenticationRequired(true) |
| 171 | .setUnlockedDeviceRequired(true) |
| 172 | .build() |
| 173 | val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") |
| 174 | kpg.initialize(spec) |
| 175 | kpg.generateKeyPair() |
| 176 | ``` |
| 177 | |
| 178 | ### Qualcomm QSEE (Trusted App sketch) |
| 179 | |
| 180 | ```c |
| 181 | // Signed TA running inside QSEE; the Python SDK talks to it via QSEECom. |
| 182 | int pqc_enclave_ta_store_session(uint8_t *session_key, uint32_t len) { |
| 183 | sealed_key_t sealed; |
| 184 | ta_kek_wrap(g_ta_kek, session_key, len, &sealed); |
| 185 | return qseecom_write_sealed_blob(&sealed); // persists to Keystore |
| 186 | } |
| 187 | ``` |
| 188 | |
| 189 | ## API Reference |
| 190 | |
| 191 | ### `EnclaveVault` |
| 192 | |
| 193 | | Method | Description | |
| 194 | |---|---| |
| 195 | | `unlock(ttl_seconds=3600)` | Derive a session key via ML-KEM-768 and mark the vault usable. | |
| 196 | | `lock()` | Wipe the session key from memory. | |
| 197 | | `put_artifact(name, kind, content, ...)` | AES-256-GCM encrypt and store. Returns the `EncryptedArtifact`. | |
| 198 | | `get_artifact(name_or_id)` | Decrypt and return `EnclaveArtifact` (metadata + plaintext). | |
| 199 | | `delete_artifact(name_or_id)` | Remove by name or id. | |
| 200 | | `list_artifacts()` | List `ArtifactMetadata` for everything in the vault. | |
| 201 | | `save()` | Persist the encrypted store to the backend. | |
| 202 | | `is_unlocked` | Property; also re-checks session expiry. | |
| 203 | |
| 204 | ### `EnclaveArtifact` |
| 205 | |
| 206 | | Field / Method | Description | |
| 207 | |---|---| |
| 208 | | `metadata` | `ArtifactMetadata` frozen dataclass. | |
| 209 | | `content` | Plaintext bytes. | |
| 210 | | `sha3_256_hex()` | SHA3-256 of the content, hex. | |
| 211 | | `content_hash(bytes)` (static) | SHA3-256 helper. | |
| 212 | |
| 213 | ### `AccessPolicy` / `ArtifactPolicy` |
| 214 | |
| 215 | | Method | Description | |
| 216 | |---|---| |
| 217 | | `AccessPolicy().add(rule)` | Attach a rule for an `ArtifactKind`. | |
| 218 | | `.check(metadata, caller_bundle_id)` | Raises `PolicyViolationError` on deny. | |
| 219 | | `ArtifactPolicy(kind, allowed_bundle_ids, require_biometric, max_uses_per_hour)` | Per-kind rule. | |
| 220 | |
| 221 | ### `DeviceAttester` |
| 222 | |
| 223 | | Method | Description | |
| 224 | |---|---| |
| 225 | | `DeviceAttester(identity, device_id, device_model, enclave_vendor)` | Bind an `AgentIdentity` to a device. | |
| 226 | | `.attest(artifact_id, content_hash)` | Produce a signed `DeviceAttestation`. | |
| 227 | | `DeviceAttester.verify(att)` (static) | Returns True / False. | |
| 228 | | `DeviceAttester.verify_or_raise(att)` (static) | Raises `AttestationError` on invalid. | |
| 229 | |
| 230 | ### Exceptions |
| 231 | |
| 232 | | Exception | When | |
| 233 | |---|---| |
| 234 | | `EnclaveSDKError` | Base class. | |
| 235 | | `UnknownArtifactError` | `get_artifact` / `delete_artifact` against a missing id or name. | |
| 236 | | `EnclaveLockedError` | Operation attempted on a locked vault. | |
| 237 | | `DecryptionError` | AES-GCM tag rejected ciphertext or AAD. | |
| 238 | | `BackendError` | iOS / Android / QSEE backend refused or is stubbed. | |
| 239 | | `AttestationError` | `DeviceAttester.verify_or_raise` saw an invalid signature. | |
| 240 | | `PolicyViolationError` | `AccessPolicy.check` denied the caller. | |
| 241 | |
| 242 | ## Why PQC for On-Device AI |
| 243 | |
| 244 | On-device model weights live on a user's phone for **five or more years** - longer than any reasonable cryptanalytic lead time against classical RSA/ECDSA. Proprietary fine-tunes, biometric templates, and OAuth refresh tokens embedded in those artifacts are exactly the kind of data a patient adversary will harvest now to decrypt later. |
| 245 | |
| 246 | This is the HNDL threat model at its most concrete: the ciphertext blob is already on the user's device, already in cloud backups, and already syncing through MDM pipes. Every one of those copies is at risk the instant a CRQC arrives. ML-KEM-768 and AES-256-GCM close that window today - no platform-vendor timeline dependency, no waiting for iOS 19 or Android 16 to ship their post-quantum Keystore updates. |
| 247 | |
| 248 | ## Examples |
| 249 | |
| 250 | See the `examples/` directory: |
| 251 | |
| 252 | - **`store_model_weights.py`** - 256 KB model weight lifecycle through an in-memory vault. |
| 253 | - **`store_credentials.py`** - three API credentials across three different app bundles. |
| 254 | - **`device_attestation.py`** - sign and verify a DeviceAttestation, and show the tamper case. |
| 255 | |
| 256 | Run them: |
| 257 | |
| 258 | ```bash |
| 259 | python examples/store_model_weights.py |
| 260 | python examples/store_credentials.py |
| 261 | python examples/device_attestation.py |
| 262 | ``` |
| 263 | |
| 264 | ## Development |
| 265 | |
| 266 | ```bash |
| 267 | pip install -e ".[dev]" |
| 268 | pytest |
| 269 | ruff check src/ tests/ examples/ |
| 270 | ``` |
| 271 | |
| 272 | ## Related |
| 273 | |
| 274 | Part of the [QuantaMrkt](https://quantamrkt.com) post-quantum tooling registry. See also: |
| 275 | |
| 276 | - **QuantumShield** - the underlying PQC toolkit (`AgentIdentity`, `SignatureAlgorithm`, `generate_kem_keypair`, `sign / verify`). |
| 277 | - **PQC Agent Wallet** - sister tool for passphrase-unlocked credential vaults. |
| 278 | - **PQC GPU Driver** - sister tool for keeping tensors encrypted on discrete accelerators. |
| 279 | - **PQC Hypervisor Attestation** - sister tool for confidential-VM memory attestation. |
| 280 | |
| 281 | ## License |
| 282 | |
| 283 | Apache License 2.0. See [LICENSE](LICENSE). |
| 284 | |