PQC Secure Enclave SDK for on-device AI. ML-KEM-768 key encapsulation + AES-256-GCM encrypted model weights and credentials, pluggable backends for iOS Secure Enclave, Android StrongBox, Qualcomm QSEE.
Project description
PQC Secure Enclave SDK
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.
The Problem
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.
This is the HNDL threat model (Harvest Now, Decrypt Later) applied to on-device AI:
- An attacker who exfiltrates encrypted weight files today - from backups, compromised cloud sync, supply-chain tooling, or forensic device imaging - can store them indefinitely.
- When a cryptographically relevant quantum computer arrives, every RSA/ECDSA-wrapped symmetric key is retroactively broken and the plaintext weights fall out.
- For proprietary fine-tunes, biometric templates, and long-lived OAuth refresh tokens, "eventually decrypted" is functionally equivalent to "decrypted".
The Solution
Wrap every on-device AI artifact in a PQC-protected envelope:
- ML-KEM-768 (FIPS 203, NIST PQC) for the session key that the enclave unwraps.
- AES-256-GCM (FIPS 197) for the artifact body. Key is 32 bytes, nonce 12 bytes, tag 16 bytes.
- SHA3-256 content hash authenticated via AES-GCM AAD - any metadata tampering breaks decryption.
- ML-DSA (FIPS 204) signatures for device attestations that commit to what was stored.
- Pluggable backends:
iOSEnclaveBackend,AndroidEnclaveBackend,QSEEBackend, plusInMemoryEnclaveBackendfor tests.
Installation
pip install pqc-enclave-sdk
Development:
pip install -e ".[dev]"
Quick Start
from pqc_enclave_sdk import (
ArtifactKind,
EnclaveVault,
InMemoryEnclaveBackend,
)
backend = InMemoryEnclaveBackend(device_id="iphone-alice", device_model="iphone-15-pro")
vault = EnclaveVault(backend=backend)
vault.unlock()
vault.put_artifact(
name="llama-3.2-1b-int4",
kind=ArtifactKind.MODEL_WEIGHTS,
content=weights_bytes,
version="1.0.0",
app_bundle_id="com.example.localllm",
)
vault.save()
vault.lock()
# Later, in the same process or another app launch:
vault.unlock()
weights = vault.get_artifact("llama-3.2-1b-int4").content
Architecture
Your App EnclaveVault EnclaveBackend Device Secure Enclave
-------- ------------ -------------- ---------------------
| | | |
| put_artifact(bytes) | | |
| ---------------------> | | |
| | 1. derive session key | |
| | via ML-KEM-768 | |
| | 2. AES-256-GCM encrypt | |
| | with content-hash AAD| |
| | 3. store_session_key ------------------------------>|
| | | wraps w/ hardware KEK |
| | 4. save_artifacts | |
| | ----------------------> | |
| | | persists ciphertext |
| | | to Keychain/Keystore |
| | | |
| get_artifact(name) | | |
| ---------------------> | | |
| | 5. load_session_key --------------------------------|
| | (unwrap inside SEP) |
| | 6. AES-256-GCM decrypt | |
| <--- plaintext | | |
Artifact Kinds
| Kind | Purpose |
|---|---|
MODEL_WEIGHTS |
Full model weight tensors (INT4 / INT8 / FP16 on-device checkpoints). |
LORA_ADAPTER |
Low-rank fine-tune adapters; smaller but sensitive for proprietary tunes. |
TOKENIZER |
Tokenizer vocab + merges; lower-sensitivity but integrity-critical. |
CREDENTIAL |
API keys, OAuth tokens, auth bearer tokens. |
BIOMETRIC_TEMPLATE |
Encoded face / fingerprint templates. Highest sensitivity. |
INFERENCE_CACHE |
KV-cache blobs from prior conversations. |
SAFETY_MODEL |
Jailbreak classifier / content-safety adapter. |
OTHER |
Everything else. |
Cryptography
| Primitive | Role | Standard |
|---|---|---|
| ML-KEM-768 | Session-key encapsulation to the enclave's PQC public key | FIPS 203 |
| AES-256-GCM | Symmetric encryption of every artifact body | FIPS 197 / SP 800-38D |
| SHA3-256 | Content hash + canonical AAD hashing | FIPS 202 |
| ML-DSA-65 / 87 | Signatures over DeviceAttestations | FIPS 204 |
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.
Threat Model
| Threat | Mitigation |
|---|---|
| Device theft (attacker has the phone) | Symmetric key never leaves the enclave. Access control requires biometrics / device unlock. |
| 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). |
| Rogue app reading another app's artifacts | AccessPolicy.allowed_bundle_ids filters callers; OS Keychain / Keystore access-control flags enforce at the kernel level. |
| Stale session key (long-lived re-use) | DEFAULT_SESSION_TTL = 3600; is_unlocked re-checks expiration on every call. |
| Post-quantum forgery of attestation | DeviceAttester signs with ML-DSA, not ECDSA. |
| 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. |
| Downgrade to classical crypto | Algorithm is baked into the AAD; a rewrite requires access to the PQC session key. |
Backend Integration Guides
iOS Secure Enclave (CryptoKit sketch)
import CryptoKit
// 1. Generate a non-extractable SEP key at app install.
let sepKey = try SecureEnclave.P256.KeyAgreement.PrivateKey(
accessControl: SecAccessControlCreateWithFlags(
nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet], nil)!
)
// 2. On unlock, receive the 32-byte AES-GCM key from the Python SDK
// (ideally via an ML-KEM-768 ciphertext the SEP decapsulates). Wrap it
// with the SEP key and write the sealed blob to the Keychain:
let sealedBox = try AES.GCM.seal(sessionKey, using: sepSymmetricKey)
SecItemAdd([
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.dyber.pqc.enclave",
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
kSecValueData: sealedBox.combined!,
] as CFDictionary, nil)
Android StrongBox (Kotlin sketch)
val spec = KeyGenParameterSpec.Builder(
"com.dyber.pqc.enclave.session",
KeyProperties.PURPOSE_WRAP_KEY or KeyProperties.PURPOSE_ENCRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setIsStrongBoxBacked(true) // Titan M / Knox Vault
.setUserAuthenticationRequired(true)
.setUnlockedDeviceRequired(true)
.build()
val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
kpg.initialize(spec)
kpg.generateKeyPair()
Qualcomm QSEE (Trusted App sketch)
// Signed TA running inside QSEE; the Python SDK talks to it via QSEECom.
int pqc_enclave_ta_store_session(uint8_t *session_key, uint32_t len) {
sealed_key_t sealed;
ta_kek_wrap(g_ta_kek, session_key, len, &sealed);
return qseecom_write_sealed_blob(&sealed); // persists to Keystore
}
API Reference
EnclaveVault
| Method | Description |
|---|---|
unlock(ttl_seconds=3600) |
Derive a session key via ML-KEM-768 and mark the vault usable. |
lock() |
Wipe the session key from memory. |
put_artifact(name, kind, content, ...) |
AES-256-GCM encrypt and store. Returns the EncryptedArtifact. |
get_artifact(name_or_id) |
Decrypt and return EnclaveArtifact (metadata + plaintext). |
delete_artifact(name_or_id) |
Remove by name or id. |
list_artifacts() |
List ArtifactMetadata for everything in the vault. |
save() |
Persist the encrypted store to the backend. |
is_unlocked |
Property; also re-checks session expiry. |
EnclaveArtifact
| Field / Method | Description |
|---|---|
metadata |
ArtifactMetadata frozen dataclass. |
content |
Plaintext bytes. |
sha3_256_hex() |
SHA3-256 of the content, hex. |
content_hash(bytes) (static) |
SHA3-256 helper. |
AccessPolicy / ArtifactPolicy
| Method | Description |
|---|---|
AccessPolicy().add(rule) |
Attach a rule for an ArtifactKind. |
.check(metadata, caller_bundle_id) |
Raises PolicyViolationError on deny. |
ArtifactPolicy(kind, allowed_bundle_ids, require_biometric, max_uses_per_hour) |
Per-kind rule. |
DeviceAttester
| Method | Description |
|---|---|
DeviceAttester(identity, device_id, device_model, enclave_vendor) |
Bind an AgentIdentity to a device. |
.attest(artifact_id, content_hash) |
Produce a signed DeviceAttestation. |
DeviceAttester.verify(att) (static) |
Returns True / False. |
DeviceAttester.verify_or_raise(att) (static) |
Raises AttestationError on invalid. |
Exceptions
| Exception | When |
|---|---|
EnclaveSDKError |
Base class. |
UnknownArtifactError |
get_artifact / delete_artifact against a missing id or name. |
EnclaveLockedError |
Operation attempted on a locked vault. |
DecryptionError |
AES-GCM tag rejected ciphertext or AAD. |
BackendError |
iOS / Android / QSEE backend refused or is stubbed. |
AttestationError |
DeviceAttester.verify_or_raise saw an invalid signature. |
PolicyViolationError |
AccessPolicy.check denied the caller. |
Why PQC for On-Device AI
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.
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.
Examples
See the examples/ directory:
store_model_weights.py- 256 KB model weight lifecycle through an in-memory vault.store_credentials.py- three API credentials across three different app bundles.device_attestation.py- sign and verify a DeviceAttestation, and show the tamper case.
Run them:
python examples/store_model_weights.py
python examples/store_credentials.py
python examples/device_attestation.py
Development
pip install -e ".[dev]"
pytest
ruff check src/ tests/ examples/
Related
Part of the QuantaMrkt post-quantum tooling registry. See also:
- QuantumShield - the underlying PQC toolkit (
AgentIdentity,SignatureAlgorithm,generate_kem_keypair,sign / verify). - PQC Agent Wallet - sister tool for passphrase-unlocked credential vaults.
- PQC GPU Driver - sister tool for keeping tensors encrypted on discrete accelerators.
- PQC Hypervisor Attestation - sister tool for confidential-VM memory attestation.
License
Apache License 2.0. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pqc_enclave_sdk-0.1.0.tar.gz.
File metadata
- Download URL: pqc_enclave_sdk-0.1.0.tar.gz
- Upload date:
- Size: 24.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cfa29b6802d7eaeaf29cc3997976f13d7c55bae56609483fa04f8c75fb200e2f
|
|
| MD5 |
c8a8b19c89ca32928fd40637b6ae88f3
|
|
| BLAKE2b-256 |
8aa16a6652a92decdb5d015c5bb2334efc69798da4ae9cccafff9f717d6888a6
|
File details
Details for the file pqc_enclave_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pqc_enclave_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 25.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d4f2501b7790d6c145459c1911ff7f1d991263c7621f55f5e9e0d1602dbe8f6
|
|
| MD5 |
78e994dd49ebdf70fe8d4cd0f61188d6
|
|
| BLAKE2b-256 |
37ca8fd0de6ec108f50dd663dc41f2ed4e1fceff37f3fbb40f48eefc3c2f262a
|