src/pqc_enclave_sdk/backends/ios.py
| 1 | """iOS Secure Enclave backend (stub interface). |
| 2 | |
| 3 | Real integration uses Apple's CryptoKit + Keychain Services. This stub |
| 4 | documents the expected shape; app developers plug in their Swift / ObjC |
| 5 | bridge layer. |
| 6 | |
| 7 | A production implementation of this backend is expected to: |
| 8 | |
| 9 | * Generate a hardware-backed key in the Secure Enclave with |
| 10 | ``SecureEnclave.P256.KeyAgreement.PrivateKey`` (CryptoKit). The private |
| 11 | key is non-extractable and bound to the device's SEP. |
| 12 | * For :meth:`store_session_key`, wrap the 32-byte AES-GCM session key with |
| 13 | the SEP key via ``HPKE`` or ``AES.GCM.seal`` and persist the wrapped blob |
| 14 | to the Keychain with ``kSecAttrTokenIDSecureEnclave`` and access-control |
| 15 | flags ``kSecAccessControlBiometryCurrentSet | kSecAccessControlPrivateKeyUsage``. |
| 16 | * For :meth:`load_session_key`, query the Keychain for the wrapped key and |
| 17 | unwrap it inside the SEP - the plaintext symmetric key never leaves |
| 18 | enclave memory. |
| 19 | * For :meth:`save_artifacts` and :meth:`load_artifacts`, serialize the |
| 20 | encrypted artifact store to a file in the app's Data Protection |
| 21 | ``NSFileProtectionCompleteUntilFirstUserAuthentication`` container. |
| 22 | * Optionally attach a DeviceCheck / App Attest token to prove the binary |
| 23 | is genuine and the device has not been jailbroken. |
| 24 | |
| 25 | ML-KEM-768 support on iOS is arriving via Apple's PQ3 iMessage stack and |
| 26 | CryptoKit post-quantum primitives - plug that in where ``generate_kem_keypair`` |
| 27 | currently runs in-process. |
| 28 | """ |
| 29 | |
| 30 | from __future__ import annotations |
| 31 | |
| 32 | from pqc_enclave_sdk.artifact import EncryptedArtifact |
| 33 | from pqc_enclave_sdk.backends.base import EnclaveBackend |
| 34 | from pqc_enclave_sdk.errors import BackendError |
| 35 | |
| 36 | |
| 37 | class iOSEnclaveBackend(EnclaveBackend): |
| 38 | """Stub Apple Secure Enclave backend. |
| 39 | |
| 40 | Raises :class:`BackendError` when invoked without real SEP wiring. |
| 41 | """ |
| 42 | |
| 43 | name = "ios-secure-enclave" |
| 44 | platform = "ios" |
| 45 | enclave_vendor = "apple-se" |
| 46 | |
| 47 | def __init__( |
| 48 | self, |
| 49 | device_id: str = "iphone-unknown", |
| 50 | device_model: str = "iphone-unknown", |
| 51 | keychain_service: str = "com.dyber.pqc.enclave", |
| 52 | ) -> None: |
| 53 | self.device_id = device_id |
| 54 | self.device_model = device_model |
| 55 | self.keychain_service = keychain_service |
| 56 | |
| 57 | def store_session_key(self, key_id: str, key: bytes, expires_at: str) -> None: |
| 58 | raise BackendError( |
| 59 | "iOSEnclaveBackend.store_session_key is a stub. A real implementation " |
| 60 | f"wraps the 32-byte key with a SecureEnclave.P256 key and writes the " |
| 61 | f"wrapped blob to the Keychain service {self.keychain_service!r} with " |
| 62 | "kSecAttrTokenIDSecureEnclave + access-control flags." |
| 63 | ) |
| 64 | |
| 65 | def load_session_key(self, key_id: str) -> bytes | None: |
| 66 | raise BackendError( |
| 67 | "iOSEnclaveBackend.load_session_key is a stub. A real implementation " |
| 68 | f"queries the Keychain service {self.keychain_service!r} for key " |
| 69 | f"{key_id} and unwraps the blob inside the SEP." |
| 70 | ) |
| 71 | |
| 72 | def save_artifacts(self, artifacts: dict[str, EncryptedArtifact]) -> None: |
| 73 | raise BackendError( |
| 74 | "iOSEnclaveBackend.save_artifacts is a stub. A real implementation " |
| 75 | "serializes the encrypted artifacts to a file under " |
| 76 | "NSFileProtectionCompleteUntilFirstUserAuthentication in the app's " |
| 77 | "Data Protection container." |
| 78 | ) |
| 79 | |
| 80 | def load_artifacts(self) -> dict[str, EncryptedArtifact]: |
| 81 | raise BackendError( |
| 82 | "iOSEnclaveBackend.load_artifacts is a stub. A real implementation " |
| 83 | "reads the encrypted artifact file back from the Data Protection " |
| 84 | "container and deserializes it via EncryptedArtifact.from_dict." |
| 85 | ) |
| 86 | |