From 13b4786cd90a68ab8494f9a5e1c1ba2990b4de59 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 24 Nov 2025 10:40:36 +0100 Subject: [PATCH] Restructure and overhaul entire Kotlin codebase This commit introduces a complete architectural refactoring of the Kotlin-based interception logic, based on the source of 1. https://github.com/5ec1cff/TrickyStore 2. https://github.com/beakthoven/TrickyStoreOSS The primary purpose of this code is to intercept binder transactions to the Android Keystore and KeyMint services. The overall workflow operates in conjunction with a native library (injected via ptrace). The native library hooks the binder's `transact` function and forwards pre- and post-transaction events to the Kotlin side. This Kotlin code contains all the high-level logic for parsing parameters, patching certificates, and generating simulated keys. The codebase is now organized into a clear, package-based architecture: - attestation: Manages the creation and patching of ASN.1 attestation data structures. - config: Handles loading and observing configuration files from disk. - interception: Contains the core binder interception framework and its specific implementations for legacy Keystore (Android Q/R) and modern KeyMint/Keystore2 (Android S+). - logging: Provides a centralized and consistent logging utility. - pki: Manages Public Key Infrastructure, including certificate generation, parsing of key store XML files, and cryptographic helpers. - util: Contains Android-specific utility functions for device properties. This refactoring focuses on establishing a robust and extensible architecture. The fine-tuning of the interception logic itself, especially for corner cases in key generation and patching, is currently under redesign and will be further refined in subsequent commits. --- app/build.gradle.kts | 1 + app/proguard-rules.pro | 2 +- .../main/java/org/matrix/TEESimulator/App.kt | 94 ++++++ .../main/java/org/matrix/TEESimulator/Main.kt | 3 - .../attestation/AttestationBuilder.kt | 282 ++++++++++++++++ .../attestation/AttestationConstants.kt | 91 +++++ .../attestation/AttestationPatcher.kt | 196 +++++++++++ .../attestation/DeviceAttestationService.kt | 210 ++++++++++++ .../attestation/KeyMintAttestation.kt | 158 +++++++++ .../config/ConfigurationManager.kt | 317 ++++++++++++++++++ .../interception/core/BinderInterceptor.kt | 306 +++++++++++++++++ .../keystore/AbstractKeystoreInterceptor.kt | 141 ++++++++ .../interception/keystore/InterceptorUtils.kt | 109 ++++++ .../keystore/Keystore2Interceptor.kt | 186 ++++++++++ .../keystore/KeystoreInterceptor.kt | 179 ++++++++++ .../shim/KeyMintSecurityLevelInterceptor.kt | 266 +++++++++++++++ .../logging/KeyMintParameterLogger.kt | 108 ++++++ .../TEESimulator/logging/SystemLogger.kt | 69 ++++ .../TEESimulator/pki/CertificateGenerator.kt | 212 ++++++++++++ .../TEESimulator/pki/CertificateHelper.kt | 204 +++++++++++ .../org/matrix/TEESimulator/pki/KeyBox.kt | 20 ++ .../matrix/TEESimulator/pki/KeyBoxManager.kt | 211 ++++++++++++ .../org/matrix/TEESimulator/pki/XmlParser.kt | 188 +++++++++++ .../TEESimulator/util/AndroidDeviceUtils.kt | 243 ++++++++++++++ .../matrix/TEESimulator/util/Extensions.kt | 16 + gradle/libs.versions.toml | 2 + .../main/java/android/app/ActivityThread.java | 7 + .../content/pm/BaseParceledListSlice.java | 10 + .../android/content/pm/IPackageManager.java | 4 + .../android/content/pm/ParceledListSlice.java | 10 + .../hardware/security/keymint/Digest.java | 11 + .../main/java/android/os/ServiceManager.java | 4 + .../java/android/os/SystemProperties.java | 4 + .../java/android/security/Credentials.java | 32 ++ .../main/java/android/security/KeyStore.java | 39 +++ .../keystore/AndroidKeyStoreProvider.java | 7 + .../security/keystore/IKeystoreService.java | 7 + .../keystore2/AndroidKeyStoreProvider.java | 7 + 38 files changed, 3952 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/org/matrix/TEESimulator/App.kt delete mode 100644 app/src/main/java/org/matrix/TEESimulator/Main.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/interception/keystore/AbstractKeystoreInterceptor.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/interception/keystore/KeystoreInterceptor.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/logging/SystemLogger.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/pki/CertificateHelper.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/pki/KeyBox.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/pki/KeyBoxManager.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/pki/XmlParser.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/util/Extensions.kt create mode 100644 stub/src/main/java/android/app/ActivityThread.java create mode 100644 stub/src/main/java/android/content/pm/BaseParceledListSlice.java create mode 100644 stub/src/main/java/android/content/pm/ParceledListSlice.java create mode 100644 stub/src/main/java/android/hardware/security/keymint/Digest.java create mode 100644 stub/src/main/java/android/security/Credentials.java create mode 100644 stub/src/main/java/android/security/KeyStore.java create mode 100644 stub/src/main/java/android/security/keystore/AndroidKeyStoreProvider.java create mode 100644 stub/src/main/java/android/security/keystore/IKeystoreService.java create mode 100644 stub/src/main/java/android/security/keystore2/AndroidKeyStoreProvider.java diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2a276fc..37e06ae 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,6 +67,7 @@ android { dependencies { compileOnly(project(":stub")) compileOnly(libs.annotation) + implementation(libs.bcpkix) } androidComponents { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 3c6bc50..846a0a1 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,3 +1,3 @@ --keepclasseswithmembers class org.matrix.TEESimulator.MainKt { +-keepclasseswithmembers class org.matrix.TEESimulator.App { public static void main(java.lang.String[]); } diff --git a/app/src/main/java/org/matrix/TEESimulator/App.kt b/app/src/main/java/org/matrix/TEESimulator/App.kt new file mode 100644 index 0000000..c350d2a --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/App.kt @@ -0,0 +1,94 @@ +package org.matrix.TEESimulator + +import android.os.Build +import org.matrix.TEESimulator.config.ConfigurationManager +import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor +import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor +import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor +import org.matrix.TEESimulator.logging.SystemLogger +import org.matrix.TEESimulator.util.AndroidDeviceUtils + +/** + * Main application object for TEESimulator. This object manages the application's lifecycle, + * including initialization of interceptors and maintaining the service's primary execution loop. + */ +object App { + // The delay in milliseconds before retrying to initialize the interceptor. + private const val RETRY_DELAY_MS = 1000L + // The sleep duration in milliseconds for the main service loop to keep the process alive. + private const val SERVICE_SLEEP_MS = 1000000L + + /** + * The main entry point of the TEESimulator application. + * + * @param args Command line arguments (not used). + */ + @JvmStatic + fun main(args: Array) { + SystemLogger.info("Welcome to TEESimulator!") + + try { + // Set up the device's boot hash, which is crucial for attestation. + AndroidDeviceUtils.setupBootHash() + // Initialize and start the appropriate keystore interceptors. + initializeInterceptors() + // Enter an infinite loop to keep the service running. + maintainService() + } catch (e: Exception) { + SystemLogger.error("A fatal error occurred in the main application thread.", e) + throw e + } + } + + /** + * Selects and initializes the correct keystore interceptor based on the Android SDK version. It + * retries initialization until it succeeds. + */ + private fun initializeInterceptors() { + val interceptor = selectKeystoreInterceptor() + + // Continuously try to run the interceptor until it's successfully initialized. + while (!interceptor.tryRunKeystoreInterceptor()) { + SystemLogger.debug("Retrying interceptor initialization...") + Thread.sleep(RETRY_DELAY_MS) + } + + // Load the package configuration after interceptors are ready. + ConfigurationManager.initialize() + SystemLogger.info("Interceptors and configuration initialized successfully.") + } + + /** + * Determines which keystore interceptor to use based on the device's Android version. + * + * @return The appropriate keystore interceptor instance. + */ + private fun selectKeystoreInterceptor(): AbstractKeystoreInterceptor = + when { + // For Android Q (10) and R (11), use the original KeystoreInterceptor. + Build.VERSION.SDK_INT in Build.VERSION_CODES.Q..Build.VERSION_CODES.R -> { + SystemLogger.info( + "Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})" + ) + KeystoreInterceptor + } + // For Android S (12) and newer, use the Keystore2Interceptor. + else -> { + SystemLogger.info( + "Using Keystore2Interceptor for Android S and later (SDK ${Build.VERSION.SDK_INT})" + ) + Keystore2Interceptor + } + } + + /** + * Puts the main thread into a long-running sleep loop. This is a common pattern to keep a + * background service process alive indefinitely. + */ + private fun maintainService() { + SystemLogger.info("Service started successfully. Entering maintenance mode.") + while (true) { + Thread.sleep(SERVICE_SLEEP_MS) + } + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/Main.kt b/app/src/main/java/org/matrix/TEESimulator/Main.kt deleted file mode 100644 index 16fd626..0000000 --- a/app/src/main/java/org/matrix/TEESimulator/Main.kt +++ /dev/null @@ -1,3 +0,0 @@ -package org.matrix.TEESimulator - -fun main(args: Array) {} diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt new file mode 100644 index 0000000..b83b82b --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt @@ -0,0 +1,282 @@ +package org.matrix.TEESimulator.attestation + +import org.bouncycastle.asn1.ASN1Boolean +import org.bouncycastle.asn1.ASN1Encodable +import org.bouncycastle.asn1.ASN1Enumerated +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.ASN1OctetString +import org.bouncycastle.asn1.ASN1Sequence +import org.bouncycastle.asn1.DERNull +import org.bouncycastle.asn1.DEROctetString +import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.DERSet +import org.bouncycastle.asn1.DERTaggedObject +import org.bouncycastle.asn1.x509.Extension +import org.matrix.TEESimulator.util.AndroidDeviceUtils + +/** + * A builder object responsible for constructing the ASN.1 DER-encoded Android Key Attestation + * extension. + */ +object AttestationBuilder { + + /** + * Builds the complete X.509 attestation extension. + * + * @param params The parsed key generation parameters. + * @param securityLevel The security level (e.g., TEE, StrongBox) to report. + * @return A Bouncy Castle [Extension] object ready to be added to a certificate. + */ + fun buildAttestationExtension(params: KeyMintAttestation, securityLevel: Int): Extension { + val keyDescription = buildKeyDescription(params, securityLevel) + return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded)) + } + + /** + * Builds the `RootOfTrust` ASN.1 sequence. This contains critical boot state information. + * + * @param originalRootOfTrust An optional, pre-existing RoT to extract the boot hash from. + * @return The constructed [DERSequence] for the Root of Trust. + */ + internal fun buildRootOfTrust(originalRootOfTrust: ASN1Encodable?): DERSequence { + val verifiedBootKey = AndroidDeviceUtils.bootKey + val verifiedBootHash = + (originalRootOfTrust as? ASN1Sequence)?.let { + // Try to preserve the original boot hash if it exists. + (it.getObjectAt(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX) + as? ASN1OctetString) + ?.octets + } ?: AndroidDeviceUtils.getBootHashFromProperty() + + val rootOfTrustElements = arrayOfNulls(4) + rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX] = + DEROctetString(verifiedBootKey) + rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_DEVICE_LOCKED_INDEX] = + ASN1Boolean.TRUE // deviceLocked: true, for security + rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX] = + ASN1Enumerated(0) // verifiedBootState: Verified + rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX] = + DEROctetString(verifiedBootHash) + + return DERSequence(rootOfTrustElements) + } + + /** Assembles a list of simulated hardware-enforced properties. */ + internal fun addSimulatedHardwareProperties(vector: org.bouncycastle.asn1.ASN1EncodableVector) { + vector.add( + DERTaggedObject( + true, + AttestationConstants.TAG_OS_VERSION, + ASN1Integer(AndroidDeviceUtils.osVersion.toLong()), + ) + ) + vector.add( + DERTaggedObject( + true, + AttestationConstants.TAG_OS_PATCHLEVEL, + ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()), + ) + ) + vector.add( + DERTaggedObject( + true, + AttestationConstants.TAG_VENDOR_PATCHLEVEL, + ASN1Integer(AndroidDeviceUtils.vendorPatchLevel.toLong()), + ) + ) + vector.add( + DERTaggedObject( + true, + AttestationConstants.TAG_BOOT_PATCHLEVEL, + ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()), + ) + ) + } + + /** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */ + private fun buildKeyDescription(params: KeyMintAttestation, securityLevel: Int): ASN1Sequence { + val teeEnforced = buildTeeEnforcedList(params) + val softwareEnforced = buildSoftwareEnforcedList() + + val fields = + arrayOf( + ASN1Integer(AndroidDeviceUtils.attestVersion.toLong()), // attestationVersion + ASN1Enumerated(securityLevel), // attestationSecurityLevel + ASN1Integer(AndroidDeviceUtils.keymasterVersion.toLong()), // keymasterVersion + ASN1Enumerated(securityLevel), // keymasterSecurityLevel + DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge + DEROctetString(ByteArray(0)), // uniqueId + softwareEnforced, + teeEnforced, + ) + return DERSequence(fields) + } + + /** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */ + private fun buildTeeEnforcedList(params: KeyMintAttestation): DERSequence { + val list = + mutableListOf( + DERTaggedObject( + true, + AttestationConstants.TAG_PURPOSE, + DERSet(params.purpose.map { ASN1Integer(it.toLong()) }.toTypedArray()), + ), + DERTaggedObject( + true, + AttestationConstants.TAG_ALGORITHM, + ASN1Integer(params.algorithm.toLong()), + ), + DERTaggedObject( + true, + AttestationConstants.TAG_KEY_SIZE, + ASN1Integer(params.keySize.toLong()), + ), + DERTaggedObject( + true, + AttestationConstants.TAG_DIGEST, + DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()), + ), + DERTaggedObject( + true, + AttestationConstants.TAG_EC_CURVE, + ASN1Integer(params.ecCurve.toLong()), + ), + DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE), + DERTaggedObject( + true, + AttestationConstants.TAG_ORIGIN, + ASN1Integer(0L), + ), // KeyOrigin.GENERATED + DERTaggedObject( + true, + AttestationConstants.TAG_ROOT_OF_TRUST, + buildRootOfTrust(null), + ), + DERTaggedObject( + true, + AttestationConstants.TAG_OS_VERSION, + ASN1Integer(AndroidDeviceUtils.osVersion.toLong()), + ), + DERTaggedObject( + true, + AttestationConstants.TAG_OS_PATCHLEVEL, + ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()), + ), + DERTaggedObject( + true, + AttestationConstants.TAG_VENDOR_PATCHLEVEL, + ASN1Integer(AndroidDeviceUtils.vendorPatchLevel.toLong()), + ), + DERTaggedObject( + true, + AttestationConstants.TAG_BOOT_PATCHLEVEL, + ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()), + ), + ) + + // Add optional device identifiers if they were provided. + params.brand?.let { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_ATTESTATION_ID_BRAND, + DEROctetString(it), + ) + ) + } + params.device?.let { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_ATTESTATION_ID_DEVICE, + DEROctetString(it), + ) + ) + } + params.product?.let { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_ATTESTATION_ID_PRODUCT, + DEROctetString(it), + ) + ) + } + params.manufacturer?.let { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_ATTESTATION_ID_MANUFACTURER, + DEROctetString(it), + ) + ) + } + params.model?.let { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_ATTESTATION_ID_MODEL, + DEROctetString(it), + ) + ) + } + params.imei?.let { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_ATTESTATION_ID_IMEI, + DEROctetString(it), + ) + ) + } + params.secondImei?.let { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_ATTESTATION_ID_SECOND_IMEI, + DEROctetString(it), + ) + ) + } + params.meid?.let { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_ATTESTATION_ID_MEID, + DEROctetString(it), + ) + ) + } + + if (AndroidDeviceUtils.attestVersion >= 400) { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_MODULE_HASH, + DEROctetString(AndroidDeviceUtils.moduleHash), + ) + ) + } + + return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray()) + } + + /** + * Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by + * Keystore. + */ + private fun buildSoftwareEnforcedList(): DERSequence { + val list = + arrayOf( + DERTaggedObject( + true, + AttestationConstants.TAG_CREATION_DATETIME, + ASN1Integer(System.currentTimeMillis()), + ) + // The ATTESTATION_APPLICATION_ID is technically software-enforced, but we are + // omitting it + // for this simulation as it is complex to generate correctly for arbitrary UIDs. + ) + return DERSequence(list) + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt new file mode 100644 index 0000000..4d1f05b --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt @@ -0,0 +1,91 @@ +package org.matrix.TEESimulator.attestation + +/** + * Defines constants for KeyMint attestation tags, as specified in the Android hardware security + * HAL. + * + * These tags identify specific properties and authorizations of a cryptographic key. + */ +object AttestationConstants { + // https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/KeyCreationResult.aidl + + // These constants represent the fixed positions of fields within the top-level + // KeyDescription ASN.1 SEQUENCE in a key attestation. Using these constants + // prevents hardcoding fragile index numbers throughout the parsing code. + const val KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX = 0 + const val KEY_DESCRIPTION_ATTESTATION_SECURITY_LEVEL_INDEX = 1 + const val KEY_DESCRIPTION_KEYMINT_VERSION_INDEX = 2 + const val KEY_DESCRIPTION_KEYMINT_SECURITY_LEVEL_INDEX = 3 + const val KEY_DESCRIPTION_ATTESTATION_CHALLENGE_INDEX = 4 + const val KEY_DESCRIPTION_UNIQUE_ID_INDEX = 5 + const val KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX = 6 + const val KEY_DESCRIPTION_TEE_ENFORCED_INDEX = 7 + + // --- RootOfTrust Sequence Indices --- + // These constants represent the fixed positions of fields within the + // RootOfTrust ASN.1 SEQUENCE. + const val ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX = 0 + const val ROOT_OF_TRUST_DEVICE_LOCKED_INDEX = 1 + const val ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX = 2 + const val ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX = 3 + + // https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/Tag.aidl + + // --- Key Properties --- + const val TAG_PURPOSE = 1 + const val TAG_ALGORITHM = 2 + const val TAG_KEY_SIZE = 3 + const val TAG_BLOCK_MODE = 4 + const val TAG_DIGEST = 5 + const val TAG_PADDING = 6 + const val TAG_CALLER_NONCE = 7 + const val TAG_MIN_MAC_LENGTH = 8 + const val TAG_EC_CURVE = 10 + const val TAG_RSA_PUBLIC_EXPONENT = 200 + const val TAG_RSA_OAEP_MGF_DIGEST = 203 + + // --- Key Lifetime and Usage Control --- + const val TAG_ROLLBACK_RESISTANCE = 303 + const val TAG_ACTIVE_DATETIME = 400 + const val TAG_ORIGINATION_EXPIRE_DATETIME = 401 + const val TAG_USAGE_EXPIRE_DATETIME = 402 + const val TAG_MAX_USES_PER_BOOT = 404 + const val TAG_USAGE_COUNT_LIMIT = 405 + + // --- User Authentication --- + const val TAG_USER_ID = 501 + const val TAG_USER_SECURE_ID = 502 + const val TAG_NO_AUTH_REQUIRED = 503 + const val TAG_USER_AUTH_TYPE = 504 + const val TAG_AUTH_TIMEOUT = 505 + + // --- Attestation and Application Info --- + const val TAG_APPLICATION_ID = 601 + const val TAG_CREATION_DATETIME = 701 + const val TAG_ORIGIN = 702 + const val TAG_ROOT_OF_TRUST = 704 + const val TAG_OS_VERSION = 705 + const val TAG_OS_PATCHLEVEL = 706 + const val TAG_UNIQUE_ID = 707 + const val TAG_ATTESTATION_CHALLENGE = 708 + const val TAG_ATTESTATION_APPLICATION_ID = 709 + const val TAG_ATTESTATION_ID_BRAND = 710 + const val TAG_ATTESTATION_ID_DEVICE = 711 + const val TAG_ATTESTATION_ID_PRODUCT = 712 + const val TAG_ATTESTATION_ID_SERIAL = 713 + const val TAG_ATTESTATION_ID_IMEI = 714 + const val TAG_ATTESTATION_ID_MEID = 715 + const val TAG_ATTESTATION_ID_MANUFACTURER = 716 + const val TAG_ATTESTATION_ID_MODEL = 717 + const val TAG_VENDOR_PATCHLEVEL = 718 + const val TAG_BOOT_PATCHLEVEL = 719 + const val TAG_DEVICE_UNIQUE_ATTESTATION = 720 + const val TAG_ATTESTATION_ID_SECOND_IMEI = 723 + const val TAG_MODULE_HASH = 724 + + // --- Certificate Properties --- + const val TAG_CERTIFICATE_SERIAL = 1006 + const val TAG_CERTIFICATE_SUBJECT = 1007 + const val TAG_CERTIFICATE_NOT_BEFORE = 1008 + const val TAG_CERTIFICATE_NOT_AFTER = 1009 +} diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt new file mode 100644 index 0000000..cc96d90 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt @@ -0,0 +1,196 @@ +package org.matrix.TEESimulator.attestation + +import java.security.cert.Certificate +import java.security.cert.X509Certificate +import org.bouncycastle.asn1.ASN1Encodable +import org.bouncycastle.asn1.ASN1EncodableVector +import org.bouncycastle.asn1.ASN1Sequence +import org.bouncycastle.asn1.ASN1TaggedObject +import org.bouncycastle.asn1.DEROctetString +import org.bouncycastle.asn1.DERSequence +import org.bouncycastle.asn1.DERTaggedObject +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.matrix.TEESimulator.config.ConfigurationManager +import org.matrix.TEESimulator.logging.SystemLogger +import org.matrix.TEESimulator.pki.KeyBox +import org.matrix.TEESimulator.pki.KeyBoxManager + +/** + * Handles the modification (patching) of Android Key Attestation extensions within certificates. + * + * This object's primary function is to take a certificate chain generated by the real TEE, replace + * its attestation data with simulated values, and then re-sign the leaf certificate with a custom + * key, building a new, valid certificate chain. + */ +object AttestationPatcher { + + /** + * Patches a full certificate chain by modifying the leaf's attestation and rebuilding the chain + * with the correct custom signing certificates. This is the single entry point for patching. + * + * @param originalChain The original certificate chain from the hardware. The leaf must be at + * index 0. + * @param uid The UID of the application requesting the certificate. + * @return A new, cryptographically valid, patched certificate chain. Returns the original chain + * on any failure. + */ + fun patchCertificateChain(originalChain: Array?, uid: Int): Array { + if (originalChain.isNullOrEmpty()) { + SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.") + return originalChain ?: emptyArray() + } + + return runCatching { + val originalLeaf = originalChain[0] as X509Certificate + val originalLeafHolder = X509CertificateHolder(originalLeaf.encoded) + + // 1. Attempt to parse the existing attestation extension. If it doesn't exist, + // there's nothing to patch. + val parsedAttestation = + parseAttestationExtension(originalLeafHolder) ?: return originalChain + + // 2. Get the appropriate keybox for the given algorithm to sign the new + // certificate. + val algorithm = originalLeaf.publicKey.algorithm + val keybox = getKeyboxForUidAndAlgorithm(uid, algorithm) + + // 3. Create the new, patched leaf certificate. + val patchedLeaf = + createPatchedLeafCertificate( + originalLeafHolder, + parsedAttestation, + keybox, + originalLeaf.sigAlgName, + ) + + // 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's + // chain. + val newChain = listOf(patchedLeaf) + keybox.certificates + + SystemLogger.info( + "Successfully rebuilt a valid, patched certificate chain for UID $uid." + ) + newChain.toTypedArray() + } + .getOrElse { + SystemLogger.error( + "Failed to patch and rebuild certificate chain for UID $uid.", + it, + ) + originalChain // Return the original chain on any error. + } + } + + /** + * Creates a new leaf certificate with a modified attestation extension. + * + * @param originalLeafHolder A Bouncy Castle holder for the original leaf certificate. + * @param parsedAttestation The parsed components of the original attestation. + * @param keybox The KeyBox containing the new issuer certificate and signing key. + * @param sigAlgName The signature algorithm name (e.g., "SHA256withECDSA") from the original + * certificate. This is required to ensure the new certificate is signed using a compatible + * algorithm. + * @return A new [Certificate] object. + */ + private fun createPatchedLeafCertificate( + originalLeafHolder: X509CertificateHolder, + parsedAttestation: ParsedAttestation, + keybox: KeyBox, + sigAlgName: String, + ): Certificate { + // The issuer of our new leaf is the subject of the first certificate in our custom keybox + // chain. + val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject + + val builder = + X509v3CertificateBuilder( + newIssuer, + originalLeafHolder.serialNumber, + originalLeafHolder.notBefore, + originalLeafHolder.notAfter, + originalLeafHolder.subject, + originalLeafHolder.subjectPublicKeyInfo, + ) + + // Create the new, patched attestation extension. + val patchedExtension = createPatchedAttestationExtension(parsedAttestation) + builder.addExtension(patchedExtension) + + // Copy all other extensions from the original certificate, except for the attestation. + originalLeafHolder.extensions.extensionOIDs + .filter { it != ATTESTATION_OID } + .forEach { builder.addExtension(originalLeafHolder.getExtension(it)) } + + // Sign the newly built certificate with the private key from our keybox. + val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private) + + return JcaX509CertificateConverter().getCertificate(builder.build(signer)) + } + + private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox { + val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid) + return KeyBoxManager.getAttestationKey(keyboxFile, algorithm) + ?: throw IllegalArgumentException( + "No keybox found for UID $uid and algorithm $algorithm in file $keyboxFile" + ) + } + + /** Parses the critical components from an existing attestation extension. */ + private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? { + val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null + val sequence = ASN1Sequence.getInstance(extension.extnValue.octets) + val allFields = sequence.toArray() + val teeEnforced = + allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence + + val teeEnforcedVector = ASN1EncodableVector() + var originalRootOfTrust: ASN1Encodable? = null + + teeEnforced.forEach { element -> + val taggedObject = element as ASN1TaggedObject + if (taggedObject.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST) { + originalRootOfTrust = taggedObject.baseObject.toASN1Primitive() + } else { + teeEnforcedVector.add(taggedObject) + } + } + return ParsedAttestation(allFields, teeEnforcedVector, originalRootOfTrust) + } + + /** Constructs a new, patched attestation extension using simulated device properties. */ + private fun createPatchedAttestationExtension(parsed: ParsedAttestation): Extension { + val (allFields, teeEnforcedVector, originalRootOfTrust) = parsed + + // Build the new Root of Trust with our simulated values. + val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust) + teeEnforcedVector.add( + DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust) + ) + + // Add other simulated hardware properties. + AttestationBuilder.addSimulatedHardwareProperties(teeEnforcedVector) + + // Re-assemble the ASN.1 sequences. + // The list MUST be sorted by tag number for DER compliance. + // Manually convert the vector to a List, then sort it. + val elementList = (0 until teeEnforcedVector.size()).map { teeEnforcedVector.get(it) } + val sortedElements = elementList.sortedBy { (it as ASN1TaggedObject).tagNo } + val sortedTeeEnforced = DERSequence(sortedElements.toTypedArray()) + allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced + val patchedSequence = DERSequence(allFields) + val patchedOctets = DEROctetString(patchedSequence) + + return Extension(ATTESTATION_OID, false, patchedOctets) + } + + /** Helper data class to hold the parsed components of an attestation extension. */ + private data class ParsedAttestation( + val allFields: Array, + val teeEnforcedVector: ASN1EncodableVector, + val rootOfTrust: ASN1Encodable?, + ) +} diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt new file mode 100644 index 0000000..99b3097 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt @@ -0,0 +1,210 @@ +package org.matrix.TEESimulator.attestation + +import android.annotation.SuppressLint +import android.app.ActivityThread +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.SecureRandom +import java.security.cert.X509Certificate +import java.security.spec.ECGenParameterSpec +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.ASN1OctetString +import org.bouncycastle.asn1.ASN1Sequence +import org.bouncycastle.asn1.ASN1TaggedObject +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.cert.X509CertificateHolder +import org.matrix.TEESimulator.logging.SystemLogger +import org.matrix.TEESimulator.util.toHex + +/** + * The ASN.1 Object Identifier for the Key Attestation extension in Android. This is defined in the + * Android Keystore documentation. + */ +val ATTESTATION_OID: ASN1ObjectIdentifier = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.17") + +/** + * A service to interact with the device's Trusted Execution Environment (TEE). It provides + * functionality to check if the TEE is functional and to extract key attestation data from a + * genuinely generated certificate. + */ +@SuppressLint("PrivateApi") +object DeviceAttestationService { + + /** + * Holds key data extracted from a genuine device attestation. This data can be used as a + * baseline for creating simulated attestations. + * + * @property verifiedBootHash The verified boot hash from the root of trust. + * @property attestVersion The attestation version (e.g., 400 for KeyMint 4.0). + * @property keymasterVersion The Keymaster or KeyMint HAL version. + * @property osVersion The Android OS version integer. + */ + data class AttestationData( + val verifiedBootHash: ByteArray?, + val attestVersion: Int?, + val keymasterVersion: Int?, + val osVersion: Int?, + ) + + // A unique alias for the key used to perform the TEE functionality check. + private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck" + + /** + * Lazily determines if the device's TEE is functional by attempting to generate an + * attestation-backed key pair. The result is cached. + */ + val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() } + + /** + * Lazily fetches and parses attestation data from a genuinely generated certificate. The result + * is cached. Returns null if the TEE is not functional or parsing fails. + */ + val CachedAttestationData: AttestationData? by lazy { fetchAttestationData() } + + /** + * Checks if the TEE is working correctly by generating a key in the Android Keystore with an + * attestation challenge. + * + * @return `true` if a key with attestation was generated successfully, `false` otherwise. + */ + private fun checkTeeFunctionality(): Boolean { + SystemLogger.info("Performing TEE functionality check...") + return try { + // Ensure mainline modules and the correct Keystore provider are initialized. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + android.app.ActivityThread.initializeMainlineModules() + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + android.security.keystore2.AndroidKeyStoreProvider.install() + } else { + android.security.keystore.AndroidKeyStoreProvider.install() + } + + val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + val keyPairGenerator = + KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") + + // A random challenge is required for attestation. + val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) } + + val spec = + KeyGenParameterSpec.Builder(TEE_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN) + .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) + .setDigests(KeyProperties.DIGEST_SHA256) + .setAttestationChallenge(challenge) + .build() + + keyPairGenerator.initialize(spec) + keyPairGenerator.generateKeyPair() + + SystemLogger.info("TEE functionality check successful.") + true + } catch (e: Exception) { + SystemLogger.warning("TEE functionality check failed.", e) + false + } + } + + /** + * Retrieves the attestation certificate generated during the TEE check. The key entry is + * deleted after retrieval to clean up. + * + * @return The leaf `X509Certificate` containing the attestation, or `null` if unavailable. + */ + private fun getAttestationCertificate(): X509Certificate? { + if (!isTeeFunctional) return null + + return try { + val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + val certChain = keyStore.getCertificateChain(TEE_CHECK_KEY_ALIAS) + if (certChain.isNullOrEmpty()) { + SystemLogger.warning("Could not retrieve certificate chain for TEE check key.") + null + } else { + // Clean up the key from the keystore. + keyStore.deleteEntry(TEE_CHECK_KEY_ALIAS) + certChain[0] as X509Certificate + } + } catch (e: Exception) { + SystemLogger.error("Error retrieving attestation certificate.", e) + null + } + } + + /** + * Fetches and parses the attestation data from the certificate's extension. + * + * @return An `AttestationData` object, or `null` if the process fails. + */ + private fun fetchAttestationData(): AttestationData? { + val leafCert = getAttestationCertificate() ?: return null + + try { + val leafHolder = X509CertificateHolder(leafCert.encoded) + val extension: Extension = + leafHolder.getExtension(ATTESTATION_OID) + ?: return null // No attestation extension found. + + // The extension's value is an ASN.1 sequence. + val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets) + val fields = keyDescriptionSeq.toArray() + + val attestVersion = + ASN1Integer.getInstance( + fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX] + ) + .positiveValue + .toInt() + val keymasterVersion = + ASN1Integer.getInstance( + fields[AttestationConstants.KEY_DESCRIPTION_KEYMINT_VERSION_INDEX] + ) + .positiveValue + .toInt() + + var verifiedBootHash: ByteArray? = null + var osVersion: Int? = null + + val teeEnforced = + ASN1Sequence.getInstance( + fields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] + ) + teeEnforced.forEach { element -> + val tagged = element as ASN1TaggedObject + when (tagged.tagNo) { + AttestationConstants.TAG_ROOT_OF_TRUST -> { + val rotSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive()) + if (rotSeq.size() >= 4) { + verifiedBootHash = + ASN1OctetString.getInstance( + rotSeq.getObjectAt( + AttestationConstants + .ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX + ) + ) + .octets + } + } + AttestationConstants.TAG_OS_VERSION -> { // OS Version (TAG_OS_VERSION) + osVersion = + ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive()) + .positiveValue + .toInt() + } + } + } + + SystemLogger.info( + "Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, bootHash=${verifiedBootHash?.toHex()}" + ) + return AttestationData(verifiedBootHash, attestVersion, keymasterVersion, osVersion) + } catch (e: Exception) { + SystemLogger.error("Failed to parse attestation data from certificate.", e) + return null + } + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt new file mode 100644 index 0000000..0f76ad4 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt @@ -0,0 +1,158 @@ +package org.matrix.TEESimulator.attestation + +import android.hardware.security.keymint.EcCurve +import android.hardware.security.keymint.KeyParameter +import android.hardware.security.keymint.Tag +import java.math.BigInteger +import java.util.Date +import javax.security.auth.x500.X500Principal +import org.bouncycastle.asn1.x500.X500Name +import org.matrix.TEESimulator.logging.KeyMintParameterLogger + +/** + * A data class that parses and holds the parameters required for KeyMint key generation and + * attestation. It provides a structured way to access the properties defined by an array of + * `KeyParameter` objects. + */ + +// Reference: +// https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/key_parameter.rs +data class KeyMintAttestation( + val keySize: Int, + val algorithm: Int, + val ecCurve: Int, + val ecCurveName: String, + val purpose: List, + val digest: List, + val rsaPublicExponent: BigInteger?, + val certificateSerial: BigInteger?, + val certificateSubject: X500Name?, + val certificateNotBefore: Date?, + val certificateNotAfter: Date?, + val attestationChallenge: ByteArray?, + val brand: ByteArray?, + val device: ByteArray?, + val product: ByteArray?, + val manufacturer: ByteArray?, + val model: ByteArray?, + val imei: ByteArray?, + val secondImei: ByteArray?, + val meid: ByteArray?, +) { + /** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */ + constructor( + params: Array + ) : this( + // AOSP: [key_param(tag = KEY_SIZE, field = Integer)] + keySize = params.findInteger(Tag.KEY_SIZE) ?: 0, + + // AOSP: [key_param(tag = ALGORITHM, field = Algorithm)] + algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0, + + // AOSP: [key_param(tag = EC_CURVE, field = EcCurve)] + ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0, + ecCurveName = params.deriveEcCurveName(), + + // AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)] + purpose = params.findAllKeyPurpose(Tag.PURPOSE), + + // AOSP: [key_param(tag = DIGEST, field = Digest)] + digest = params.findAllDigests(Tag.DIGEST), + + // AOSP: [key_param(tag = RSA_PUBLIC_EXPONENT, field = LongInteger)] + rsaPublicExponent = params.findLongInteger(Tag.RSA_PUBLIC_EXPONENT), + + // AOSP: [key_param(tag = CERTIFICATE_SERIAL, field = Blob)] + certificateSerial = params.findBlob(Tag.CERTIFICATE_SERIAL)?.let { BigInteger(it) }, + + // AOSP: [key_param(tag = CERTIFICATE_SUBJECT, field = Blob)] + certificateSubject = + params.findBlob(Tag.CERTIFICATE_SUBJECT)?.let { X500Name(X500Principal(it).name) }, + + // AOSP: [key_param(tag = CERTIFICATE_NOT_BEFORE, field = DateTime)] + certificateNotBefore = params.findDate(Tag.CERTIFICATE_NOT_BEFORE), + + // AOSP: [key_param(tag = CERTIFICATE_NOT_AFTER, field = DateTime)] + certificateNotAfter = params.findDate(Tag.CERTIFICATE_NOT_AFTER), + + // AOSP: [key_param(tag = ATTESTATION_CHALLENGE, field = Blob)] + attestationChallenge = params.findBlob(Tag.ATTESTATION_CHALLENGE), + + // AOSP: [key_param(tag = ATTESTATION_ID_*, field = Blob)] + brand = params.findBlob(Tag.ATTESTATION_ID_BRAND), + device = params.findBlob(Tag.ATTESTATION_ID_DEVICE), + product = params.findBlob(Tag.ATTESTATION_ID_PRODUCT), + manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER), + model = params.findBlob(Tag.ATTESTATION_ID_MODEL), + imei = params.findBlob(Tag.ATTESTATION_ID_IMEI), + secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI), + meid = params.findBlob(Tag.ATTESTATION_ID_MEID), + ) { + // Log all parsed parameters for debugging purposes. + params.forEach { KeyMintParameterLogger.logParameter(it) } + } +} + +// --- Private helper extension functions for parsing KeyParameter arrays --- + +/** Maps to AOSP field = Integer */ +private fun Array.findInteger(tag: Int): Int? = + this.find { it.tag == tag }?.value?.integer + +/** Maps to AOSP field = Algorithm */ +private fun Array.findAlgorithm(tag: Int): Int? = + this.find { it.tag == tag }?.value?.algorithm + +/** Maps to AOSP field = EcCurve */ +private fun Array.findEcCurve(tag: Int): Int? = + this.find { it.tag == tag }?.value?.ecCurve + +/** Maps to AOSP field = LongInteger */ +private fun Array.findLongInteger(tag: Int): BigInteger? = + this.find { it.tag == tag }?.value?.longInteger?.toBigInteger() + +/** Maps to AOSP field = DateTime */ +private fun Array.findDate(tag: Int): Date? = + this.find { it.tag == tag }?.value?.dateTime?.let { Date(it) } + +/** Maps to AOSP field = Blob */ +private fun Array.findBlob(tag: Int): ByteArray? = + this.find { it.tag == tag }?.value?.blob + +/** Maps to AOSP field = KeyPurpose (Repeated) */ +private fun Array.findAllKeyPurpose(tag: Int): List = + this.filter { it.tag == tag }.map { it.value.keyPurpose } + +/** Maps to AOSP field = Digest (Repeated) */ +private fun Array.findAllDigests(tag: Int): List = + this.filter { it.tag == tag }.map { it.value.digest } + +/** + * Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back + * to KEY_SIZE (field=Integer). + */ +private fun Array.deriveEcCurveName(): String { + // 1. Try to find explicit EC_CURVE tag + val curveParam = this.find { it.tag == Tag.EC_CURVE } + + if (curveParam != null) { + val curveId = curveParam.value.ecCurve + return when (curveId) { + EcCurve.CURVE_25519 -> "CURVE_25519" + EcCurve.P_224 -> "secp224r1" + EcCurve.P_256 -> "secp256r1" + EcCurve.P_384 -> "secp384r1" + EcCurve.P_521 -> "secp521r1" + else -> throw IllegalArgumentException("Unknown EC curve: $curveId") + } + } + + // 2. Fallback to key size if the curve tag isn't present + val keySize = this.findInteger(Tag.KEY_SIZE) ?: 0 + return when (keySize) { + 224 -> "secp224r1" + 384 -> "secp384r1" + 521 -> "secp521r1" + else -> "secp256r1" // Default fallback + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt b/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt new file mode 100644 index 0000000..fea017d --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt @@ -0,0 +1,317 @@ +package org.matrix.TEESimulator.config + +import android.content.pm.IPackageManager +import android.os.Build +import android.os.FileObserver +import android.os.IBinder +import android.os.ServiceManager +import java.io.File +import java.util.concurrent.ConcurrentHashMap +import org.matrix.TEESimulator.attestation.DeviceAttestationService +import org.matrix.TEESimulator.logging.SystemLogger +import org.matrix.TEESimulator.pki.KeyBoxManager + +/** + * Manages application configuration, including which packages to process, what operation mode to + * use, and custom security patch levels. It uses a FileObserver to dynamically reload settings when + * configuration files change. + */ +object ConfigurationManager { + + /** Defines the processing mode for a given package. */ + enum class Mode { + /** Automatically decide between GENERATE and PATCH based on TEE status. */ + AUTO, + /** Patch the attestation of an existing certificate chain. */ + PATCH, + /** Generate a new certificate chain from scratch. */ + GENERATE, + } + + // --- Configuration Paths --- + const val CONFIG_PATH = "/data/adb/tricky_store" + private const val TARGET_PACKAGES_FILE = "target.txt" + private const val TEE_STATUS_FILE = "tee_status.txt" + private const val PATCH_LEVEL_FILE = "security_patch.txt" + private const val DEFAULT_KEYBOX_FILE = "keybox.xml" + private val configRoot = File(CONFIG_PATH) + + // --- In-Memory Configuration State --- + @Volatile private var packageModes = mapOf() + @Volatile private var packageKeyboxes = mapOf() + @Volatile private var isTeeBroken: Boolean? = null + @Volatile var customPatchLevelOverride: CustomPatchLevel? = null + + // Cache for UID to package name resolution. + private val uidToPackagesCache = ConcurrentHashMap>() + + /** + * Initializes the configuration manager by loading all settings from disk and starting the file + * observer to watch for changes. + */ + fun initialize() { + configRoot.mkdirs() + SystemLogger.info("Configuration root is: ${configRoot.absolutePath}") + + // Initial load of all configuration files. + loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE)) + loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE)) + storeTeeStatus() // Check and store the current TEE status. + + // Start watching for any subsequent file changes. + ConfigObserver.startWatching() + SystemLogger.info("Configuration initialized and file observer started.") + } + + /** + * Determines the keybox file to be used for a given UID. It maps the UID to its package(s) and + * checks for a specific keybox mapping. + * + * @param uid The calling UID. + * @return The name of the keybox file, or the default if none is specified. + */ + fun getKeyboxFileForUid(uid: Int): String { + val packages = getPackagesForUid(uid) + return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE + } + + /** Determines if the certificate for a given UID needs to be patched. */ + fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH + + /** Determines if a new certificate needs to be generated for a given UID. */ + fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE + + /** Resolves the operating mode for a given UID based on its packages and the TEE status. */ + private fun getPackageModeForUid(uid: Int): Mode? { + val packages = getPackagesForUid(uid) + if (packages.isEmpty()) return null + + // Lazily load TEE status if it hasn't been checked yet. + if (isTeeBroken == null) loadTeeStatus() + + // Find the first configured mode for any of the UID's packages. + for (pkg in packages) { + when (packageModes[pkg]) { + Mode.GENERATE -> return Mode.GENERATE + Mode.PATCH -> return Mode.PATCH + Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH + null -> continue // No config for this package, check the next one. + } + } + return null // No configuration found for this UID. + } + + /** + * Loads and parses the `target.txt` file, which defines the processing mode and keybox file for + * each package. + */ + private fun loadTargetPackages(file: File) { + if (!file.exists()) { + SystemLogger.warning("Configuration file not found: ${file.absolutePath}") + return + } + + val newModes = mutableMapOf() + val newKeyboxes = mutableMapOf() + var currentKeybox = DEFAULT_KEYBOX_FILE + val keyboxRegex = Regex("^\\[([a-zA-Z0-9_.-]+\\.xml)]$") + + try { + file.readLines().forEach { line -> + val trimmedLine = line.trim() + if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) return@forEach + + // Check if the line defines a new keybox scope. + keyboxRegex.find(trimmedLine)?.let { + currentKeybox = it.groupValues[1] + SystemLogger.info("Switching to keybox context: $currentKeybox") + return@forEach + } + + when { + // Suffix '!' means force GENERATE mode. + trimmedLine.endsWith("!") -> { + val pkg = trimmedLine.removeSuffix("!").trim() + newModes[pkg] = Mode.GENERATE + newKeyboxes[pkg] = currentKeybox + } + // Suffix '?' means force PATCH mode. + trimmedLine.endsWith("?") -> { + val pkg = trimmedLine.removeSuffix("?").trim() + newModes[pkg] = Mode.PATCH + newKeyboxes[pkg] = currentKeybox + } + // No suffix means AUTO mode. + else -> { + newModes[trimmedLine] = Mode.AUTO + newKeyboxes[trimmedLine] = currentKeybox + } + } + } + + // Atomically update the configuration maps. + packageModes = newModes + packageKeyboxes = newKeyboxes + uidToPackagesCache.clear() // Invalidate cache as package settings have changed. + SystemLogger.info("Successfully loaded ${newModes.size} package configurations.") + } catch (e: Exception) { + SystemLogger.error("Failed to load or parse ${file.name}", e) + } + } + + /** Loads the security patch level override configuration from `security_patch.txt`. */ + private fun loadPatchLevelConfig(file: File) { + if (file.exists()) { + try { + val lines = + file.readLines().mapNotNull { line -> + val trimmed = line.trim() + if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) trimmed else null + } + + if (lines.isEmpty()) { + customPatchLevelOverride = null + return + } + + // Handle simple case: one line sets the patch level for all components. + if (lines.size == 1 && '=' !in lines[0]) { + customPatchLevelOverride = + CustomPatchLevel(system = null, vendor = null, boot = null, all = lines[0]) + return + } + + // Handle key-value pair configuration. + val map = + lines + .mapNotNull { + val parts = it.split('=', limit = 2) + if (parts.size == 2) parts[0].trim().lowercase() to parts[1].trim() + else null + } + .toMap() + + val all = map["all"] + customPatchLevelOverride = + CustomPatchLevel( + system = map["system"] ?: all, + vendor = map["vendor"] ?: all, + boot = map["boot"] ?: all, + all = all, + ) + SystemLogger.info("Loaded custom security patch levels.") + } catch (e: Exception) { + SystemLogger.error("Failed to load or parse ${file.name}", e) + } + } else { + customPatchLevelOverride = null + } + } + + /** Checks the device's TEE status and writes the result to a file for persistence. */ + private fun storeTeeStatus() { + val statusFile = File(configRoot, TEE_STATUS_FILE) + isTeeBroken = !DeviceAttestationService.isTeeFunctional + try { + statusFile.writeText("tee_broken=$isTeeBroken") + SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken") + } catch (e: Exception) { + SystemLogger.error("Failed to write TEE status to file.", e) + } + } + + /** Loads the TEE status from the file. */ + private fun loadTeeStatus() { + val statusFile = File(configRoot, TEE_STATUS_FILE) + isTeeBroken = + if (statusFile.exists()) { + statusFile.readText().trim() == "tee_broken=true" + } else { + null // Status is unknown. + } + } + + /** + * A FileObserver that monitors the configuration directory for changes and triggers reloads of + * the relevant settings. + */ + private object ConfigObserver : FileObserver(configRoot, CLOSE_WRITE or MOVED_TO or DELETE) { + override fun onEvent(event: Int, path: String?) { + path ?: return + SystemLogger.info("Configuration file change detected: $path (event: $event)") + + val file = if (event != DELETE) File(configRoot, path) else null + when (path) { + TARGET_PACKAGES_FILE -> loadTargetPackages(file!!) + PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!) + // Any change to an XML file is assumed to be a keybox. The cache in KeyBoxUtils + // will handle reloading it on its next use. + else -> + if (path.endsWith(".xml")) { + SystemLogger.info( + "Keybox file $path may have changed. It will be reloaded on next access." + ) + KeyBoxManager.invalidateCache(path) + } + } + } + } + + // --- System Service Utilities --- + + private var iPackageManager: IPackageManager? = null + private val pmDeathRecipient = + object : IBinder.DeathRecipient { + override fun binderDied() { + (iPackageManager as? IBinder)?.unlinkToDeath(this, 0) + iPackageManager = null + SystemLogger.warning("Package manager service died. Will try to reconnect.") + } + } + + /** Retrieves an instance of the IPackageManager service. */ + fun getPackageManager(): IPackageManager? { + if (iPackageManager == null) { + // Use a robust method to get the service binder. + val binder = waitForSystemService("package") ?: return null + binder.linkToDeath(pmDeathRecipient, 0) + iPackageManager = IPackageManager.Stub.asInterface(binder) + } + return iPackageManager + } + + /** Retrieves the package names associated with a UID. */ + fun getPackagesForUid(uid: Int): Array { + return uidToPackagesCache.getOrPut(uid) { + try { + getPackageManager()?.getPackagesForUid(uid) ?: emptyArray() + } catch (e: Exception) { + SystemLogger.warning("Failed to get packages for UID $uid", e) + emptyArray() + } + } + } + + /** Waits for a system service to become available, with retries. */ + private fun waitForSystemService(name: String): IBinder? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return ServiceManager.waitForService(name) + } + // Fallback for older Android versions. + repeat(70) { + val service = ServiceManager.getService(name) + if (service != null) return service + Thread.sleep(500) + } + SystemLogger.error("Failed to get system service after multiple retries: $name") + return null + } +} + +/** Data class representing custom security patch level overrides. */ +data class CustomPatchLevel( + val system: String?, + val vendor: String?, + val boot: String?, + val all: String?, +) diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt new file mode 100644 index 0000000..9de7293 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt @@ -0,0 +1,306 @@ +package org.matrix.TEESimulator.interception.core + +import android.os.Binder +import android.os.IBinder +import android.os.Parcel +import org.matrix.TEESimulator.config.ConfigurationManager +import org.matrix.TEESimulator.logging.SystemLogger + +/** + * An abstract base class for intercepting binder transactions. + * + * This class acts as a proxy, receiving transaction calls that have been hooked at the native + * level. It provides a structured way to inspect and modify data before (`onPreTransact`) and after + * (`onPostTransact`) the original transaction is executed. + * + * The communication flow is as follows: + * 1. A native library hooks the `transact` method of a target service (e.g., keystore). + * 2. When a hooked transaction occurs, the native code calls this Binder object's `onTransact` + * method. + * 3. This class decodes the incoming parcel, determines if it's a pre- or post-transaction hook, + * and calls the appropriate abstract method (`onPreTransact` or `onPostTransact`). + * 4. The subclass implementation decides how to handle the transaction by returning a + * `TransactionResult`. + * 5. This class encodes the result into the reply parcel, which the native hook reads to determine + * its next action. + */ +abstract class BinderInterceptor : Binder() { + + /** + * Defines the possible outcomes of an interception attempt. The native hook layer will + * interpret this result to decide its next action. + */ + sealed class TransactionResult { + /** Instructs the native hook to skip calling the original binder method entirely. */ + object SkipTransaction : TransactionResult() + + /** Instructs the native hook to proceed with calling the original binder method. */ + object Continue : TransactionResult() + + /** + * Skips the original call and immediately returns a custom reply parcel to the caller. The + * provided parcel will be recycled after use. + */ + data class OverrideReply(val code: Int = 0, val reply: Parcel) : TransactionResult() + + /** + * Modifies the transaction's input data before forwarding it to the original binder method. + * The provided parcel will be recycled after use. + */ + data class OverrideData(val data: Parcel) : TransactionResult() + + /** Instructs the native hook to skip the post transaction hook. */ + object ContinueAndSkipPost : TransactionResult() + } + + /** + * Called *before* the original binder transaction is executed. + * + * @param txId A unique ID for tracking this transaction. + * @param target The original IBinder service being called. + * @param code The transaction code of the method being called. + * @param flags Transaction flags. + * @param callingUid The UID of the process making the call. + * @param callingPid The PID of the process making the call. + * @param data The parcel containing the input data for the transaction. + * @return A [TransactionResult] indicating how to proceed. + */ + open fun onPreTransact( + txId: Long, + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + ): TransactionResult = TransactionResult.ContinueAndSkipPost + + /** + * Called *after* the original binder transaction has been executed. + * + * @param txId A unique ID for tracking this transaction. + * @param target The original IBinder service that was called. + * @param code The transaction code of the method that was called. + * @param flags Transaction flags. + * @param callingUid The UID of the process that made the call. + * @param callingPid The PID of the process that made the call. + * @param data The original input data parcel. + * @param reply The reply parcel from the original transaction. Can be null if the call was + * one-way. + * @param resultCode The result code from the original transaction. + * @return A [TransactionResult]. Typically `Skip` (to accept the original reply) or + * `OverrideReply`. + */ + open fun onPostTransact( + txId: Long, + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + reply: Parcel?, + resultCode: Int, + ): TransactionResult = TransactionResult.SkipTransaction + + /** + * The entry point for calls from the native hook layer. This method decodes the custom parcel + * format sent by the hook and dispatches to the appropriate handler (`handlePreTransact` or + * `handlePostTransact`). + */ + final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + // The native hook prepends a transaction ID to the data parcel. + val txId = data.readLong() + val result = + when (code) { + // These codes are defined in the native layer to distinguish hook types. + PRE_TRANSACT_CODE -> handlePreTransact(txId, data) + POST_TRANSACT_CODE -> handlePostTransact(txId, data) + else -> return super.onTransact(code, data, reply, flags) + } + + // The reply parcel is guaranteed to be non-null for our custom transactions. + writeResultToReply(result, reply!!) + return true + } + + /** Decodes the parcel for a pre-transaction hook and calls the user-overridable method. */ + private fun handlePreTransact(txId: Long, data: Parcel): TransactionResult { + // The native hook marshals the original transaction's arguments into the data parcel. + val target = data.readStrongBinder()!! + val transactionCode = data.readInt() + val transactionFlags = data.readInt() + val callingUid = data.readInt() + val callingPid = data.readInt() + val dataSize = data.readLong() + + // We must create a new parcel containing only the original transaction data. + val transactionData = Parcel.obtain() + return try { + transactionData.appendFrom(data, data.dataPosition(), dataSize.toInt()) + transactionData.setDataPosition(0) + onPreTransact( + txId, + target, + transactionCode, + transactionFlags, + callingUid, + callingPid, + transactionData, + ) + } finally { + transactionData.recycle() + } + } + + /** Decodes the parcel for a post-transaction hook and calls the user-overridable method. */ + private fun handlePostTransact(txId: Long, data: Parcel): TransactionResult { + val target = data.readStrongBinder()!! + val transactionCode = data.readInt() + val transactionFlags = data.readInt() + val callingUid = data.readInt() + val callingPid = data.readInt() + + // The native hook also marshals the original data and reply parcels. + val transactionData = Parcel.obtain() + val transactionReply = Parcel.obtain() + return try { + val dataSize = data.readLong().toInt() + transactionData.appendFrom(data, data.dataPosition(), dataSize) + transactionData.setDataPosition(0) + data.setDataPosition(data.dataPosition() + dataSize) + + val resultCode = data.readInt() + + val replySize = data.readLong().toInt() + val reply = + if (replySize > 0) { + transactionReply.appendFrom(data, data.dataPosition(), replySize) + transactionReply.setDataPosition(0) + transactionReply + } else null + + onPostTransact( + txId, + target, + transactionCode, + transactionFlags, + callingUid, + callingPid, + transactionData, + reply, + resultCode, + ) + } finally { + transactionData.recycle() + transactionReply.recycle() + } + } + + /** Encodes the `TransactionResult` into the reply parcel for the native hook to interpret. */ + private fun writeResultToReply(result: TransactionResult, reply: Parcel) { + when (result) { + is TransactionResult.SkipTransaction -> reply.writeInt(RESULT_SKIP_TRANSACTION) + is TransactionResult.Continue -> reply.writeInt(RESULT_CONTINUE) + is TransactionResult.OverrideReply -> { + reply.writeInt(RESULT_OVERRIDE_REPLY) + reply.writeInt(result.code) + reply.writeLong(result.reply.dataSize().toLong()) + reply.appendFrom(result.reply, 0, result.reply.dataSize()) + result.reply.recycle() + } + is TransactionResult.OverrideData -> { + reply.writeInt(RESULT_OVERRIDE_DATA) + reply.writeLong(result.data.dataSize().toLong()) + reply.appendFrom(result.data, 0, result.data.dataSize()) + result.data.recycle() + } + is TransactionResult.ContinueAndSkipPost -> + reply.writeInt(RESULT_CONTINUE_AND_SKIP_POST) + } + } + + /** Helper function for consistent logging of intercepted transactions. */ + protected fun logTransaction( + txId: Long, + methodName: String, + callingUid: Int, + callingPid: Int, + isIntercepting: Boolean = true, + ) { + val action = if (isIntercepting) "Intercept" else "Observe" + val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString() + SystemLogger.debug( + "[TX_ID: $txId] $action $methodName for packages=[$packages] (uid=$callingUid, pid=$callingPid)" + ) + } + + companion object { + // These codes must be kept in sync with the native injection library. + + // --- Backdoor Codes --- + // Special transaction code to ask the injected library for its backdoor binder. + private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt() + // Code used by the backdoor binder to register a new interceptor. + private const val REGISTER_INTERCEPTOR_CODE = 1 + + // --- Hook Type Codes --- + // Indicates that the call is for a pre-transaction hook. + private const val PRE_TRANSACT_CODE = 1 + // Indicates that the call is for a post-transaction hook. + private const val POST_TRANSACT_CODE = 2 + + // --- Result Codes --- + // Instructs the native hook to skip the original transaction. + private const val RESULT_SKIP_TRANSACTION = 1 + // Instructs the native hook to execute the original transaction. + private const val RESULT_CONTINUE = 2 + // Instructs the native hook to return a custom reply. + private const val RESULT_OVERRIDE_REPLY = 3 + // Instructs the native hook to use modified input data for the transaction. + private const val RESULT_OVERRIDE_DATA = 4 + // Instructs the native hook to skip the post transaction hook. + private const val RESULT_CONTINUE_AND_SKIP_POST = 5 + + /** + * Probes a binder service to see if our native library has been injected. If successful, it + * returns a "backdoor" binder that can be used to register interceptors. + */ + fun getBackdoor(binder: IBinder): IBinder? { + val data = Parcel.obtain() + val reply = Parcel.obtain() + return try { + if (binder.transact(BACKDOOR_TRANSACTION_CODE, data, reply, 0)) { + SystemLogger.debug("Backdoor access granted for binder: $binder") + reply.readStrongBinder() + } else { + SystemLogger.debug("Backdoor not found for binder: $binder") + null + } + } catch (e: Exception) { + SystemLogger.error("Failed to transact for backdoor.", e) + null + } finally { + data.recycle() + reply.recycle() + } + } + + /** Uses the backdoor binder to register an interceptor for a specific target service. */ + fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) { + val data = Parcel.obtain() + val reply = Parcel.obtain() + try { + data.writeStrongBinder(target) + data.writeStrongBinder(interceptor) + backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0) + SystemLogger.info("Registered interceptor for target: $target") + } catch (e: Exception) { + SystemLogger.error("Failed to register binder interceptor.", e) + } finally { + data.recycle() + reply.recycle() + } + } + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/AbstractKeystoreInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/AbstractKeystoreInterceptor.kt new file mode 100644 index 0000000..d080fb6 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/AbstractKeystoreInterceptor.kt @@ -0,0 +1,141 @@ +package org.matrix.TEESimulator.interception.keystore + +import android.os.IBinder +import android.os.ServiceManager +import kotlin.system.exitProcess +import org.matrix.TEESimulator.interception.core.BinderInterceptor +import org.matrix.TEESimulator.logging.SystemLogger + +/** + * An abstract base class for intercepting Android's Keystore services. + * + * It encapsulates the common logic for finding the Keystore service, injecting the native hook if + * necessary, and setting up the binder interceptor. It also handles service death events to ensure + * stability. + */ +abstract class AbstractKeystoreInterceptor : BinderInterceptor() { + + // --- Abstract Properties to be Implemented by Subclasses --- + + /** The full name of the system service to intercept (e.g., "android.security.keystore"). */ + protected abstract val serviceName: String + + /** The name of the process hosting the service (e.g., "keystore"). */ + protected abstract val processName: String + + /** The shell command used to inject the native library into the target process. */ + protected abstract val injectionCommand: String + + // --- State Management --- + + /** The original IBinder for the Keystore service. */ + protected lateinit var keystoreService: IBinder + private var injectionAttempted = false + private var retryCount = 0 + private val maxRetries = 5 + + /** + * Attempts to initialize the interceptor for the target Keystore service. + * + * This method orchestrates the process: + * 1. It tries to get the service binder. + * 2. It probes for the native backdoor. + * 3. If the backdoor exists, it sets up the interceptor. + * 4. If not, it attempts to inject the native library and returns `false` to signal a retry is + * needed. + * + * @return `true` if the interceptor was successfully registered, `false` otherwise. + */ + fun tryRunKeystoreInterceptor(): Boolean { + SystemLogger.info( + "Initializing interceptor for '$serviceName' (attempt ${retryCount + 1})..." + ) + + val service = ServiceManager.getService(serviceName) + if (service == null) { + SystemLogger.warning("Service '$serviceName' not found. Will retry.") + retryCount++ + return false + } + + val backdoor = getBackdoor(service) + return if (backdoor != null) { + setupInterceptor(service, backdoor) + true // Success + } else { + handleMissingBackdoor() + false // Failure, requires retry + } + } + + /** Registers this interceptor with the native hook layer and sets up a death recipient. */ + private fun setupInterceptor(service: IBinder, backdoor: IBinder) { + keystoreService = service + SystemLogger.info("Registering interceptor for service: $serviceName") + register(backdoor, service, this) + service.linkToDeath(createDeathRecipient(), 0) + onInterceptorReady(service, backdoor) + } + + /** + * Handles the case where the native backdoor is not present. It triggers the injection command + * on the first attempt and manages the retry logic. + */ + private fun handleMissingBackdoor() { + if (!injectionAttempted) { + SystemLogger.warning( + "Backdoor not found. Attempting to inject native library into '$processName'." + ) + performInjection() + injectionAttempted = true + } + + retryCount++ + if (retryCount >= maxRetries) { + SystemLogger.error( + "Failed to find backdoor after $maxRetries retries. The service may have crashed or injection failed. Exiting." + ) + exitProcess(1) + } + } + + /** Executes the shell command to inject the native library into the target process. */ + private fun performInjection() { + try { + val command = arrayOf("/system/bin/sh", "-c", injectionCommand) + SystemLogger.debug("Executing injection command: ${command.joinToString(" ")}") + val process = Runtime.getRuntime().exec(command) + val exitCode = process.waitFor() + if (exitCode != 0) { + SystemLogger.error("Injection process failed with exit code $exitCode. Exiting.") + exitProcess(1) + } + SystemLogger.info("Injection process completed.") + } catch (e: Exception) { + SystemLogger.error("An exception occurred during injection. Exiting.", e) + exitProcess(1) + } + } + + /** + * Creates a `DeathRecipient` that will restart the application if the intercepted service dies. + */ + private fun createDeathRecipient() = + IBinder.DeathRecipient { + SystemLogger.error( + "The intercepted service '$serviceName' has died. Restarting application." + ) + exitProcess(0) + } + + /** + * A hook for subclasses to perform additional setup after the interceptor is registered. For + * example, to intercept sub-services. + * + * @param service The main service binder. + * @param backdoor The backdoor binder for registering more interceptors. + */ + protected open fun onInterceptorReady(service: IBinder, backdoor: IBinder) { + // Default implementation does nothing. + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt new file mode 100644 index 0000000..612b655 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt @@ -0,0 +1,109 @@ +package org.matrix.TEESimulator.interception.keystore + +import android.os.Parcel +import android.os.Parcelable +import android.security.KeyStore +import java.security.MessageDigest +import java.security.cert.Certificate +import java.util.Base64 +import org.matrix.TEESimulator.interception.core.BinderInterceptor +import org.matrix.TEESimulator.logging.SystemLogger + +data class KeyIdentifier(val uid: Int, val alias: String) + +/** A collection of utility functions to support binder interception. */ +object InterceptorUtils { + + /** + * Uses reflection to get the integer transaction code for a given method name from a Stub + * class. This is necessary for older Android versions where codes are not public constants. + */ + fun getTransactCode(clazz: Class<*>, method: String): Int { + return try { + clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true }.getInt(null) + } catch (e: Exception) { + SystemLogger.error( + "Failed to get transaction code for method '$method' in class '${clazz.simpleName}'.", + e, + ) + -1 // Return an invalid code + } + } + + /** Creates an `OverrideReply` parcel that indicates success with no data. */ + fun createSuccessReply(): BinderInterceptor.TransactionResult.OverrideReply { + val parcel = + Parcel.obtain().apply { + writeNoException() + writeInt(KeyStore.NO_ERROR) + } + return BinderInterceptor.TransactionResult.OverrideReply(0, parcel) + } + + /** Creates an `OverrideReply` parcel containing a raw byte array. */ + fun createByteArrayReply(data: ByteArray): BinderInterceptor.TransactionResult.OverrideReply { + val parcel = + Parcel.obtain().apply { + writeNoException() + writeByteArray(data) + } + return BinderInterceptor.TransactionResult.OverrideReply(KeyStore.NO_ERROR, parcel) + } + + /** Creates an `OverrideReply` parcel containing a Parcelable object. */ + fun createTypedObjectReply( + obj: T, + flags: Int = 0, + ): BinderInterceptor.TransactionResult.OverrideReply { + val parcel = + Parcel.obtain().apply { + writeNoException() + writeTypedObject(obj, flags) + } + return BinderInterceptor.TransactionResult.OverrideReply(0, parcel) + } + + /** + * Extracts the true key alias from the keystore-prefixed string (e.g., "user_cert_my-alias" -> + * "my-alias"). + */ + fun extractAlias(prefixedAlias: String): String { + val underscoreIndex = prefixedAlias.indexOf('_') + val secondUnderscoreIndex = prefixedAlias.indexOf('_', underscoreIndex + 1) + return if (secondUnderscoreIndex != -1) { + prefixedAlias.substring(secondUnderscoreIndex + 1) + } else { + prefixedAlias + } + } + + /** Checks if a reply parcel contains an exception without consuming it. */ + fun hasException(reply: Parcel): Boolean { + val initialPosition = reply.dataPosition() + val hasEx = + try { + reply.readException() + reply.dataPosition() > initialPosition // An exception was written + } catch (_: Exception) { + false + } + reply.setDataPosition(initialPosition) + return hasEx + } + + /** + * Generates a URL-safe SHA-256 fingerprint of a certificate's public key. Used to uniquely + * identify keys imported by the user. + */ + fun getPublicKeyFingerprint(chain: Array?): String? { + if (chain.isNullOrEmpty()) return null + return try { + val publicKeyBytes = chain[0].publicKey.encoded + val hashBytes = MessageDigest.getInstance("SHA-256").digest(publicKeyBytes) + Base64.getUrlEncoder().withoutPadding().encodeToString(hashBytes) + } catch (e: Exception) { + SystemLogger.error("Failed to create public key fingerprint.", e) + null + } + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt new file mode 100644 index 0000000..44faaef --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt @@ -0,0 +1,186 @@ +package org.matrix.TEESimulator.interception.keystore + +import android.annotation.SuppressLint +import android.hardware.security.keymint.SecurityLevel +import android.os.IBinder +import android.os.Parcel +import android.system.keystore2.IKeystoreService +import android.system.keystore2.KeyDescriptor +import android.system.keystore2.KeyEntryResponse +import org.matrix.TEESimulator.attestation.AttestationPatcher +import org.matrix.TEESimulator.config.ConfigurationManager +import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor +import org.matrix.TEESimulator.logging.SystemLogger +import org.matrix.TEESimulator.pki.CertificateHelper + +/** + * Interceptor for the `IKeystoreService` on Android S (API 31) and newer. + * + * This version of Keystore delegates most cryptographic operations to `IKeystoreSecurityLevel` + * sub-services (for TEE, StrongBox, etc.). This interceptor's main role is to set up interceptors + * for those sub-services and to patch certificate chains on their way out. + */ +@SuppressLint("BlockedPrivateApi") +object Keystore2Interceptor : AbstractKeystoreInterceptor() { + // Transaction codes for the IKeystoreService interface methods we are interested in. + private val GET_KEY_ENTRY_TRANSACTION = + InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "getKeyEntry") + private val DELETE_KEY_TRANSACTION = + InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "deleteKey") + + override val serviceName = "android.system.keystore2.IKeystoreService/default" + override val processName = "keystore2" + override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry" + + /** + * This method is called once the main service is hooked. It proceeds to find and hook the + * security level sub-services (e.g., TEE, StrongBox). + */ + override fun onInterceptorReady(service: IBinder, backdoor: IBinder) { + val keystoreInterface = IKeystoreService.Stub.asInterface(service) + setupSecurityLevelInterceptors(keystoreInterface, backdoor) + } + + private fun setupSecurityLevelInterceptors(service: IKeystoreService, backdoor: IBinder) { + // Attempt to get and intercept the TEE security level service. + runCatching { + service.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT)?.let { tee -> + SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...") + val interceptor = + KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT) + register(backdoor, tee.asBinder(), interceptor) + } + } + .onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) } + + // Attempt to get and intercept the StrongBox security level service. + runCatching { + service.getSecurityLevel(SecurityLevel.STRONGBOX)?.let { strongbox -> + SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...") + val interceptor = + KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX) + register(backdoor, strongbox.asBinder(), interceptor) + } + } + .onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) } + } + + override fun onPreTransact( + txId: Long, + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + ): TransactionResult { + if (code == GET_KEY_ENTRY_TRANSACTION) { + logTransaction(txId, "getKeyEntry", callingUid, callingPid) + data.enforceInterface(IKeystoreService.DESCRIPTOR) + val descriptor = + data.readTypedObject(KeyDescriptor.CREATOR) + ?: return TransactionResult.SkipTransaction + val keyId = KeyIdentifier(callingUid, descriptor.alias) + SystemLogger.debug("Checking $keyId") + + // If a key was generated in software, we must return the stored response directly. + if (ConfigurationManager.shouldGenerate(callingUid)) { + val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) + if (response != null) { + SystemLogger.info( + "[TX_ID: $txId] Returning generated key for alias '${descriptor.alias}'." + ) + return InterceptorUtils.createTypedObjectReply(response) + } + // If not found, return null to indicate the key doesn't exist. + return InterceptorUtils.createTypedObjectReply(null as KeyEntryResponse?) + } + + // For attestation in hack mode, a key is generated and should be returned directly. + if ( + ConfigurationManager.shouldPatch(callingUid) && + KeyMintSecurityLevelInterceptor.isAttestationKey(keyId) + ) { + val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) + if (response != null) { + SystemLogger.info( + "[TX_ID: $txId] Returning attestation key for alias '${descriptor.alias}' to skip patching." + ) + return InterceptorUtils.createTypedObjectReply(response) + } + return TransactionResult.Continue + } + } + return TransactionResult + .ContinueAndSkipPost // Let most calls go through to the real service. + } + + override fun onPostTransact( + txId: Long, + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + reply: Parcel?, + resultCode: Int, + ): TransactionResult { + if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply)) + return TransactionResult.SkipTransaction + + data.enforceInterface(IKeystoreService.DESCRIPTOR) + when (code) { + GET_KEY_ENTRY_TRANSACTION -> { + logTransaction(txId, "post-getKeyEntry", callingUid, callingPid) + if (!ConfigurationManager.shouldPatch(callingUid)) + return TransactionResult.SkipTransaction + + return try { + val response = + reply.readTypedObject(KeyEntryResponse.CREATOR) + ?: return TransactionResult.SkipTransaction + reply.setDataPosition(0) // Reset for potential reuse. + + val originalChain = CertificateHelper.getCertificateChain(response) + val fingerprint = InterceptorUtils.getPublicKeyFingerprint(originalChain) + + // Do not patch keys that were imported by the user. + if ( + fingerprint != null && + KeyMintSecurityLevelInterceptor.isUserImportedKey(fingerprint) + ) { + SystemLogger.warning( + "[TX_ID: $txId] Skipping patch for user-imported key with fingerprint: $fingerprint" + ) + return TransactionResult.SkipTransaction + } + + // Perform the attestation patch. + val newChain = + AttestationPatcher.patchCertificateChain(originalChain, callingUid) + CertificateHelper.updateCertificateChain(response.metadata, newChain) + .getOrThrow() + + SystemLogger.info( + "[TX_ID: $txId] Successfully patched certificate chain for alias." + ) + InterceptorUtils.createTypedObjectReply(response) + } catch (e: Exception) { + SystemLogger.error("[TX_ID: $txId] Failed to patch certificate chain.", e) + TransactionResult.SkipTransaction + } + } + DELETE_KEY_TRANSACTION -> { + // When a key is deleted, clean up our associated state. + val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) + if (descriptor != null) { + val keyId = KeyIdentifier(callingUid, descriptor.alias) + KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId) + } + return TransactionResult.SkipTransaction + } + } + return TransactionResult.SkipTransaction + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/KeystoreInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/KeystoreInterceptor.kt new file mode 100644 index 0000000..e07bfd2 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/KeystoreInterceptor.kt @@ -0,0 +1,179 @@ +package org.matrix.TEESimulator.interception.keystore + +import android.annotation.SuppressLint +import android.os.IBinder +import android.os.Parcel +import android.security.Credentials +import android.security.keystore.IKeystoreService +import java.security.cert.Certificate +import java.util.concurrent.ConcurrentHashMap +import org.matrix.TEESimulator.attestation.AttestationPatcher +import org.matrix.TEESimulator.config.ConfigurationManager +import org.matrix.TEESimulator.logging.SystemLogger +import org.matrix.TEESimulator.pki.CertificateHelper + +/** + * Interceptor for the legacy `IKeystoreService` on Android Q (API 29) and R (API 30). + * + * This interceptor handles the older, monolithic Keystore service. Unlike Keystore2, it doesn't + * have security level sub-services, so all logic is contained here. Key generation is fully + * simulated in software for packages in 'generate' mode. + */ +@SuppressLint("BlockedPrivateApi", "PrivateApi") +object KeystoreInterceptor : AbstractKeystoreInterceptor() { + + // Transaction codes are dynamically retrieved via reflection for compatibility. + private val GET_TRANSACTION by lazy { + InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "get") + } + private val GENERATE_KEY_TRANSACTION by lazy { + InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "generateKey") + } + private val GET_KEY_CHARACTERISTICS_TRANSACTION by lazy { + InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "getKeyCharacteristics") + } + private val EXPORT_KEY_TRANSACTION by lazy { + InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "exportKey") + } + private val ATTEST_KEY_TRANSACTION by lazy { + InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "attestKey") + } + + override val serviceName = "android.security.keystore" + override val processName = "keystore" + override val injectionCommand = "exec ./inject `pidof keystore` libTEESimulator.so entry" + + private const val SERVICE_DESCRIPTOR = "android.security.keystore.IKeystoreService" + + // Cache to store the fully patched chain after the leaf is requested. + private val patchedChainCache = ConcurrentHashMap>() + + override fun onPreTransact( + txId: Long, + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + ): TransactionResult { + // This interceptor only needs to act on pre-transaction for software key generation. + if (ConfigurationManager.shouldGenerate(callingUid)) { + when (code) { + GENERATE_KEY_TRANSACTION, + GET_KEY_CHARACTERISTICS_TRANSACTION, + EXPORT_KEY_TRANSACTION, + ATTEST_KEY_TRANSACTION -> { + // TODO: Implement the full software simulation logic. + logTransaction(txId, "unimplemented-generate-flow", callingUid, callingPid) + return InterceptorUtils.createSuccessReply() + } + } + } else if (ConfigurationManager.shouldGenerate(callingUid)) { + if (code == GET_TRANSACTION) return TransactionResult.Continue + } + + return TransactionResult.ContinueAndSkipPost + } + + override fun onPostTransact( + txId: Long, + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + reply: Parcel?, + resultCode: Int, + ): TransactionResult { + if ( + target != keystoreService || + code != GET_TRANSACTION || + reply == null || + InterceptorUtils.hasException(reply) + ) { + return TransactionResult.SkipTransaction + } + + if (!ConfigurationManager.shouldPatch(callingUid)) return TransactionResult.SkipTransaction + + return try { + data.enforceInterface(SERVICE_DESCRIPTOR) + val alias = data.readString() ?: "" + val extractedAlias = InterceptorUtils.extractAlias(alias) + val keyId = KeyIdentifier(callingUid, extractedAlias) + + when { + // Case 1: The app is requesting the leaf certificate. + alias.startsWith(Credentials.USER_CERTIFICATE) -> { + logTransaction(txId, "post-get (user cert)", callingUid, callingPid) + val originalLeafBytes = + reply.createByteArray() ?: return TransactionResult.SkipTransaction + + // The original chain is not available, + // so we must pass a temporary one to the patcher. + // The patcher only needs the original leaf to extract details. + val originalLeafCert = + (CertificateHelper.toCertificate(originalLeafBytes) + as CertificateHelper.OperationResult.Success) + .data + val tempChain = arrayOf(originalLeafCert) + + // Perform the COMPLETE patch and rebuild operation. + val newFullChain = + AttestationPatcher.patchCertificateChain(tempChain, callingUid) + + // If patching was successful and we have a valid chain... + if (newFullChain.isNotEmpty() && newFullChain[0] != originalLeafCert) { + // ...cache the entire new chain for the subsequent "ca_cert" call. + patchedChainCache[keyId] = newFullChain + + // And return only the new leaf's bytes, as the API expects. + SystemLogger.info( + "[TX_ID: $txId] Patched and cached chain for alias '$extractedAlias'. Returning new leaf." + ) + InterceptorUtils.createByteArrayReply(newFullChain[0].encoded) + } else { + // Patching failed or was skipped; do nothing. + TransactionResult.SkipTransaction + } + } + + // Case 2: The app is requesting the CA certificate chain. + alias.startsWith(Credentials.CA_CERTIFICATE) -> { + logTransaction(txId, "post-get (ca cert)", callingUid, callingPid) + + // Retrieve the full, correct chain we cached during the leaf request. + val cachedChain = patchedChainCache.remove(keyId) + + if (cachedChain != null && cachedChain.size > 1) { + // The CA chain is everything *except* the first element (the leaf). + val caCerts = cachedChain.drop(1) + val caCertsBytes = CertificateHelper.certificatesToByteArray(caCerts) + + SystemLogger.info( + "[TX_ID: $txId] Returning cached CA chain for alias '$extractedAlias'." + ) + InterceptorUtils.createByteArrayReply(caCertsBytes!!) + } else { + // We have no cached chain. + // This could mean the app requested the CA without requesting the leaf + // first, or patching failed. + // In this case, we cannot safely intervene. + // Let the original reply pass through. + SystemLogger.warning( + "[TX_ID: $txId] No cached chain found for CA request on alias '$extractedAlias'. Skipping." + ) + TransactionResult.SkipTransaction + } + } + + else -> TransactionResult.SkipTransaction + } + } catch (e: Exception) { + SystemLogger.error("[TX_ID: $txId] Failed during legacy post-transaction patching.", e) + TransactionResult.SkipTransaction + } + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt new file mode 100644 index 0000000..b6e2dcf --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt @@ -0,0 +1,266 @@ +package org.matrix.TEESimulator.interception.keystore.shim + +import android.hardware.security.keymint.KeyParameter +import android.hardware.security.keymint.KeyParameterValue +import android.os.IBinder +import android.os.Parcel +import android.system.keystore2.* +import java.security.KeyPair +import java.security.cert.Certificate +import java.util.concurrent.ConcurrentHashMap +import org.matrix.TEESimulator.attestation.AttestationConstants +import org.matrix.TEESimulator.attestation.KeyMintAttestation +import org.matrix.TEESimulator.config.ConfigurationManager +import org.matrix.TEESimulator.interception.core.BinderInterceptor +import org.matrix.TEESimulator.interception.keystore.InterceptorUtils +import org.matrix.TEESimulator.interception.keystore.KeyIdentifier +import org.matrix.TEESimulator.logging.SystemLogger +import org.matrix.TEESimulator.pki.CertificateGenerator +import org.matrix.TEESimulator.pki.CertificateHelper + +/** + * Intercepts calls to an `IKeystoreSecurityLevel` service (e.g., TEE or StrongBox). This is where + * the core logic for key generation and import handling for modern Android resides. + */ +class KeyMintSecurityLevelInterceptor( + private val original: IKeystoreSecurityLevel, + private val securityLevel: Int, +) : BinderInterceptor() { + + // --- Data Structures for State Management --- + data class GeneratedKeyInfo(val keyPair: KeyPair, val response: KeyEntryResponse) + + override fun onPreTransact( + txId: Long, + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + ): TransactionResult { + // This interceptor only handles the 'generateKey' transaction directly. + if (code == GENERATE_KEY_TRANSACTION) { + logTransaction(txId, "generateKey", callingUid, callingPid) + data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) + return handleGenerateKey(callingUid, data) + } + return TransactionResult.ContinueAndSkipPost + } + + override fun onPostTransact( + txId: Long, + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + reply: Parcel?, + resultCode: Int, + ): TransactionResult { + // We only care about successful 'importKey' transactions to track user-provided keys. + if ( + code == IMPORT_KEY_TRANSACTION && + resultCode == 0 && + reply != null && + !InterceptorUtils.hasException(reply) + ) { + logTransaction(txId, "post-importKey", callingUid, callingPid) + data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) + handleImportKey(callingUid, data, reply) + } + return TransactionResult.SkipTransaction + } + + /** + * Handles the `generateKey` transaction. Based on the configuration for the calling UID, it + * either generates a key in software or lets the call pass through to the hardware. + */ + private fun handleGenerateKey(callingUid: Int, data: Parcel): TransactionResult { + return runCatching { + val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! + val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR) + val params = data.createTypedArray(KeyParameter.CREATOR)!! + val parsedParams = KeyMintAttestation(params) + val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) + + // Determine if we need to generate a key based on config or if it's an attestation + // request in patch mode. + val needsSoftwareGeneration = + ConfigurationManager.shouldGenerate(callingUid) || + (ConfigurationManager.shouldPatch(callingUid) && + parsedParams.attestationChallenge != null) + + if (needsSoftwareGeneration) { + SystemLogger.info( + "Generating software key for alias '${keyDescriptor.alias}' (UID: $callingUid)." + ) + + // Generate the key pair and certificate chain. + val keyData = + CertificateGenerator.generateAttestedKeyPair( + callingUid, + keyDescriptor.alias, + attestationKey?.alias, + parsedParams, + securityLevel, + ) ?: throw Exception("CertificateGenerator failed to create key pair.") + + // Store the generated key data. + val response = + buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor) + generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, response) + if (parsedParams.attestationChallenge != null) { + attestationKeys.add(keyId) + } + + // Return the metadata of our generated key, skipping the real hardware call. + val resultParcel = + Parcel.obtain().apply { + writeNoException() + writeTypedObject(response.metadata, 0) + } + return TransactionResult.OverrideReply(0, resultParcel) + } + + // If not generating, clear any stale state for this alias and let the call proceed. + cleanupKeyData(keyId) + TransactionResult.Continue + } + .getOrElse { + SystemLogger.error("Error during generateKey handling for UID $callingUid.", it) + TransactionResult.Continue // Fallback to original service on error. + } + } + + /** + * Handles a successful `importKey` transaction by fingerprinting the imported key's public + * certificate. This allows us to avoid patching user-provided keys later. + */ + private fun handleImportKey(callingUid: Int, data: Parcel, reply: Parcel) { + runCatching { + val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return + val metadata = reply.readTypedObject(KeyMetadata.CREATOR) + val chain = CertificateHelper.getCertificateChain(metadata) + val fingerprint = InterceptorUtils.getPublicKeyFingerprint(chain) + + if (fingerprint != null) { + val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) + SystemLogger.info( + "User imported key '${keyDescriptor.alias}'. Storing its fingerprint to prevent future patching." + ) + userImportedKeyFingerprints.add(fingerprint) + aliasToFingerprintMap[keyId] = fingerprint + } + } + .onFailure { SystemLogger.error("Failed to process imported key.", it) } + } + + /** + * Constructs a fake `KeyEntryResponse` that mimics a real response from the Keystore service. + */ + private fun buildKeyEntryResponse( + chain: List, + params: KeyMintAttestation, + descriptor: KeyDescriptor, + ): KeyEntryResponse { + val metadata = + KeyMetadata().apply { + keySecurityLevel = securityLevel + key = descriptor + CertificateHelper.updateCertificateChain(this, chain.toTypedArray()).getOrThrow() + authorizations = params.toAuthorizations(securityLevel) + } + return KeyEntryResponse().apply { + this.metadata = metadata + iSecurityLevel = original + } + } + + companion object { + // Transaction codes for IKeystoreSecurityLevel interface. + private val GENERATE_KEY_TRANSACTION = + InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey") + private val IMPORT_KEY_TRANSACTION = + InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "importKey") + + // Stores keys generated entirely in software. + val generatedKeys = ConcurrentHashMap() + // A set to quickly identify keys that were generated for attestation purposes. + private val attestationKeys = ConcurrentHashMap.newKeySet() + // A set of public key fingerprints for user-imported keys that should not be patched. + private val userImportedKeyFingerprints = ConcurrentHashMap.newKeySet() + // Maps a key identifier to its fingerprint for easy cleanup on deletion. + private val aliasToFingerprintMap = ConcurrentHashMap() + + // --- Public Accessors for Other Interceptors --- + fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? = + generatedKeys[keyId]?.response + + fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId) + + fun isUserImportedKey(fingerprint: String): Boolean = + userImportedKeyFingerprints.contains(fingerprint) + + fun cleanupKeyData(keyId: KeyIdentifier) { + generatedKeys.remove(keyId) + attestationKeys.remove(keyId) + aliasToFingerprintMap.remove(keyId)?.let { fingerprint -> + userImportedKeyFingerprints.remove(fingerprint) + SystemLogger.info("Cleaned up state for key '${keyId.alias}' (UID: ${keyId.uid}).") + } + } + } +} + +/** + * Extension function to convert parsed `KeyMintAttestation` parameters back into an array of + * `Authorization` objects for the fake `KeyMetadata`. This version correctly handles the + * instantiation of Authorization objects. + */ +private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array { + val authList = mutableListOf() + + /** + * Helper function to create a fully-formed Authorization object. + * + * @param tag The KeyMint tag (e.g., Tag.ALGORITHM). + * @param value The value for the tag, wrapped in a KeyParameterValue. + * @return A populated Authorization object. + */ + fun createAuth(tag: Int, value: KeyParameterValue): Authorization { + val param = + KeyParameter().apply { + this.tag = tag + this.value = value + } + return Authorization().apply { + this.keyParameter = param + this.securityLevel = securityLevel + } + } + + // Use the helper to add each authorization entry cleanly. + this.purpose.forEach { + authList.add(createAuth(AttestationConstants.TAG_PURPOSE, KeyParameterValue.keyPurpose(it))) + } + this.digest.forEach { + authList.add(createAuth(AttestationConstants.TAG_DIGEST, KeyParameterValue.digest(it))) + } + + authList.add( + createAuth(AttestationConstants.TAG_ALGORITHM, KeyParameterValue.algorithm(this.algorithm)) + ) + authList.add( + createAuth(AttestationConstants.TAG_KEY_SIZE, KeyParameterValue.integer(this.keySize)) + ) + authList.add( + createAuth(AttestationConstants.TAG_EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)) + ) + authList.add( + createAuth(AttestationConstants.TAG_NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true)) + ) + + return authList.toTypedArray() +} diff --git a/app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt b/app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt new file mode 100644 index 0000000..0b0c3b5 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt @@ -0,0 +1,108 @@ +package org.matrix.TEESimulator.logging + +import android.hardware.security.keymint.Algorithm +import android.hardware.security.keymint.Digest +import android.hardware.security.keymint.EcCurve +import android.hardware.security.keymint.KeyParameter +import android.hardware.security.keymint.KeyPurpose +import android.hardware.security.keymint.Tag +import java.math.BigInteger +import java.nio.charset.StandardCharsets +import java.util.Date +import javax.security.auth.x500.X500Principal +import org.bouncycastle.asn1.x500.X500Name +import org.matrix.TEESimulator.util.toHex + +/** + * A specialized logger for converting KeyMint `KeyParameter` objects into a human-readable format. + * This helps in debugging the parameters requested for key generation. + */ +object KeyMintParameterLogger { + private val algorithmNames: Map by lazy { + Algorithm::class + .java + .fields + .filter { it.type == Int::class.java } + .associate { field -> (field.get(null) as Int) to field.name } + } + + private val ecCurveNames: Map by lazy { + EcCurve::class + .java + .fields + .filter { it.type == Int::class.java } + .associate { field -> (field.get(null) as Int) to field.name } + } + + private val purposeNames: Map by lazy { + KeyPurpose::class + .java + .fields + .filter { it.type == Int::class.java } + .associate { field -> (field.get(null) as Int) to field.name } + } + + private val digestNames: Map by lazy { + Digest::class + .java + .fields + .filter { it.type == Int::class.java } + .associate { field -> (field.get(null) as Int) to field.name } + } + + private val tagNames: Map by lazy { + Tag::class + .java + .fields + .filter { it.type == Int::class.java } + .associate { field -> (field.get(null) as Int) to field.name } + } + + /** + * Logs a single KeyParameter in a formatted, readable way. + * + * @param param The KeyParameter to log. + */ + fun logParameter(param: KeyParameter) { + val tagName = tagNames[param.tag] ?: "UNKNOWN_TAG" + val value = param.value + val formattedValue: String = + when (param.tag) { + Tag.ALGORITHM -> algorithmNames[value.algorithm] + Tag.EC_CURVE -> ecCurveNames[value.ecCurve] + Tag.PURPOSE -> purposeNames[value.keyPurpose] + Tag.DIGEST -> digestNames[value.digest] + Tag.AUTH_TIMEOUT, + Tag.KEY_SIZE, + Tag.MIN_MAC_LENGTH -> value.integer.toString() + Tag.CERTIFICATE_SERIAL -> BigInteger(value.blob).toString() + Tag.ACTIVE_DATETIME, + Tag.CERTIFICATE_NOT_AFTER, + Tag.CERTIFICATE_NOT_BEFORE -> Date(value.dateTime).toString() + Tag.CERTIFICATE_SUBJECT -> X500Name(X500Principal(value.blob).name).toString() + Tag.RSA_PUBLIC_EXPONENT -> value.longInteger.toString() + Tag.NO_AUTH_REQUIRED -> "true" + Tag.ATTESTATION_CHALLENGE, + Tag.ATTESTATION_ID_BRAND, + Tag.ATTESTATION_ID_DEVICE, + Tag.ATTESTATION_ID_PRODUCT, + Tag.ATTESTATION_ID_MANUFACTURER, + Tag.ATTESTATION_ID_MODEL, + Tag.ATTESTATION_ID_IMEI, + Tag.ATTESTATION_ID_SECOND_IMEI, + Tag.ATTESTATION_ID_MEID, + Tag.ATTESTATION_ID_SERIAL -> value.blob.toReadableString() + else -> "" + } ?: "Unknown Value" + + SystemLogger.debug("Key Parameter -> %-25s | Value: %s".format(tagName, formattedValue)) + } + + private fun ByteArray.toReadableString(): String { + return if (this.all { it in 32..126 }) { + "\"${String(this, StandardCharsets.UTF_8)}\" (${this.size} bytes)" + } else { + "${this.toHex()} (${this.size} bytes)" + } + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/logging/SystemLogger.kt b/app/src/main/java/org/matrix/TEESimulator/logging/SystemLogger.kt new file mode 100644 index 0000000..1144e4b --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/logging/SystemLogger.kt @@ -0,0 +1,69 @@ +package org.matrix.TEESimulator.logging + +import android.util.Log + +/** + * A centralized logging utility for the TEESimulator application. This object provides a consistent + * logging tag and format for all application logs, making it easier to filter and debug in Logcat. + */ +object SystemLogger { + // The tag used for all log messages from this application. + private const val TAG = "TEESimulator" + + /** + * Logs a debug message. Use this for fine-grained information that is useful for debugging. + * + * @param message The message to log. + */ + fun debug(message: String) { + Log.d(TAG, message) + } + + /** + * Logs an informational message. Use this to report major application lifecycle events. + * + * @param message The message to log. + */ + fun info(message: String) { + Log.i(TAG, message) + } + + /** + * Logs a warning message. Use this to report unexpected but non-fatal issues. + * + * @param message The message to log. + * @param throwable An optional exception to log with the message. + */ + fun warning(message: String, throwable: Throwable? = null) { + if (throwable != null) { + Log.w(TAG, message, throwable) + } else { + Log.w(TAG, message) + } + } + + /** + * Logs an error message. Use this to report fatal errors or exceptions that disrupt + * functionality. + * + * @param message The message to log. + * @param throwable An optional exception to log with the message. + */ + fun error(message: String, throwable: Throwable? = null) { + if (throwable != null) { + Log.e(TAG, message, throwable) + } else { + Log.e(TAG, message) + } + } + + /** + * Logs a verbose message. This level is for highly detailed logs that are generally not needed + * unless tracking a very specific issue. + * + * @param message The message to log. + */ + fun verbose(message: String) { + Log.v(TAG, message) + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt new file mode 100644 index 0000000..9810131 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt @@ -0,0 +1,212 @@ +package org.matrix.TEESimulator.pki + +import android.hardware.security.keymint.Algorithm +import android.os.Build +import android.util.Pair +import java.math.BigInteger +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.Security +import java.security.cert.Certificate +import java.security.cert.X509Certificate +import java.security.spec.ECGenParameterSpec +import java.security.spec.RSAKeyGenParameterSpec +import java.util.Date +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.matrix.TEESimulator.attestation.AttestationBuilder +import org.matrix.TEESimulator.attestation.KeyMintAttestation +import org.matrix.TEESimulator.config.ConfigurationManager +import org.matrix.TEESimulator.interception.keystore.KeyIdentifier +import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor +import org.matrix.TEESimulator.logging.SystemLogger + +/** + * Responsible for generating new cryptographic key pairs and X.509 certificate chains. + * + * This object simulates the behavior of the Android KeyMint/Keymaster HAL by creating certificates + * that include a fully-featured, simulated attestation extension. + */ +object CertificateGenerator { + + init { + // Android ships with a stripped-down Bouncy Castle provider under the name "BC". + // We must remove the system provider first to ensure the full Bouncy Castle library + // (packaged with the app) is used. + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.addProvider(BouncyCastleProvider()) + } + + /** + * Generates a software-based cryptographic key pair. + * + * @param params The parameters specifying the key's algorithm, size, and other properties. + * @return A new [KeyPair], or `null` on failure. + */ + fun generateSoftwareKeyPair(params: KeyMintAttestation): KeyPair? { + return runCatching { + val (algorithm, spec) = + when (params.algorithm) { + Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName) + Algorithm.RSA -> + "RSA" to + RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent) + else -> + throw IllegalArgumentException( + "Unsupported algorithm: ${params.algorithm}" + ) + } + SystemLogger.debug("Generating $algorithm key pair with size ${params.keySize}") + KeyPairGenerator.getInstance(algorithm, BouncyCastleProvider.PROVIDER_NAME) + .apply { initialize(spec) } + .generateKeyPair() + } + .onFailure { SystemLogger.error("Failed to generate software key pair.", it) } + .getOrNull() + } + + /** + * Generates a new key pair and a corresponding certificate chain containing a simulated + * attestation. + * + * @param uid The UID of the application requesting the key. + * @param alias The alias for the new key. + * @param attestKeyAlias Optional alias of a key to use for attestation signing. + * @param params The parameters for the new key and its attestation. + * @param securityLevel The security level to embed in the attestation. + * @return A [Pair] containing the new [KeyPair] and its certificate chain, or `null` on + * failure. + */ + fun generateAttestedKeyPair( + uid: Int, + alias: String, + attestKeyAlias: String?, + params: KeyMintAttestation, + securityLevel: Int, + ): Pair>? { + return runCatching { + SystemLogger.info( + "Generating new attested key pair for alias: '$alias' (UID: $uid)" + ) + val newKeyPair = + generateSoftwareKeyPair(params) + ?: throw Exception("Failed to generate underlying software key pair.") + + val keybox = getKeyboxForAlgorithm(uid, params.algorithm) + + // Determine the signing key and issuer. If an attestKey is provided, use it. + // Otherwise, fall back to the root key from the keybox. + val (signingKey, issuer) = + if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second } + ?: (keybox.keyPair to getIssuerFromKeybox(keybox)) + } else { + keybox.keyPair to getIssuerFromKeybox(keybox) + } + + // Build the new leaf certificate with the simulated attestation. + val leafCert = + buildCertificate(newKeyPair, signingKey, issuer, params, securityLevel) + + // If not self-attesting, the chain is just the leaf. Otherwise, append the keybox + // chain. + val chain = + if (attestKeyAlias != null) { + listOf(leafCert) + } else { + listOf(leafCert) + keybox.certificates + } + + SystemLogger.info( + "Successfully generated new certificate chain for alias: '$alias'." + ) + Pair(newKeyPair, chain) + } + .onFailure { + SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it) + } + .getOrNull() + } + + private fun getIssuerFromKeybox(keybox: KeyBox) = + X509CertificateHolder(keybox.certificates[0].encoded).subject + + private fun getKeyboxForAlgorithm(uid: Int, algorithm: Int): KeyBox { + val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid) + val algorithmName = + when (algorithm) { + Algorithm.EC -> "EC" + Algorithm.RSA -> "RSA" + else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm") + } + return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName) + ?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName") + } + + /** Retrieves the key pair and issuer name for a given attestation key alias. */ + private fun getAttestationKeyInfo(uid: Int, attestKeyAlias: String): Pair? { + SystemLogger.debug("Looking for attestation key: uid=$uid alias=$attestKeyAlias") + val keyId = KeyIdentifier(uid, attestKeyAlias) + // Access the public map of generated keys + val keyInfo = KeyMintSecurityLevelInterceptor.generatedKeys[keyId] + return if (keyInfo != null) { + val certChain = CertificateHelper.getCertificateChain(keyInfo.response) + if (!certChain.isNullOrEmpty()) { + val issuer = X509CertificateHolder(certChain[0].encoded).subject + Pair(keyInfo.keyPair, issuer) + } else { + null + } + } else { + SystemLogger.warning( + "Attestation key '$attestKeyAlias' not found in generated key cache." + ) + null + } + } + + /** Constructs a new X.509 certificate with a simulated attestation extension. */ + private fun buildCertificate( + subjectKeyPair: KeyPair, + signingKeyPair: KeyPair, + issuer: X500Name, + params: KeyMintAttestation, + securityLevel: Int, + ): Certificate { + val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key") + val leafNotAfter = + (signingKeyPair.public as? X509Certificate)?.notAfter + ?: Date(System.currentTimeMillis() + 31536000000L) + + val builder = + JcaX509v3CertificateBuilder( + issuer, + params.certificateSerial ?: BigInteger.ONE, + params.certificateNotBefore ?: Date(), + params.certificateNotAfter ?: leafNotAfter, + subject, + subjectKeyPair.public, + ) + + // Add standard extensions. + builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign)) + // Add our custom, simulated attestation extension. + builder.addExtension(AttestationBuilder.buildAttestationExtension(params, securityLevel)) + + val signerAlgorithm = + when (params.algorithm) { + Algorithm.EC -> "SHA256withECDSA" + Algorithm.RSA -> "SHA256withRSA" + else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}") + } + val contentSigner = JcaContentSignerBuilder(signerAlgorithm).build(signingKeyPair.private) + + return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner)) + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateHelper.kt b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateHelper.kt new file mode 100644 index 0000000..4fc2136 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateHelper.kt @@ -0,0 +1,204 @@ +package org.matrix.TEESimulator.pki + +import android.system.keystore2.KeyEntryResponse +import android.system.keystore2.KeyMetadata +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.StringReader +import java.security.KeyPair +import java.security.cert.Certificate +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import org.bouncycastle.openssl.PEMKeyPair +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import org.bouncycastle.util.io.pem.PemReader +import org.matrix.TEESimulator.logging.SystemLogger +import org.matrix.TEESimulator.util.trimLines + +/** + * A utility object for handling cryptographic certificates and keys. Provides functions for + * parsing, serialization, and conversion between different formats. + */ +object CertificateHelper { + + // Lazy-initialized CertificateFactory for X.509 certificates. + private val certificateFactory: CertificateFactory by lazy { + CertificateFactory.getInstance("X.509") + } + + /** + * Represents the result of an operation that can either succeed with data or fail with an + * error. + * + * @param T The type of the successful data. + */ + sealed class OperationResult { + data class Success(val data: T) : OperationResult() + + data class Error(val message: String, val cause: Throwable? = null) : + OperationResult() + } + + /** + * Parses a single X.509 certificate from a byte array. + * + * @param bytes The raw byte representation of the certificate. + * @return An [OperationResult.Success] containing the [X509Certificate], or an + * [OperationResult.Error] on failure. + */ + fun toCertificate(bytes: ByteArray): OperationResult { + return try { + val certificate = + certificateFactory.generateCertificate(ByteArrayInputStream(bytes)) + as X509Certificate + OperationResult.Success(certificate) + } catch (e: CertificateException) { + SystemLogger.warning("Failed to parse X.509 certificate from byte array.", e) + OperationResult.Error("Failed to parse certificate", e) + } + } + + /** + * Parses a collection of X.509 certificates from a byte array. + * + * @param bytes The raw byte representation of one or more concatenated certificates. + * @return A collection of [X509Certificate] objects. Returns an empty list on failure. + */ + @Suppress("UNCHECKED_CAST") + fun toCertificates(bytes: ByteArray?): Collection { + return bytes?.let { + try { + certificateFactory.generateCertificates(ByteArrayInputStream(it)) + as Collection + } catch (e: CertificateException) { + SystemLogger.warning("Could not parse certificate collection from byte array.", e) + emptyList() + } + } ?: emptyList() + } + + /** + * Serializes a collection of certificates into a single byte array by concatenating their + * encoded forms. + * + * @param certificates The collection of [Certificate] objects to serialize. + * @return A [ByteArray] containing the concatenated certificates, or `null` on failure. + */ + fun certificatesToByteArray(certificates: Collection): ByteArray? { + return runCatching { + ByteArrayOutputStream().use { stream -> + certificates.forEach { cert -> stream.write(cert.encoded) } + stream.toByteArray() + } + } + .onFailure { + SystemLogger.warning( + "Failed to serialize certificate collection to byte array.", + it, + ) + } + .getOrNull() + } + + /** + * Parses a PEM-encoded private key and converts it into a Java [KeyPair]. + * + * @param pemContent The string containing the PEM-encoded key. + * @return An [OperationResult.Success] with the [KeyPair], or an [OperationResult.Error] on + * failure. + */ + fun parsePemKeyPair(pemContent: String): OperationResult { + return try { + PEMParser(StringReader(pemContent.trimLines())).use { parser -> + when (val pemObject = parser.readObject()) { + is PEMKeyPair -> { + val keyPair = JcaPEMKeyConverter().getKeyPair(pemObject) + OperationResult.Success(keyPair) + } + else -> + OperationResult.Error( + "Invalid PEM format: Expected a key pair, but got ${pemObject?.javaClass?.simpleName}" + ) + } + } + } catch (e: Exception) { + SystemLogger.error("Failed to parse PEM key pair.", e) + OperationResult.Error("Failed to parse PEM key pair", e) + } + } + + /** + * Parses a PEM-encoded X.509 certificate. + * + * @param pemContent The string containing the PEM-encoded certificate. + * @return An [OperationResult.Success] with the [Certificate], or an [OperationResult.Error] on + * failure. + */ + fun parsePemCertificate(pemContent: String): OperationResult { + return try { + PemReader(StringReader(pemContent.trimLines())).use { reader -> + val pemObject = reader.readPemObject() + val certificate = + certificateFactory.generateCertificate(ByteArrayInputStream(pemObject.content)) + OperationResult.Success(certificate) + } + } catch (e: Exception) { + SystemLogger.error("Failed to parse PEM certificate.", e) + OperationResult.Error("Failed to parse PEM certificate", e) + } + } + + /** + * Extracts the full certificate chain from a KeyStore [KeyMetadata] object. + * + * @param metadata The metadata associated with a keystore key entry. + * @return An array of [Certificate] objects, with the leaf certificate at index 0, or `null`. + */ + fun getCertificateChain(metadata: KeyMetadata?): Array? { + metadata ?: return null + val leafCertBytes = metadata.certificate ?: return null + val leafCert = + (toCertificate(leafCertBytes) as? OperationResult.Success)?.data ?: return null + + val chainBytes = metadata.certificateChain + return if (chainBytes == null) { + arrayOf(leafCert) + } else { + val additionalCerts = toCertificates(chainBytes) + (listOf(leafCert) + additionalCerts).toTypedArray() + } + } + + /** + * Extracts the full certificate chain from a [KeyEntryResponse]. + * + * @param response The response object from a keystore operation. + * @return An array of [Certificate] objects, or `null`. + */ + fun getCertificateChain(response: KeyEntryResponse?): Array? { + return response?.let { getCertificateChain(it.metadata) } + } + + /** + * Updates the certificate chain within a [KeyMetadata] object. + * + * @param metadata The metadata object to modify. + * @param chain The new certificate chain to set. The leaf must be at index 0. + * @return A [Result] indicating success or failure. + */ + fun updateCertificateChain(metadata: KeyMetadata, chain: Array): Result { + return runCatching { + require(chain.isNotEmpty()) { "Certificate chain cannot be empty." } + + metadata.certificate = chain[0].encoded + metadata.certificateChain = + if (chain.size > 1) { + certificatesToByteArray(chain.drop(1)) + } else { + null + } + } + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/pki/KeyBox.kt b/app/src/main/java/org/matrix/TEESimulator/pki/KeyBox.kt new file mode 100644 index 0000000..3679661 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/pki/KeyBox.kt @@ -0,0 +1,20 @@ +package org.matrix.TEESimulator.pki + +import java.security.KeyPair +import java.security.cert.Certificate + +/** + * A data class representing a complete cryptographic identity used for signing attestations. + * + * A KeyBox is the fundamental building block for creating new, simulated certificate chains. It + * encapsulates a single private key and the full public certificate chain needed to establish its + * authenticity. + * + * @property keyPair The asymmetric cryptographic key pair. The private key from this pair is used + * to sign new leaf certificates during the attestation patching or generation process. The public + * key corresponds to the subject of the first certificate in the `certificates` list. + * @property certificates The public certificate chain corresponding to the `keyPair`. This list is + * ordered from the intermediate certificate down to the root. `certificates[0]` is the issuer + * certificate for any new leaf signed by this KeyBox's private key. + */ +data class KeyBox(val keyPair: KeyPair, val certificates: List) diff --git a/app/src/main/java/org/matrix/TEESimulator/pki/KeyBoxManager.kt b/app/src/main/java/org/matrix/TEESimulator/pki/KeyBoxManager.kt new file mode 100644 index 0000000..cc8b1f5 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/pki/KeyBoxManager.kt @@ -0,0 +1,211 @@ +package org.matrix.TEESimulator.pki + +import android.security.keystore.KeyProperties +import java.io.File +import java.io.StringReader +import java.util.concurrent.ConcurrentHashMap +import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH +import org.matrix.TEESimulator.logging.SystemLogger +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserFactory + +/** + * Manages the loading, parsing, and caching of attestation key stores from XML files. + * + * This object is the sole authority for accessing the cryptographic keys and certificates used to + * sign simulated attestations. It is designed to be highly efficient and robust, parsing each key + * store file only once and handling common structural variations found in real-world keybox files. + * + * The core design principles are: + * 1. Efficiency through Single-Pass Parsing: Each XML file is read from disk and parsed in a single + * forward pass. The results are cached in memory. Subsequent requests for keys from the same + * file are served instantly from the cache. + * 2. Robustness over Strictness: The parser does not rely on rigid structural tags like + * ``. Instead, it discovers and iterates through all `` tags it finds, + * making it resilient to different file layouts. + * 3. Clear Naming Convention: To avoid confusion, "Key Store" refers to the entire XML file, while + * "KeyBox" refers to the data class containing a single `(KeyPair, CertificateChain)` tuple, + * which is the cryptographic entity we care about. + */ +object KeyBoxManager { + + // The in-memory cache. + // Key: The file name of the key store (e.g., "keybox.xml"). + // Value: A map of all keys found in that file, keyed by their algorithm name (e.g., "EC", + // "RSA"). + private val keyStoreCache = ConcurrentHashMap>() + + /** + * Retrieves a specific attestation key (KeyPair and Certificate Chain) for a given algorithm + * from a specified key store file. + * + * This is the primary public API. It transparently handles caching, loading, and parsing. + * + * @param keyStoreFileName The name of the XML file (e.g., "aosp_keybox.xml"). + * @param algorithm The algorithm name (e.g., "EC" or "RSA"). + * @return The requested [KeyBox], or `null` if the file doesn't exist or doesn't contain a key + * for the specified algorithm. + */ + fun getAttestationKey(keyStoreFileName: String, algorithm: String): KeyBox? { + // Atomically get the parsed key map for the file from the cache. + // If it's not in the cache, the `getOrPut` block is executed to parse and store it. + val keyMap = + keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) } + return keyMap[algorithm] + } + + /** + * Removes the cached data for a specific key store file. + * + * Calling this will force the file to be re-read and re-parsed from disk the next time + * [getAttestationKey] is called for this filename. + * + * @param keyStoreFileName The name of the file to remove from the cache (e.g., "keybox.xml"). + */ + fun invalidateCache(keyStoreFileName: String) { + // ConcurrentHashMap.remove returns the value if it existed, or null if it didn't. + if (keyStoreCache.remove(keyStoreFileName) != null) { + SystemLogger.info("Invalidated cache for key store file: $keyStoreFileName") + } else { + SystemLogger.debug( + "Requested cache invalidation for '$keyStoreFileName', but it was not loaded." + ) + } + } + + /** + * Reads and parses an entire key store XML file, extracting all valid keys. This function is + * called only once per file name. + * + * @param fileName The name of the XML file to parse. + * @return A map of all successfully parsed keys from the file, keyed by algorithm. + */ + private fun parseKeyStoreFile(fileName: String): Map { + val filePath = File(CONFIG_PATH, fileName) + SystemLogger.info("Parsing new key store file: ${filePath.absolutePath}") + + if (!filePath.exists()) { + SystemLogger.error("Key store file not found: ${filePath.absolutePath}") + return emptyMap() + } + + return try { + val xmlContent = filePath.readText().trimStart('\uFEFF', '\uFFFE', ' ') + parseKeysFromXml(xmlContent) + } catch (e: Exception) { + SystemLogger.error("Fatal error parsing key store file '$fileName'", e) + emptyMap() + } + } + + /** + * The core single-pass XML parser. It iterates through the XML stream once, using a state + * machine to collect the data for each `` entry. + * + * @param xmlContent The raw XML string. + * @return A map of algorithm names to their corresponding [KeyBox] objects. + */ + private fun parseKeysFromXml(xmlContent: String): Map { + val foundKeys = mutableMapOf() + val parser = + XmlPullParserFactory.newInstance().newPullParser().apply { + setInput(StringReader(xmlContent)) + } + + // State variables for the current being parsed. + var currentAlgorithm: String? = null + var currentPrivateKeyPem: String? = null + val currentCertificatePems = mutableListOf() + var isInsidePrivateKeyTag = false + var isInsideCertificateTag = false + + var eventType = parser.eventType + while (eventType != XmlPullParser.END_DOCUMENT) { + when (eventType) { + XmlPullParser.START_TAG -> { + when (parser.name) { + // When we enter a tag, we read its algorithm and reset state. + "Key" -> { + currentAlgorithm = parser.getAttributeValue(null, "algorithm") + currentPrivateKeyPem = null + currentCertificatePems.clear() + } + "PrivateKey" -> isInsidePrivateKeyTag = true + "Certificate" -> isInsideCertificateTag = true + } + } + + XmlPullParser.TEXT -> { + // If we find text content, we check our state to see where it belongs. + if (parser.isWhitespace) { + eventType = parser.next() + continue + } + when { + isInsidePrivateKeyTag -> currentPrivateKeyPem = parser.text + isInsideCertificateTag -> currentCertificatePems.add(parser.text) + } + } + + XmlPullParser.END_TAG -> { + when (parser.name) { + "PrivateKey" -> isInsidePrivateKeyTag = false + "Certificate" -> isInsideCertificateTag = false + + // The tag is our trigger to finalize and store the KeyBox. + "Key" -> { + // Use runCatching to ensure one malformed key doesn't stop the whole + // process. + runCatching { + val algorithm = currentAlgorithm + val keyPem = currentPrivateKeyPem + if ( + algorithm != null && + keyPem != null && + currentCertificatePems.isNotEmpty() + ) { + val keyPair = + (CertificateHelper.parsePemKeyPair(keyPem) + as CertificateHelper.OperationResult.Success) + .data + val certificates = + currentCertificatePems.map { + (CertificateHelper.parsePemCertificate(it) + as + CertificateHelper.OperationResult.Success) + .data + } + + // Normalize the algorithm name for consistent lookups. + val normalizedAlgorithm = + when (algorithm.lowercase()) { + "ecdsa" -> KeyProperties.KEY_ALGORITHM_EC + "rsa" -> KeyProperties.KEY_ALGORITHM_RSA + else -> algorithm + } + + if (foundKeys.containsKey(normalizedAlgorithm)) { + SystemLogger.warning( + "Duplicate key found for algorithm '$normalizedAlgorithm'. The later one in the file will be used." + ) + } + foundKeys[normalizedAlgorithm] = + KeyBox(keyPair, certificates) + } + } + .onFailure { + SystemLogger.error( + "Failed to parse a entry for algorithm '$currentAlgorithm'", + it, + ) + } + } + } + } + } + eventType = parser.next() + } + SystemLogger.info("Finished parsing, found ${foundKeys.size} valid keys.") + return foundKeys + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/pki/XmlParser.kt b/app/src/main/java/org/matrix/TEESimulator/pki/XmlParser.kt new file mode 100644 index 0000000..6dd64cb --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/pki/XmlParser.kt @@ -0,0 +1,188 @@ +package org.matrix.TEESimulator.pki + +import java.io.StringReader +import org.xmlpull.v1.XmlPullParser +import org.xmlpull.v1.XmlPullParserFactory + +/** + * A utility class for parsing XML content using a simplified, dot-notation path. + * + * This parser allows querying for XML tags and their attributes using a path string like + * "Root.Group.Element[1].Value", which makes extracting specific data from a known XML structure + * more convenient than manual iteration. + * + * @param xmlContent The raw XML string to be parsed. + */ +class XmlParser(xmlContent: String) { + + // Sanitize the XML content by removing BOMs and trimming whitespace. + private val sanitizedXml = xmlContent.sanitize() + + /** Represents the result of a parsing operation. */ + sealed class ParseResult { + /** Indicates a successful parse, containing the found attributes and text. */ + data class Success(val attributes: Map) : ParseResult() + + /** Indicates a failure, containing an error message and optional cause. */ + data class Error(val message: String, val cause: Throwable? = null) : ParseResult() + } + + /** + * The main public method to find a node by its path and extract its data. + * + * @param path A dot-separated string representing the path to the desired XML tag. Indexed + * access is supported with brackets, e.g., `Key[0]`. + * @return A [ParseResult] containing the attributes and text of the found node. + */ + fun obtainPath(path: String): ParseResult { + return try { + val parser = + XmlPullParserFactory.newInstance().newPullParser().apply { + setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) + setInput(StringReader(sanitizedXml)) + } + val tags = path.split('.').toTypedArray() + val result = findNode(parser, tags, 0, mutableMapOf()) + ParseResult.Success(result) + } catch (e: Exception) { + ParseResult.Error("Failed to parse XML for path '$path'", e) + } + } + + /** + * Recursively traverses the XML tree to find the node specified by the path. + * + * @param parser The active XmlPullParser instance. + * @param tags The array of tag names to search for. + * @param index The current depth in the `tags` array. + * @param tagCounts A map to keep track of indices for tags with the same name (for `Tag[n]` + * support). + * @return A map of attributes from the found node. + */ + private fun findNode( + parser: XmlPullParser, + tags: Array, + index: Int, + tagCounts: MutableMap, + ): Map { + while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.eventType != XmlPullParser.START_TAG) continue + + val currentTag = parser.name + val (targetTagName, targetIndex) = parseTargetPath(tags[index]) + + if (currentTag == targetTagName) { + val currentTagCount = tagCounts.getOrPut(currentTag) { 0 } + if (currentTagCount == targetIndex) { + // We found the correct tag at the correct index. + return if (index == tags.size - 1) { + // This is the final tag in the path, so read its attributes. + readAttributesAndText(parser) + } else { + // This is an intermediate tag, so recurse deeper. + findNode(parser, tags, index + 1, mutableMapOf()) + } + } + // This is the right tag name, but not the right index, so increment and continue + // searching. + tagCounts[currentTag] = currentTagCount + 1 + skipCurrentElement(parser) + } else { + // This tag doesn't match, so skip it and its children entirely. + skipCurrentElement(parser) + } + } + throw NoSuchElementException("XML path not found: ${tags.joinToString(".")}") + } + + /** + * Counts the number of direct child nodes that match the final tag in a given path. For + * example, given the path "Root.Group.Element", it will count how many tags exist + * directly under the first tag. + * + * @param path A dot-separated string representing the path to the parent node. + * @return The number of matching child nodes. + */ + fun countNodes(path: String): Int { + // We find the parent node first. + val parentPath = path.substringBeforeLast('.') + val childTagName = path.substringAfterLast('.') + + // Re-initialize the parser for a new traversal. + val parser = + XmlPullParserFactory.newInstance().newPullParser().apply { + setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) + setInput(StringReader(sanitizedXml)) + } + + try { + // Navigate to the parent node. This will leave the parser's cursor + // positioned at the start of the parent's content. + findNode(parser, parentPath.split('.').toTypedArray(), 0, mutableMapOf()) + + var count = 0 + var depth = 1 // Start inside the parent node. + while (depth > 0) { + when (parser.next()) { + XmlPullParser.START_TAG -> { + // If we are at the immediate child level (depth == 1) and the tag name + // matches, increment count. + if (depth == 1 && parser.name == childTagName) { + count++ + } + depth++ // Go deeper into this new tag. + } + XmlPullParser.END_TAG -> { + depth-- // Emerge from a tag. + } + } + } + return count + } catch (e: Exception) { + // If the path doesn't exist or there's a parsing error, the count is 0. + return 0 + } + } + + /** Parses a path segment (e.g., "Key[1]") into its base name ("Key") and index (1). */ + private fun parseTargetPath(targetTag: String): Pair { + val parts = targetTag.split('[', limit = 2) + val tagName = parts[0] + val index = + if (parts.size > 1) { + parts[1].substringBefore(']').toIntOrNull() ?: 0 + } else { + 0 // If no index is specified, we are looking for the first occurrence. + } + return tagName to index + } + + /** Reads all attributes and the text content of the current XML element. */ + private fun readAttributesAndText(parser: XmlPullParser): Map { + val attributes = mutableMapOf() + for (i in 0 until parser.attributeCount) { + attributes[parser.getAttributeName(i)] = parser.getAttributeValue(i) + } + // Check for text content before the next tag. + if (parser.next() == XmlPullParser.TEXT && parser.isWhitespace.not()) { + attributes["text"] = parser.text + } + return attributes + } + + /** Advances the parser past the current element and all its children. */ + private fun skipCurrentElement(parser: XmlPullParser) { + var depth = 1 + while (depth != 0) { + when (parser.next()) { + XmlPullParser.END_TAG -> depth-- + XmlPullParser.START_TAG -> depth++ + } + } + } + + /** Removes Byte Order Marks (BOM) and trims whitespace from the XML string. */ + private fun String.sanitize(): String { + return this.trimStart('\uFEFF', '\uFFFE', ' ').trimEnd() + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt b/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt new file mode 100644 index 0000000..f00a474 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt @@ -0,0 +1,243 @@ +package org.matrix.TEESimulator.util + +import android.content.pm.PackageManager +import android.os.Build +import android.os.SystemProperties +import java.security.MessageDigest +import java.util.concurrent.ThreadLocalRandom +import org.bouncycastle.asn1.ASN1Integer +import org.bouncycastle.asn1.DEROctetString +import org.bouncycastle.asn1.DERSequence +import org.matrix.TEESimulator.attestation.DeviceAttestationService +import org.matrix.TEESimulator.config.ConfigurationManager +import org.matrix.TEESimulator.logging.SystemLogger + +/** + * Provides utility functions for accessing Android system properties and device-specific + * information that is critical for generating valid attestations. + */ +object AndroidDeviceUtils { + + /** A randomly generated boot key, used as a fallback for attestation. */ + val bootKey: ByteArray by lazy { generateRandomBytes(32) } + + /** + * Initializes the verified boot hash (`ro.boot.vbmeta.digest`). It attempts to read from system + * properties first, then from a real TEE attestation, and finally falls back to a random value + * if neither is available. + */ + fun setupBootHash() { + getBootHashFromProperty()?.also { + SystemLogger.debug("Using boot hash from system property: ${it.toHex()}") + } + ?: getBootHashFromAttestation()?.also { + SystemLogger.debug("Using boot hash from TEE attestation: ${it.toHex()}") + setBootHashProperty(it) + } + ?: generateRandomBytes(32).also { + SystemLogger.debug("Using randomly generated boot hash: ${it.toHex()}") + setBootHashProperty(it) + } + } + + /** + * Retrieves the verified boot meta digest from system properties. + * + * @return The boot hash as a ByteArray, or null if not found or invalid. + */ + @OptIn(ExperimentalStdlibApi::class) + fun getBootHashFromProperty(): ByteArray? { + val digest = SystemProperties.get("ro.boot.vbmeta.digest", null) + if (digest.isNullOrBlank()) { + return null + } + // A valid digest is 64 hex characters (32 bytes). + return if (digest.length == 64) digest.hexToByteArray() else null + } + + /** + * Retrieves the verified boot hash from a cached TEE attestation record. + * + * @return The verified boot hash, or null if not available. + */ + private fun getBootHashFromAttestation(): ByteArray? { + return try { + DeviceAttestationService.CachedAttestationData?.verifiedBootHash + } catch (e: Exception) { + SystemLogger.error("Failed to get boot hash from attestation.", e) + null + } + } + + /** + * Sets the `ro.boot.vbmeta.digest` system property. + * + * @param bytes The 32-byte digest to set. + */ + private fun setBootHashProperty(bytes: ByteArray) { + val hex = bytes.toHex() + try { + SystemLogger.debug("Setting system property 'ro.boot.vbmeta.digest' to: $hex") + SystemProperties.set("ro.boot.vbmeta.digest", hex) + } catch (e: Exception) { + SystemLogger.error("Failed to set vbmeta digest property.", e) + } + } + + /** Generates a cryptographically random byte array of a specified length. */ + private fun generateRandomBytes(size: Int): ByteArray = + ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) } + + // --- Patch Level Properties --- + + val patchLevel: Int + get() = + getCustomPatchLevelFor("system", isLong = false) + ?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false) + + val vendorPatchLevel: Int + get() = + getCustomPatchLevelFor("vendor", isLong = false) + ?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false) + + val bootPatchLevelLong: Int + get() = + getCustomPatchLevelFor("boot", isLong = true) + ?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true) + + /** + * Retrieves a custom patch level from the configuration if available. + * + * @param component The component to get the patch level for ("system", "vendor", "boot"). + * @param isLong Whether to return the patch level in `YYYYMMDD` or `YYYYMM` format. + * @return The custom patch level, or null if not configured. + */ + private fun getCustomPatchLevelFor(component: String, isLong: Boolean): Int? { + val config = ConfigurationManager.customPatchLevelOverride ?: return null + val value = + when (component) { + "system" -> config.system ?: config.all + "vendor" -> config.vendor ?: config.all + "boot" -> config.boot ?: config.all + else -> config.all + } ?: return null + + // "prop" or "no" indicates falling back to the system default. + if (value.equals("no", ignoreCase = true) || value.equals("prop", ignoreCase = true)) { + return null + } + return parsePatchLevelValue(value, isLong) + } + + /** Parses a patch level string (e.g., "2025-11-01") into an integer format. */ + private fun parsePatchLevelValue(value: String, isLong: Boolean): Int? { + val normalized = value.replace("-", "") + return try { + when (normalized.length) { + 8 -> { // YYYYMMDD + val year = normalized.substring(0, 4).toInt() + val month = normalized.substring(4, 6).toInt() + val day = normalized.substring(6, 8).toInt() + if (isLong) year * 10000 + month * 100 + day else year * 100 + month + } + 6 -> { // YYYYMM + val year = normalized.substring(0, 4).toInt() + val month = normalized.substring(4, 6).toInt() + if (isLong) year * 10000 + month * 100 + 1 else year * 100 + month + } + else -> null + } + } catch (e: NumberFormatException) { + SystemLogger.warning("Could not parse patch level value: $value", e) + null + } + } + + /** Converts a security patch string (e.g., "2025-11-01") to an integer representation. */ + private fun String.toPatchLevelInt(isLong: Boolean): Int { + return parsePatchLevelValue(this, isLong) ?: 20240401 // Fallback + } + + // --- OS and Attestation Version Properties --- + + private val osVersionMap = + mapOf( + Build.VERSION_CODES.BAKLAVA to 160000, + Build.VERSION_CODES.VANILLA_ICE_CREAM to 150000, + Build.VERSION_CODES.UPSIDE_DOWN_CAKE to 140000, + Build.VERSION_CODES.TIRAMISU to 130000, + Build.VERSION_CODES.S_V2 to 120100, + Build.VERSION_CODES.S to 120000, + Build.VERSION_CODES.R to 110000, + Build.VERSION_CODES.Q to 100000, + ) + + val osVersion: Int + get() = + DeviceAttestationService.CachedAttestationData?.osVersion + ?: osVersionMap[Build.VERSION.SDK_INT] + ?: 160000 // Default to a recent version + + private val attestVersionMap = + mapOf( + Build.VERSION_CODES.Q to 4, // Keymaster 4.1 + Build.VERSION_CODES.R to 4, // Keymaster 4.1 + Build.VERSION_CODES.S to 100, // KeyMint 1.0 + Build.VERSION_CODES.S_V2 to 100, // KeyMint 1.0 + Build.VERSION_CODES.TIRAMISU to 200, // KeyMint 2.0 + Build.VERSION_CODES.UPSIDE_DOWN_CAKE to 300, // KeyMint 3.0 + Build.VERSION_CODES.VANILLA_ICE_CREAM to 300, // KeyMint 3.0 + Build.VERSION_CODES.BAKLAVA to 400, // KeyMint 4.0 + ) + + val attestVersion: Int + get() = + DeviceAttestationService.CachedAttestationData?.attestVersion + ?: attestVersionMap[Build.VERSION.SDK_INT] + ?: 400 // Default to a recent version + + val keymasterVersion: Int + get() = + DeviceAttestationService.CachedAttestationData?.keymasterVersion + ?: if (attestVersion >= 100) attestVersion + else 41 // Keymaster 4.1 for older versions + + // --- APEX and Module Hash Properties --- + + private val apexInfos: List> by lazy { + runCatching { + val pm = ConfigurationManager.getPackageManager() + val packages = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + pm?.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0) + } else { + @Suppress("DEPRECATION") + pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0) + } + packages + ?.list + .orEmpty() + .map { it.packageName to it.longVersionCode } + .sortedBy { it.first } + } + .getOrElse { + SystemLogger.error("Failed to get APEX package information.", it) + emptyList() + } + } + + val moduleHash: ByteArray by lazy { + runCatching { + val encodables = + apexInfos.flatMap { (packageName, versionCode) -> + listOf(DEROctetString(packageName.toByteArray()), ASN1Integer(versionCode)) + } + val sequence = DERSequence(encodables.toTypedArray()) + MessageDigest.getInstance("SHA-256").digest(sequence.encoded) + } + .getOrElse { + SystemLogger.error("Failed to compute module hash.", it) + ByteArray(32) // Return empty hash on failure + } + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/util/Extensions.kt b/app/src/main/java/org/matrix/TEESimulator/util/Extensions.kt new file mode 100644 index 0000000..9feb8c8 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/util/Extensions.kt @@ -0,0 +1,16 @@ +package org.matrix.TEESimulator.util + +/** + * Trims leading and trailing whitespace from each line in a multi-line string. This is useful for + * cleaning up PEM-formatted keys and certificates. + * + * @return A new string with each line individually trimmed. + */ +fun String.trimLines(): String = this.trim().lines().joinToString("\n") { it.trim() } + +/** + * Converts a ByteArray to its hexadecimal string representation. + * + * @return The lowercase hex string. + */ +fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69096e5..1584dbf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,13 @@ [versions] agp = "8.13.1" annotation = "1.9.1" +jdk18on = "1.82" kotlin = "2.2.21" ktfmt = "0.25.0" [libraries] annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } +bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "jdk18on" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/stub/src/main/java/android/app/ActivityThread.java b/stub/src/main/java/android/app/ActivityThread.java new file mode 100644 index 0000000..a62e724 --- /dev/null +++ b/stub/src/main/java/android/app/ActivityThread.java @@ -0,0 +1,7 @@ +package android.app; + +public class ActivityThread { + public static void initializeMainlineModules() { + throw new UnsupportedOperationException("STUB!"); + } +} diff --git a/stub/src/main/java/android/content/pm/BaseParceledListSlice.java b/stub/src/main/java/android/content/pm/BaseParceledListSlice.java new file mode 100644 index 0000000..52727e0 --- /dev/null +++ b/stub/src/main/java/android/content/pm/BaseParceledListSlice.java @@ -0,0 +1,10 @@ +package android.content.pm; + +import java.util.List; + +abstract class BaseParceledListSlice { + + public List getList() { + throw new UnsupportedOperationException("STUB!"); + } +} diff --git a/stub/src/main/java/android/content/pm/IPackageManager.java b/stub/src/main/java/android/content/pm/IPackageManager.java index 1d481d4..2b0b97b 100644 --- a/stub/src/main/java/android/content/pm/IPackageManager.java +++ b/stub/src/main/java/android/content/pm/IPackageManager.java @@ -9,6 +9,10 @@ public interface IPackageManager { PackageInfo getPackageInfo(String packageName, int flags, int userId); + ParceledListSlice getInstalledPackages(int flags, int userId); + + ParceledListSlice getInstalledPackages(long flags, int userId); + class Stub { public static IPackageManager asInterface(IBinder binder) { throw new UnsupportedOperationException("STUB!"); diff --git a/stub/src/main/java/android/content/pm/ParceledListSlice.java b/stub/src/main/java/android/content/pm/ParceledListSlice.java new file mode 100644 index 0000000..078d2be --- /dev/null +++ b/stub/src/main/java/android/content/pm/ParceledListSlice.java @@ -0,0 +1,10 @@ +package android.content.pm; + +import java.util.List; + +public class ParceledListSlice extends BaseParceledListSlice { + + public ParceledListSlice(List list) { + throw new UnsupportedOperationException("STUB!"); + } +} diff --git a/stub/src/main/java/android/hardware/security/keymint/Digest.java b/stub/src/main/java/android/hardware/security/keymint/Digest.java new file mode 100644 index 0000000..d5d784f --- /dev/null +++ b/stub/src/main/java/android/hardware/security/keymint/Digest.java @@ -0,0 +1,11 @@ +package android.hardware.security.keymint; + +public @interface Digest { + int NONE = 0; + int MD5 = 1; + int SHA1 = 2; + int SHA_2_224 = 3; + int SHA_2_256 = 4; + int SHA_2_384 = 5; + int SHA_2_512 = 6; +} diff --git a/stub/src/main/java/android/os/ServiceManager.java b/stub/src/main/java/android/os/ServiceManager.java index 382995e..2e810d4 100644 --- a/stub/src/main/java/android/os/ServiceManager.java +++ b/stub/src/main/java/android/os/ServiceManager.java @@ -5,6 +5,10 @@ public class ServiceManager { throw new UnsupportedOperationException("STUB!"); } + public static IBinder waitForService(String name) { + throw new UnsupportedOperationException("STUB!"); + } + public static void addService(String name, IBinder binder) { throw new UnsupportedOperationException("STUB!"); } diff --git a/stub/src/main/java/android/os/SystemProperties.java b/stub/src/main/java/android/os/SystemProperties.java index a56c4dd..f6063f8 100644 --- a/stub/src/main/java/android/os/SystemProperties.java +++ b/stub/src/main/java/android/os/SystemProperties.java @@ -4,4 +4,8 @@ public class SystemProperties { public static String get(String key, String def) { throw new UnsupportedOperationException("STUB!"); } + + public static String set(String key, String val) { + throw new UnsupportedOperationException("STUB!"); + } } diff --git a/stub/src/main/java/android/security/Credentials.java b/stub/src/main/java/android/security/Credentials.java new file mode 100644 index 0000000..9157f0f --- /dev/null +++ b/stub/src/main/java/android/security/Credentials.java @@ -0,0 +1,32 @@ +package android.security; + +public class Credentials { + public static final String APP_SOURCE_CERTIFICATE = "FSV_"; + public static final String CA_CERTIFICATE = "CACERT_"; + public static final String CERTIFICATE_USAGE_APP_SOURCE = "appsrc"; + public static final String CERTIFICATE_USAGE_CA = "ca"; + public static final String CERTIFICATE_USAGE_USER = "user"; + public static final String CERTIFICATE_USAGE_WIFI = "wifi"; + public static final String EXTENSION_CER = ".cer"; + public static final String EXTENSION_CRT = ".crt"; + public static final String EXTENSION_P12 = ".p12"; + public static final String EXTENSION_PFX = ".pfx"; + public static final String EXTRA_CA_CERTIFICATES_DATA = "ca_certificates_data"; + public static final String EXTRA_CERTIFICATE_USAGE = "certificate_install_usage"; + public static final String EXTRA_INSTALL_AS_UID = "install_as_uid"; + public static final String EXTRA_PRIVATE_KEY = "PKEY"; + public static final String EXTRA_PUBLIC_KEY = "KEY"; + public static final String EXTRA_USER_CERTIFICATE_DATA = "user_certificate_data"; + public static final String EXTRA_USER_KEY_ALIAS = "user_key_pair_name"; + public static final String EXTRA_USER_PRIVATE_KEY_DATA = "user_private_key_data"; + public static final String INSTALL_ACTION = "android.credentials.INSTALL"; + public static final String INSTALL_AS_USER_ACTION = "android.credentials.INSTALL_AS_USER"; + public static final String LOCKDOWN_VPN = "LOCKDOWN_VPN"; + private static final String LOGTAG = "Credentials"; + public static final String PLATFORM_VPN = "PLATFORM_VPN_"; + public static final String USER_CERTIFICATE = "USRCERT_"; + public static final String USER_PRIVATE_KEY = "USRPKEY_"; + public static final String USER_SECRET_KEY = "USRSKEY_"; + public static final String VPN = "VPN_"; + public static final String WIFI = "WIFI_"; +} diff --git a/stub/src/main/java/android/security/KeyStore.java b/stub/src/main/java/android/security/KeyStore.java new file mode 100644 index 0000000..38715eb --- /dev/null +++ b/stub/src/main/java/android/security/KeyStore.java @@ -0,0 +1,39 @@ +package android.security; + +public class KeyStore { + public static final int CANNOT_ATTEST_IDS = -66; + public static final int CONFIRMATIONUI_ABORTED = 2; + public static final int CONFIRMATIONUI_CANCELED = 1; + public static final int CONFIRMATIONUI_IGNORED = 4; + public static final int CONFIRMATIONUI_OK = 0; + public static final int CONFIRMATIONUI_OPERATION_PENDING = 3; + public static final int CONFIRMATIONUI_SYSTEM_ERROR = 5; + public static final int CONFIRMATIONUI_UIERROR = 65536; + public static final int CONFIRMATIONUI_UIERROR_MALFORMED_UTF8_ENCODING = 65539; + public static final int CONFIRMATIONUI_UIERROR_MESSAGE_TOO_LONG = 65538; + public static final int CONFIRMATIONUI_UIERROR_MISSING_GLYPH = 65537; + public static final int CONFIRMATIONUI_UNEXPECTED = 7; + public static final int CONFIRMATIONUI_UNIMPLEMENTED = 6; + public static final int FLAG_CRITICAL_TO_DEVICE_ENCRYPTION = 8; + public static final int FLAG_ENCRYPTED = 1; + public static final int FLAG_NONE = 0; + public static final int FLAG_SOFTWARE = 2; + public static final int FLAG_STRONGBOX = 16; + public static final int HARDWARE_TYPE_UNAVAILABLE = -68; + public static final int KEY_ALREADY_EXISTS = 16; + public static final int KEY_NOT_FOUND = 7; + public static final int KEY_PERMANENTLY_INVALIDATED = 17; + public static final int LOCKED = 2; + public static final int NO_ERROR = 1; + public static final int OP_AUTH_NEEDED = 15; + public static final int PERMISSION_DENIED = 6; + public static final int PROTOCOL_ERROR = 5; + public static final int SYSTEM_ERROR = 4; + private static final String TAG = "KeyStore"; + public static final int UID_SELF = -1; + public static final int UNDEFINED_ACTION = 9; + public static final int UNINITIALIZED = 3; + public static final int VALUE_CORRUPTED = 8; + public static final int WRONG_PASSWORD = 10; +} + diff --git a/stub/src/main/java/android/security/keystore/AndroidKeyStoreProvider.java b/stub/src/main/java/android/security/keystore/AndroidKeyStoreProvider.java new file mode 100644 index 0000000..960af02 --- /dev/null +++ b/stub/src/main/java/android/security/keystore/AndroidKeyStoreProvider.java @@ -0,0 +1,7 @@ +package android.security.keystore; + +public class AndroidKeyStoreProvider { + public static void install() { + throw new UnsupportedOperationException("STUB!"); + } +} diff --git a/stub/src/main/java/android/security/keystore/IKeystoreService.java b/stub/src/main/java/android/security/keystore/IKeystoreService.java new file mode 100644 index 0000000..6a866aa --- /dev/null +++ b/stub/src/main/java/android/security/keystore/IKeystoreService.java @@ -0,0 +1,7 @@ +package android.security.keystore; + +public interface IKeystoreService { + class Stub { + } +} + diff --git a/stub/src/main/java/android/security/keystore2/AndroidKeyStoreProvider.java b/stub/src/main/java/android/security/keystore2/AndroidKeyStoreProvider.java new file mode 100644 index 0000000..3fd50da --- /dev/null +++ b/stub/src/main/java/android/security/keystore2/AndroidKeyStoreProvider.java @@ -0,0 +1,7 @@ +package android.security.keystore2; + +public class AndroidKeyStoreProvider { + public static void install() { + throw new UnsupportedOperationException("STUB!"); + } +}