src/pqc_enclave_sdk/backends/android.py
| 1 | """Android StrongBox + Keystore backend (stub interface). |
| 2 | |
| 3 | Real integration uses the AndroidKeyStore provider with StrongBox where |
| 4 | available (Pixel Titan M, Samsung Knox Vault, etc.). This stub documents |
| 5 | the expected shape; app developers plug in their Kotlin / JNI bridge. |
| 6 | |
| 7 | A production implementation of this backend is expected to: |
| 8 | |
| 9 | * Request a hardware-backed key via ``KeyGenParameterSpec.Builder`` with |
| 10 | ``setIsStrongBoxBacked(true)`` so the private key material lives in the |
| 11 | dedicated StrongBox secure element, not the TEE. |
| 12 | * For :meth:`store_session_key`, wrap the 32-byte AES-GCM session key with |
| 13 | the StrongBox key (``Cipher.WRAP_MODE`` over AES-GCM) and persist the |
| 14 | wrapped bytes to EncryptedSharedPreferences or the app's private files |
| 15 | directory. |
| 16 | * For :meth:`load_session_key`, unwrap the blob inside StrongBox via |
| 17 | ``Cipher.UNWRAP_MODE`` - the plaintext symmetric key never leaves the |
| 18 | secure element. |
| 19 | * Set ``setUserAuthenticationRequired(true)`` and ``setUnlockedDeviceRequired(true)`` |
| 20 | to require biometric or device credential before the key is usable. |
| 21 | * For :meth:`save_artifacts` and :meth:`load_artifacts`, serialize the |
| 22 | encrypted artifact store to a file under ``Context.filesDir`` and wrap |
| 23 | reads/writes in EncryptedFile from androidx.security. |
| 24 | |
| 25 | ML-KEM-768 is available in BoringSSL and is being integrated into |
| 26 | AndroidKeyStore - plug that in where ``generate_kem_keypair`` currently |
| 27 | 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 AndroidEnclaveBackend(EnclaveBackend): |
| 38 | """Stub Android StrongBox / Keystore backend. |
| 39 | |
| 40 | Raises :class:`BackendError` when invoked without real Keystore wiring. |
| 41 | """ |
| 42 | |
| 43 | name = "android-strongbox" |
| 44 | platform = "android" |
| 45 | enclave_vendor = "android-strongbox" |
| 46 | |
| 47 | KEY_STORE_PROVIDER = "AndroidKeyStore" |
| 48 | |
| 49 | def __init__( |
| 50 | self, |
| 51 | device_id: str = "android-unknown", |
| 52 | device_model: str = "android-unknown", |
| 53 | keystore_alias: str = "com.dyber.pqc.enclave.session", |
| 54 | ) -> None: |
| 55 | self.device_id = device_id |
| 56 | self.device_model = device_model |
| 57 | self.keystore_alias = keystore_alias |
| 58 | |
| 59 | def store_session_key(self, key_id: str, key: bytes, expires_at: str) -> None: |
| 60 | raise BackendError( |
| 61 | "AndroidEnclaveBackend.store_session_key is a stub. A real " |
| 62 | f"implementation wraps the 32-byte key with the {self.keystore_alias!r} " |
| 63 | f"key in the {self.KEY_STORE_PROVIDER} provider (StrongBox-backed) " |
| 64 | "and persists the wrapped blob." |
| 65 | ) |
| 66 | |
| 67 | def load_session_key(self, key_id: str) -> bytes | None: |
| 68 | raise BackendError( |
| 69 | "AndroidEnclaveBackend.load_session_key is a stub. A real " |
| 70 | f"implementation unwraps the blob via Cipher.UNWRAP_MODE on the " |
| 71 | f"{self.keystore_alias!r} StrongBox key for key_id {key_id}." |
| 72 | ) |
| 73 | |
| 74 | def save_artifacts(self, artifacts: dict[str, EncryptedArtifact]) -> None: |
| 75 | raise BackendError( |
| 76 | "AndroidEnclaveBackend.save_artifacts is a stub. A real implementation " |
| 77 | "persists the encrypted artifacts via androidx.security.EncryptedFile " |
| 78 | "under Context.filesDir." |
| 79 | ) |
| 80 | |
| 81 | def load_artifacts(self) -> dict[str, EncryptedArtifact]: |
| 82 | raise BackendError( |
| 83 | "AndroidEnclaveBackend.load_artifacts is a stub. A real implementation " |
| 84 | "reads the EncryptedFile back and deserializes it via " |
| 85 | "EncryptedArtifact.from_dict." |
| 86 | ) |
| 87 | |