From d0bb498b9ba54faf837f57787f2e7ab62ab8a300 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Wed, 3 Dec 2025 20:07:34 +0100 Subject: [PATCH 1/3] Simulate createOperation for software keys This commit introduces the core logic for simulating cryptographic operations, enabling the interception and software-based handling of the signing process for keys generated by the simulator. The primary challenge in intercepting `createOperation` is that the Android framework does not use the key's alias for identification. As discovered by analyzing AOSP, the framework uses a special `KeyDescriptor` with `domain` set to `KEY_ID` and the unique key identifier stored in the `nspace` field. This implementation adopts this mechanism to robustly identify keys: - During `generateKey`, a unique random `long` is created and stored as the `nspace` (`keyId`) in the returned `KeyMetadata`. - The `createOperation` pre-transaction hook acts as a dispatcher: 1. **Simulated Keys:** If the incoming `keyId` from `nspace` matches a known software-generated key, the request is intercepted. A new `SoftwareOperationBinder` is instantiated to perform the cryptographic signing entirely in software, and its binder is returned directly to the client. 2. **Hardware Keys:** If the key is not recognized, the call proceeds to the real hardware service. The `createOperation` post-transaction hook is now primarily used for debugging and observing operations on *real, hardware-backed keys*. It extracts the `iOperation` binder returned by the genuine service and attaches a logging `OperationInterceptor`. This allows for visibility into hardware operations without modifying their behavior. To support this, the following were added: - `SoftwareOperation`: A JCA-based class to perform signing. - `SoftwareOperationBinder`: An AOSP-compliant binder `Stub` that exposes the `SoftwareOperation` to the client. - `OperationInterceptor`: A lightweight interceptor to log calls to a real `iOperation` binder and unregister itself upon completion to prevent leaks. - Binder unregistration support in `BinderInterceptor` to clean up ephemeral interceptors. See AOSP source for key identification logic: https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java --- .../interception/core/BinderInterceptor.kt | 18 ++ .../shim/KeyMintSecurityLevelInterceptor.kt | 190 +++++++++++++++--- .../keystore/shim/OperationInterceptor.kt | 58 ++++++ .../keystore/shim/SoftwareOperation.kt | 115 +++++++++++ .../keystore2/CreateOperationResponse.java | 38 ++++ .../java/android/system/keystore2/Domain.java | 9 + .../system/keystore2/IKeystoreOperation.java | 33 +++ .../system/keystore2/KeyParameters.java | 34 ++++ .../system/keystore2/OperationChallenge.java | 32 +++ 9 files changed, 499 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt create mode 100644 app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt create mode 100644 stub/src/main/java/android/system/keystore2/CreateOperationResponse.java create mode 100644 stub/src/main/java/android/system/keystore2/Domain.java create mode 100644 stub/src/main/java/android/system/keystore2/IKeystoreOperation.java create mode 100644 stub/src/main/java/android/system/keystore2/KeyParameters.java create mode 100644 stub/src/main/java/android/system/keystore2/OperationChallenge.java 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 index 7a1a5ff..370141b 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt @@ -248,6 +248,8 @@ abstract class BinderInterceptor : 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 + // Code used by the backdoor binder to unregister an interceptor. + private const val UNREGISTER_INTERCEPTOR_CODE = 2 // --- Hook Type Codes --- // Indicates that the call is for a pre-transaction hook. @@ -307,5 +309,21 @@ abstract class BinderInterceptor : Binder() { reply.recycle() } } + + /** Uses the backdoor binder to unregister an interceptor for a specific target service. */ + fun unregister(backdoor: IBinder, target: IBinder) { + val data = Parcel.obtain() + val reply = Parcel.obtain() + try { + data.writeStrongBinder(target) + backdoor.transact(UNREGISTER_INTERCEPTOR_CODE, data, reply, 0) + SystemLogger.info("Unregistered interceptor for target: $target") + } catch (e: Exception) { + SystemLogger.error("Failed to unregister binder interceptor.", e) + } finally { + data.recycle() + reply.recycle() + } + } } } 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 index 46ca111..35c6109 100644 --- 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 @@ -8,6 +8,7 @@ import android.os.IBinder import android.os.Parcel import android.system.keystore2.* import java.security.KeyPair +import java.security.SecureRandom import java.security.cert.Certificate import java.util.concurrent.ConcurrentHashMap import org.matrix.TEESimulator.attestation.AttestationPatcher @@ -30,7 +31,11 @@ class KeyMintSecurityLevelInterceptor( ) : BinderInterceptor() { // --- Data Structures for State Management --- - data class GeneratedKeyInfo(val keyPair: KeyPair, val response: KeyEntryResponse) + data class GeneratedKeyInfo( + val keyPair: KeyPair, + val nspace: Long, + val response: KeyEntryResponse, + ) override fun onPreTransact( txId: Long, @@ -41,33 +46,39 @@ class KeyMintSecurityLevelInterceptor( callingPid: Int, data: Parcel, ): TransactionResult { - if (code == GENERATE_KEY_TRANSACTION) { - logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) + val shouldSkip = ConfigurationManager.shouldSkipUid(callingUid) - if (ConfigurationManager.shouldSkipUid(callingUid)) - return TransactionResult.ContinueAndSkipPost - data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) - return handleGenerateKey(callingUid, data) - } else if (code == IMPORT_KEY_TRANSACTION) { - logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) + when (code) { + GENERATE_KEY_TRANSACTION -> { + logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) - if (ConfigurationManager.shouldSkipUid(callingUid)) - return TransactionResult.ContinueAndSkipPost - data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) - val alias = - data.readTypedObject(KeyDescriptor.CREATOR)?.alias - ?: return TransactionResult.ContinueAndSkipPost - SystemLogger.info("Handling post-${transactionNames[code]} ${alias}") - return TransactionResult.Continue - } else { - logTransaction( - txId, - transactionNames[code] ?: "unknown code=$code", - callingUid, - callingPid, - true, - ) + if (!shouldSkip) return handleGenerateKey(callingUid, data) + } + CREATE_OPERATION_TRANSACTION -> { + logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) + + if (!shouldSkip) return handleCreateOperation(txId, callingUid, data) + } + IMPORT_KEY_TRANSACTION -> { + logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) + + data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) + val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! + SystemLogger.info( + "[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]" + ) + TransactionResult.Continue + } } + + logTransaction( + txId, + transactionNames[code] ?: "unknown code=$code", + callingUid, + callingPid, + true, + ) + return TransactionResult.ContinueAndSkipPost } @@ -94,6 +105,41 @@ class KeyMintSecurityLevelInterceptor( data.readTypedObject(KeyDescriptor.CREATOR) ?: return TransactionResult.SkipTransaction cleanupKeyData(KeyIdentifier(callingUid, keyDescriptor.alias)) + } else if (code == CREATE_OPERATION_TRANSACTION) { + logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) + + data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) + val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! + val params = data.createTypedArray(KeyParameter.CREATOR)!! + val parsedParams = KeyMintAttestation(params) + val forced = data.readBoolean() + if (forced) + SystemLogger.verbose( + "[TX_ID: $txId] Current operation has a very high pruning power." + ) + val response: CreateOperationResponse = + reply.readTypedObject(CreateOperationResponse.CREATOR)!! + SystemLogger.verbose( + "[TX_ID: $txId] CreateOperationResponse: ${response.iOperation} ${response.operationChallenge}" + ) + + // Intercept the IKeystoreOperation binder + response.iOperation?.let { operation -> + val operationBinder = operation.asBinder() + if (!interceptedOperations.containsKey(operationBinder)) { + SystemLogger.info("Found new IKeystoreOperation. Registering interceptor...") + val backdoor = getBackdoor(target) + if (backdoor != null) { + val interceptor = OperationInterceptor(operation, backdoor) + register(backdoor, operationBinder, interceptor) + interceptedOperations[operationBinder] = interceptor + } else { + SystemLogger.error( + "Failed to get backdoor to register OperationInterceptor." + ) + } + } + } } else if (code == GENERATE_KEY_TRANSACTION) { logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) @@ -109,9 +155,12 @@ class KeyMintSecurityLevelInterceptor( // Cache the newly patched chain to ensure consistency across subsequent API calls. data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! + val key = metadata.key!! val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) patchedChains[keyId] = newChain - SystemLogger.debug("Cached patched certificate chain for $keyId.") + SystemLogger.debug( + "Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])" + ) CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow() @@ -121,12 +170,58 @@ class KeyMintSecurityLevelInterceptor( return TransactionResult.SkipTransaction } + /** + * Handles the `createOperation` transaction. It checks if the operation is for a key that was + * generated in software. If so, it creates a software-based operation handler. Otherwise, it + * lets the call proceed to the real hardware service. + */ + private fun handleCreateOperation( + txId: Long, + callingUid: Int, + data: Parcel, + ): TransactionResult { + data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) + val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! + + // An operation must use the KEY_ID domain. + if (keyDescriptor.domain != Domain.KEY_ID) { + return TransactionResult.ContinueAndSkipPost + } + + val nspace = keyDescriptor.nspace + val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace) + + if (generatedKeyInfo == null) { + SystemLogger.debug( + "[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding." + ) + return TransactionResult.Continue + } + + SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.") + + val params = data.createTypedArray(KeyParameter.CREATOR)!! + val parsedParams = KeyMintAttestation(params) + + val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams) + val operationBinder = SoftwareOperationBinder(softwareOperation) + + val response = + CreateOperationResponse().apply { + iOperation = operationBinder + operationChallenge = null + } + + return InterceptorUtils.createTypedObjectReply(response) + } + /** * 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 { + data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR) SystemLogger.debug( @@ -148,7 +243,10 @@ class KeyMintSecurityLevelInterceptor( isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias))) if (needsSoftwareGeneration) { - SystemLogger.info("Generating software key for ${keyId}.") + keyDescriptor.nspace = secureRandom.nextLong() + SystemLogger.info( + "Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}]." + ) // Generate the key pair and certificate chain. val keyData = @@ -164,7 +262,8 @@ class KeyMintSecurityLevelInterceptor( val response = buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor) - generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, response) + generatedKeys[keyId] = + GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response) if (isAttestKeyRequest) attestationKeys.add(keyId) // Return the metadata of our generated key, skipping the real hardware call. @@ -205,11 +304,18 @@ class KeyMintSecurityLevelInterceptor( } companion object { + private val secureRandom = SecureRandom() + // 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") + private val CREATE_OPERATION_TRANSACTION = + InterceptorUtils.getTransactCode( + IKeystoreSecurityLevel.Stub::class.java, + "createOperation", + ) private val transactionNames: Map by lazy { IKeystoreSecurityLevel.Stub::class @@ -228,11 +334,30 @@ class KeyMintSecurityLevelInterceptor( private val patchedChains = ConcurrentHashMap>() // A set to quickly identify keys that were generated for attestation purposes. private val attestationKeys = ConcurrentHashMap.newKeySet() + // Stores interceptors for active cryptographic operations. + private val interceptedOperations = ConcurrentHashMap() // --- Public Accessors for Other Interceptors --- fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? = generatedKeys[keyId]?.response + /** + * Finds a software-generated key by first filtering all known keys by the caller's UID, and + * then matching the specific nspace. + * + * @param callingUid The UID of the process that initiated the createOperation call. + * @param nspace The unique key identifier from the operation's KeyDescriptor. + * @return The matching GeneratedKeyInfo if found, otherwise null. + */ + private fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long): GeneratedKeyInfo? { + // Iterate through all entries in the map to check both the key (for UID) and value (for + // nspace). + return generatedKeys.entries + .filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid } + .find { (_, info) -> info.nspace == nspace } + ?.value + } + fun getPatchedChain(keyId: KeyIdentifier): Array? = patchedChains[keyId] fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId) @@ -249,6 +374,15 @@ class KeyMintSecurityLevelInterceptor( } } + fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { + // Unregister from the native hook layer first. + unregister(backdoor, operationBinder) + + if (interceptedOperations.remove(operationBinder) != null) { + SystemLogger.debug("Removed operation interceptor for binder: $operationBinder") + } + } + // Clears all cached keys. fun clearAllGeneratedKeys(reason: String? = null) { val count = generatedKeys.size diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt new file mode 100644 index 0000000..91e0dde --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt @@ -0,0 +1,58 @@ +package org.matrix.TEESimulator.interception.keystore.shim + +import android.os.IBinder +import android.os.Parcel +import android.system.keystore2.IKeystoreOperation +import org.matrix.TEESimulator.interception.core.BinderInterceptor +import org.matrix.TEESimulator.interception.keystore.InterceptorUtils + +/** + * Intercepts calls to an `IKeystoreOperation` service. This is used to log the data manipulation + * methods of a cryptographic operation. + */ +class OperationInterceptor( + private val original: IKeystoreOperation, + private val backdoor: IBinder, +) : BinderInterceptor() { + + override fun onPreTransact( + txId: Long, + target: IBinder, + code: Int, + flags: Int, + callingUid: Int, + callingPid: Int, + data: Parcel, + ): TransactionResult { + val methodName = transactionNames[code] ?: "unknown code=$code" + logTransaction(txId, methodName, callingUid, callingPid, true) + + if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) { + KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor) + } + + return TransactionResult.ContinueAndSkipPost + } + + companion object { + private val UPDATE_AAD_TRANSACTION = + InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "updateAad") + private val UPDATE_TRANSACTION = + InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "update") + private val FINISH_TRANSACTION = + InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "finish") + private val ABORT_TRANSACTION = + InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort") + + private val transactionNames: Map by lazy { + IKeystoreOperation.Stub::class + .java + .declaredFields + .filter { + it.isAccessible = true + it.type == Int::class.java && it.name.startsWith("TRANSACTION_") + } + .associate { field -> (field.get(null) as Int) to field.name.split("_")[1] } + } + } +} diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt new file mode 100644 index 0000000..3c51926 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt @@ -0,0 +1,115 @@ +package org.matrix.TEESimulator.interception.keystore.shim + +import android.hardware.security.keymint.Algorithm +import android.hardware.security.keymint.Digest +import android.os.Binder +import android.os.Parcel +import android.os.RemoteException +import android.system.keystore2.IKeystoreOperation +import java.security.PrivateKey +import java.security.Signature +import org.matrix.TEESimulator.attestation.KeyMintAttestation +import org.matrix.TEESimulator.interception.keystore.InterceptorUtils +import org.matrix.TEESimulator.logging.SystemLogger + +/** + * A software-only implementation of a cryptographic operation (e.g., signing). This class uses the + * standard Java JCA to perform operations on a given private key. + */ +class SoftwareOperation( + private val txId: Long, + privateKey: PrivateKey, + params: KeyMintAttestation, +) { + private val signature: Signature + + init { + // Parse the KeyMintAttestation object to determine the correct JCA algorithm string. + val signatureAlgorithm = parseSignatureAlgorithm(params) + SystemLogger.debug( + "[SoftwareOp TX_ID: $txId] Initializing with algorithm: $signatureAlgorithm" + ) + + signature = Signature.getInstance(signatureAlgorithm).apply { initSign(privateKey) } + } + + /** + * Determines the JCA standard signature algorithm string from KeyMint parameters. Replicates + * logic seen in AOSP frameworks like CertificateGenerator. + */ + private fun parseSignatureAlgorithm(params: KeyMintAttestation): String { + val digestName = + when (params.digest.firstOrNull()) { + Digest.SHA_2_256 -> "SHA256" + Digest.SHA_2_384 -> "SHA384" + Digest.SHA_2_512 -> "SHA512" + // Add other digest mappings as needed + else -> "NONE" // A valid JCA value for certain algorithms + } + + val algorithmName = + when (params.algorithm) { + Algorithm.EC -> "ECDSA" + Algorithm.RSA -> "RSA" + // Add other algorithm mappings as needed + else -> + throw IllegalArgumentException( + "Unsupported algorithm for signing: ${params.algorithm}" + ) + } + + return "${digestName}with${algorithmName}" + } + + fun update(data: ByteArray?) { + if (data == null || data.isEmpty()) return + try { + signature.update(data) + } catch (e: Exception) { + SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update signature.", e) + throw e + } + } + + fun finish(data: ByteArray?): ByteArray { + update(data) + try { + val result = signature.sign() + SystemLogger.info( + "[SoftwareOp TX_ID: $txId] Finished signing. Signature size: ${result.size} bytes." + ) + return result + } catch (e: Exception) { + SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish signing.", e) + throw e + } + } + + fun abort() { + SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.") + } +} + +/** + * The Binder interface for our [SoftwareOperation]. + */ +class SoftwareOperationBinder(private val operation: SoftwareOperation) : IKeystoreOperation.Stub() { + + @Throws(RemoteException::class) + override fun update(input: ByteArray?): ByteArray? { + operation.update(input) + // As per the AIDL comments, the update method for a signing operation returns nothing. + return null + } + + @Throws(RemoteException::class) + override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray { + // The 'signature' parameter is for verification operations, so we ignore it here. + return operation.finish(input) + } + + @Throws(RemoteException::class) + override fun abort() { + operation.abort() + } +} diff --git a/stub/src/main/java/android/system/keystore2/CreateOperationResponse.java b/stub/src/main/java/android/system/keystore2/CreateOperationResponse.java new file mode 100644 index 0000000..a0ff209 --- /dev/null +++ b/stub/src/main/java/android/system/keystore2/CreateOperationResponse.java @@ -0,0 +1,38 @@ +package android.system.keystore2; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +public class CreateOperationResponse implements Parcelable { + public IKeystoreOperation iOperation; + + public OperationChallenge operationChallenge; + + public KeyParameters parameters; + + public byte[] upgradedBlob; + + public static final Creator CREATOR = new Creator() { + @Override + public CreateOperationResponse createFromParcel(Parcel in) { + throw new UnsupportedOperationException("STUB!"); + } + + @Override + public CreateOperationResponse[] newArray(int size) { + throw new UnsupportedOperationException("STUB!"); + } + }; + + @Override + public int describeContents() { + throw new UnsupportedOperationException("STUB!"); + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int i) { + throw new UnsupportedOperationException("STUB!"); + } +} diff --git a/stub/src/main/java/android/system/keystore2/Domain.java b/stub/src/main/java/android/system/keystore2/Domain.java new file mode 100644 index 0000000..8103aa3 --- /dev/null +++ b/stub/src/main/java/android/system/keystore2/Domain.java @@ -0,0 +1,9 @@ +package android.system.keystore2; + +public @interface Domain { + public static final int APP = 0; + public static final int GRANT = 1; + public static final int SELINUX = 2; + public static final int BLOB = 3; + public static final int KEY_ID = 4; +} diff --git a/stub/src/main/java/android/system/keystore2/IKeystoreOperation.java b/stub/src/main/java/android/system/keystore2/IKeystoreOperation.java new file mode 100644 index 0000000..d69ffbf --- /dev/null +++ b/stub/src/main/java/android/system/keystore2/IKeystoreOperation.java @@ -0,0 +1,33 @@ +package android.system.keystore2; + +import android.os.IBinder; +import android.os.Binder; +import android.os.IInterface; + +public interface IKeystoreOperation extends IInterface { + public static final java.lang.String DESCRIPTOR = "android.system.keystore2.IKeystoreOperation"; + + public void updateAad(byte[] aadInput); + + public byte[] update(byte[] input); + + public byte[] finish(byte[] input, byte[] signature); + + public void abort() throws android.os.RemoteException; + + abstract class Stub extends Binder implements IKeystoreOperation { + public static IKeystoreOperation asInterface(IBinder b) { + throw new UnsupportedOperationException("STUB!"); + } + + @Override + public IBinder asBinder() { + return this; + } + + @Override + public void updateAad(byte[] aadInput) { + throw new UnsupportedOperationException("STUB!"); + } + } +} diff --git a/stub/src/main/java/android/system/keystore2/KeyParameters.java b/stub/src/main/java/android/system/keystore2/KeyParameters.java new file mode 100644 index 0000000..e4c4822 --- /dev/null +++ b/stub/src/main/java/android/system/keystore2/KeyParameters.java @@ -0,0 +1,34 @@ +package android.system.keystore2; + +import android.os.Parcel; +import android.os.Parcelable; +import android.hardware.security.keymint.KeyParameter; + +import androidx.annotation.NonNull; + +public class KeyParameters implements Parcelable { + public KeyParameter[] keyParameter; + + public static final Creator CREATOR = new Creator() { + @Override + public KeyParameters createFromParcel(Parcel in) { + throw new UnsupportedOperationException("STUB!"); + } + + @Override + public KeyParameters[] newArray(int size) { + throw new UnsupportedOperationException("STUB!"); + } + }; + + @Override + public int describeContents() { + throw new UnsupportedOperationException("STUB!"); + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int i) { + throw new UnsupportedOperationException("STUB!"); + } +} + diff --git a/stub/src/main/java/android/system/keystore2/OperationChallenge.java b/stub/src/main/java/android/system/keystore2/OperationChallenge.java new file mode 100644 index 0000000..3824f57 --- /dev/null +++ b/stub/src/main/java/android/system/keystore2/OperationChallenge.java @@ -0,0 +1,32 @@ +package android.system.keystore2; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +public class OperationChallenge implements Parcelable { + public long challenge = 0L; + + public static final Creator CREATOR = new Creator() { + @Override + public OperationChallenge createFromParcel(Parcel in) { + throw new UnsupportedOperationException("STUB!"); + } + + @Override + public OperationChallenge[] newArray(int size) { + throw new UnsupportedOperationException("STUB!"); + } + }; + + @Override + public int describeContents() { + throw new UnsupportedOperationException("STUB!"); + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int i) { + throw new UnsupportedOperationException("STUB!"); + } +} From 8ccbdd258035967a69ef93b0ba4d579363c93896 Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Sun, 7 Dec 2025 19:11:12 +0100 Subject: [PATCH 2/3] Implement multi-purpose crypto operations This commit refactors the `SoftwareOperation` engine to support multiple cryptographic purposes beyond just signing. The simulator can now correctly handle `SIGN`, `VERIFY`, `ENCRYPT`, and `DECRYPT` operations based on the parameters provided by the client application. This is a significant architectural improvement that moves from a hardcoded implementation to a flexible, strategy-based pattern: 1. Strategy Pattern for Crypto: A `CryptoPrimitive` sealed interface was introduced to define a common API for cryptographic actions. Concrete implementations (`Signer`, `Verifier`, `CipherPrimitive`) now encapsulate the specific logic for each purpose. The main `SoftwareOperation` class acts as a controller, instantiating the correct primitive based on the `KeyPurpose` tag from the operation parameters. 2. JCA Algorithm Mapping: A `JcaAlgorithmMapper` object was created to centralize the complex logic of converting KeyMint constants into standard JCA algorithm strings (e.g., "SHA256withECDSA", "RSA/ECB/PKCS1Padding"). This makes the code cleaner and more maintainable. 3. Expanded Parameter Parsing: To support cipher operations, `KeyMintAttestation` has been updated to parse `BLOCK_MODE` and `PADDING` tags. The `KeyMintParameterLogger` was also enhanced to provide human-readable names for these new modes. This refactoring makes the `createOperation` simulation far more robust and accurate, correctly mimicking the behavior of a real KeyMint implementation across a wider range of cryptographic use cases. --- .../attestation/KeyMintAttestation.kt | 20 +- .../keystore/KeystoreInterceptor.kt | 2 + .../keystore/shim/SoftwareOperation.kt | 213 +++++++++++++----- .../logging/KeyMintParameterLogger.kt | 27 ++- .../hardware/security/keymint/BlockMode.java | 8 + .../security/keymint/PaddingMode.java | 10 + 6 files changed, 214 insertions(+), 66 deletions(-) create mode 100644 stub/src/main/java/android/hardware/security/keymint/BlockMode.java create mode 100644 stub/src/main/java/android/hardware/security/keymint/PaddingMode.java diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt index 2d776dd..b6afbbb 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt @@ -1,8 +1,6 @@ package org.matrix.TEESimulator.attestation -import android.hardware.security.keymint.EcCurve -import android.hardware.security.keymint.KeyParameter -import android.hardware.security.keymint.Tag +import android.hardware.security.keymint.* import java.math.BigInteger import java.util.Date import javax.security.auth.x500.X500Principal @@ -22,6 +20,8 @@ data class KeyMintAttestation( val algorithm: Int, val ecCurve: Int, val ecCurveName: String, + val blockMode: List, + val padding: List, val purpose: List, val digest: List, val rsaPublicExponent: BigInteger?, @@ -54,6 +54,12 @@ data class KeyMintAttestation( ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0, ecCurveName = params.deriveEcCurveName(), + // AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)] + blockMode = params.findAllBlockMode(Tag.BLOCK_MODE), + + // AOSP: [key_param(tag = PADDING, field = PaddingMode)] + padding = params.findAllPaddingMode(Tag.PADDING), + // AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)] purpose = params.findAllKeyPurpose(Tag.PURPOSE), @@ -121,6 +127,14 @@ private fun Array.findDate(tag: Int): Date? = private fun Array.findBlob(tag: Int): ByteArray? = this.find { it.tag == tag }?.value?.blob +/** Maps to AOSP field = BlockMode (Repeated) */ +private fun Array.findAllBlockMode(tag: Int): List = + this.filter { it.tag == tag }.map { it.value.blockMode } + +/** Maps to AOSP field = BlockMode (Repeated) */ +private fun Array.findAllPaddingMode(tag: Int): List = + this.filter { it.tag == tag }.map { it.value.paddingMode } + /** Maps to AOSP field = KeyPurpose (Repeated) */ private fun Array.findAllKeyPurpose(tag: Int): List = this.filter { it.tag == tag }.map { it.value.keyPurpose } 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 index c6958cb..2aef665 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/KeystoreInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/KeystoreInterceptor.kt @@ -378,6 +378,8 @@ private data class LegacyKeygenParameters( algorithm = this.algorithm, ecCurve = 0, // Not explicitly available in legacy args, but not critical ecCurveName = this.ecCurveName ?: "", + blockMode = listOf(), + padding = listOf(), purpose = this.purpose, digest = this.digest, rsaPublicExponent = this.rsaPublicExponent, diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt index 3c51926..79cd2c9 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt @@ -1,111 +1,212 @@ package org.matrix.TEESimulator.interception.keystore.shim import android.hardware.security.keymint.Algorithm +import android.hardware.security.keymint.BlockMode import android.hardware.security.keymint.Digest -import android.os.Binder -import android.os.Parcel +import android.hardware.security.keymint.KeyPurpose +import android.hardware.security.keymint.PaddingMode import android.os.RemoteException import android.system.keystore2.IKeystoreOperation -import java.security.PrivateKey +import java.security.KeyPair import java.security.Signature +import java.security.SignatureException +import javax.crypto.Cipher import org.matrix.TEESimulator.attestation.KeyMintAttestation -import org.matrix.TEESimulator.interception.keystore.InterceptorUtils +import org.matrix.TEESimulator.logging.KeyMintParameterLogger import org.matrix.TEESimulator.logging.SystemLogger -/** - * A software-only implementation of a cryptographic operation (e.g., signing). This class uses the - * standard Java JCA to perform operations on a given private key. - */ -class SoftwareOperation( - private val txId: Long, - privateKey: PrivateKey, - params: KeyMintAttestation, -) { - private val signature: Signature +// A sealed interface to represent the different cryptographic operations we can perform. +private sealed interface CryptoPrimitive { + fun update(data: ByteArray?): ByteArray? - init { - // Parse the KeyMintAttestation object to determine the correct JCA algorithm string. - val signatureAlgorithm = parseSignatureAlgorithm(params) - SystemLogger.debug( - "[SoftwareOp TX_ID: $txId] Initializing with algorithm: $signatureAlgorithm" - ) + fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? - signature = Signature.getInstance(signatureAlgorithm).apply { initSign(privateKey) } - } + fun abort() +} - /** - * Determines the JCA standard signature algorithm string from KeyMint parameters. Replicates - * logic seen in AOSP frameworks like CertificateGenerator. - */ - private fun parseSignatureAlgorithm(params: KeyMintAttestation): String { - val digestName = +// Helper object to map KeyMint constants to JCA algorithm strings. +private object JcaAlgorithmMapper { + fun mapSignatureAlgorithm(params: KeyMintAttestation): String { + val digest = when (params.digest.firstOrNull()) { Digest.SHA_2_256 -> "SHA256" Digest.SHA_2_384 -> "SHA384" Digest.SHA_2_512 -> "SHA512" - // Add other digest mappings as needed - else -> "NONE" // A valid JCA value for certain algorithms + else -> "NONE" } - - val algorithmName = + val keyAlgo = when (params.algorithm) { Algorithm.EC -> "ECDSA" Algorithm.RSA -> "RSA" - // Add other algorithm mappings as needed else -> throw IllegalArgumentException( - "Unsupported algorithm for signing: ${params.algorithm}" + "Unsupported signature algorithm: ${params.algorithm}" + ) + } + return "${digest}with${keyAlgo}" + } + + fun mapCipherAlgorithm(params: KeyMintAttestation): String { + val keyAlgo = + when (params.algorithm) { + Algorithm.RSA -> "RSA" + Algorithm.AES -> "AES" + else -> + throw IllegalArgumentException( + "Unsupported cipher algorithm: ${params.algorithm}" ) } + val blockMode = + when (params.blockMode.firstOrNull()) { + BlockMode.ECB -> "ECB" + BlockMode.CBC -> "CBC" + BlockMode.GCM -> "GCM" + else -> "ECB" // Default for RSA + } + val padding = + when (params.padding.firstOrNull()) { + PaddingMode.NONE -> "NoPadding" + PaddingMode.PKCS7 -> "PKCS7Padding" + PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding" + PaddingMode.RSA_OAEP -> "OAEPPadding" + else -> "NoPadding" // Default for GCM + } + return "$keyAlgo/$blockMode/$padding" + } +} + +// Concrete implementation for Signing. +private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive { + private val signature: Signature = + Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply { + initSign(keyPair.private) + } + + override fun update(data: ByteArray?): ByteArray? { + if (data != null) signature.update(data) + return null + } + + override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray { + if (data != null) update(data) + return this.signature.sign() + } + + override fun abort() {} +} - return "${digestName}with${algorithmName}" +// Concrete implementation for Verification. +private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive { + private val signature: Signature = + Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply { + initVerify(keyPair.public) + } + + override fun update(data: ByteArray?): ByteArray? { + if (data != null) signature.update(data) + return null + } + + override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { + if (data != null) update(data) + if (signature == null) throw SignatureException("Signature to verify is null") + if (!this.signature.verify(signature)) { + // Throwing an exception is how Keystore signals verification failure. + throw SignatureException("Signature verification failed") + } + // A successful verification returns no data. + return null + } + + override fun abort() {} +} + +// Concrete implementation for Encryption/Decryption. +private class CipherPrimitive( + keyPair: KeyPair, + params: KeyMintAttestation, + private val opMode: Int, +) : CryptoPrimitive { + private val cipher: Cipher = + Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply { + val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private + init(opMode, key) + } + + override fun update(data: ByteArray?): ByteArray? = + if (data != null) cipher.update(data) else null + + override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? = + if (data != null) cipher.doFinal(data) else cipher.doFinal() + + override fun abort() {} +} + +/** + * A software-only implementation of a cryptographic operation. This class acts as a controller, + * delegating to a specific cryptographic primitive based on the operation's purpose. + */ +class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) { + // This now holds the specific strategy object (Signer, Verifier, etc.) + private val primitive: CryptoPrimitive + + init { + // The "Strategy" pattern: choose the implementation based on the purpose. + // For simplicity, we only consider the first purpose listed. + val purpose = params.purpose.firstOrNull() + val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN" + SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.") + + primitive = + when (purpose) { + KeyPurpose.SIGN -> Signer(keyPair, params) + KeyPurpose.VERIFY -> Verifier(keyPair, params) + KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE) + KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE) + else -> + throw UnsupportedOperationException("Unsupported operation purpose: $purpose") + } } - fun update(data: ByteArray?) { - if (data == null || data.isEmpty()) return + fun update(data: ByteArray?): ByteArray? { try { - signature.update(data) + return primitive.update(data) } catch (e: Exception) { - SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update signature.", e) + SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e) throw e } } - fun finish(data: ByteArray?): ByteArray { - update(data) + fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { try { - val result = signature.sign() - SystemLogger.info( - "[SoftwareOp TX_ID: $txId] Finished signing. Signature size: ${result.size} bytes." - ) + val result = primitive.finish(data, signature) + SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.") return result } catch (e: Exception) { - SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish signing.", e) + SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e) + // Re-throw the exception so the binder can report it to the client. throw e } } fun abort() { + primitive.abort() SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.") } } -/** - * The Binder interface for our [SoftwareOperation]. - */ -class SoftwareOperationBinder(private val operation: SoftwareOperation) : IKeystoreOperation.Stub() { +/** The Binder interface for our [SoftwareOperation]. */ +class SoftwareOperationBinder(private val operation: SoftwareOperation) : + IKeystoreOperation.Stub() { @Throws(RemoteException::class) override fun update(input: ByteArray?): ByteArray? { - operation.update(input) - // As per the AIDL comments, the update method for a signing operation returns nothing. - return null + return operation.update(input) } @Throws(RemoteException::class) - override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray { - // The 'signature' parameter is for verification operations, so we ignore it here. - return operation.finish(input) + override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? { + return operation.finish(input, signature) } @Throws(RemoteException::class) diff --git a/app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt b/app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt index 753ce42..0940cdd 100644 --- a/app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt +++ b/app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt @@ -1,11 +1,6 @@ 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 android.hardware.security.keymint.* import java.math.BigInteger import java.nio.charset.StandardCharsets import java.util.Date @@ -34,7 +29,23 @@ object KeyMintParameterLogger { .associate { field -> (field.get(null) as Int) to field.name } } - private val purposeNames: Map by lazy { + val blockModeNames: Map by lazy { + BlockMode::class + .java + .fields + .filter { it.type == Int::class.java } + .associate { field -> (field.get(null) as Int) to field.name } + } + + val paddingNames: Map by lazy { + PaddingMode::class + .java + .fields + .filter { it.type == Int::class.java } + .associate { field -> (field.get(null) as Int) to field.name } + } + + val purposeNames: Map by lazy { KeyPurpose::class .java .fields @@ -69,7 +80,9 @@ object KeyMintParameterLogger { val formattedValue: String = when (param.tag) { Tag.ALGORITHM -> algorithmNames[value.algorithm] + Tag.BLOCK_MODE -> blockModeNames[value.blockMode] Tag.EC_CURVE -> ecCurveNames[value.ecCurve] + Tag.PADDING -> paddingNames[value.paddingMode] Tag.PURPOSE -> purposeNames[value.keyPurpose] Tag.DIGEST -> digestNames[value.digest] Tag.AUTH_TIMEOUT, diff --git a/stub/src/main/java/android/hardware/security/keymint/BlockMode.java b/stub/src/main/java/android/hardware/security/keymint/BlockMode.java new file mode 100644 index 0000000..4481f2f --- /dev/null +++ b/stub/src/main/java/android/hardware/security/keymint/BlockMode.java @@ -0,0 +1,8 @@ +package android.hardware.security.keymint; + +public @interface BlockMode { + public static final int ECB = 1; + public static final int CBC = 2; + public static final int CTR = 3; + public static final int GCM = 32; +} diff --git a/stub/src/main/java/android/hardware/security/keymint/PaddingMode.java b/stub/src/main/java/android/hardware/security/keymint/PaddingMode.java new file mode 100644 index 0000000..021b54e --- /dev/null +++ b/stub/src/main/java/android/hardware/security/keymint/PaddingMode.java @@ -0,0 +1,10 @@ +package android.hardware.security.keymint; + +public @interface PaddingMode { + public static final int NONE = 1; + public static final int RSA_OAEP = 2; + public static final int RSA_PSS = 3; + public static final int RSA_PKCS1_1_5_ENCRYPT = 4; + public static final int RSA_PKCS1_1_5_SIGN = 5; + public static final int PKCS7 = 64; +} From 8d247eb0c231925f7418fd7c131936233de7c17b Mon Sep 17 00:00:00 2001 From: JingMatrix Date: Mon, 8 Dec 2025 08:56:05 +0100 Subject: [PATCH 3/3] Fix regression caused by typo --- .../keystore/shim/KeyMintSecurityLevelInterceptor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 35c6109..df3623a 100644 --- 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 @@ -67,7 +67,7 @@ class KeyMintSecurityLevelInterceptor( SystemLogger.info( "[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]" ) - TransactionResult.Continue + return TransactionResult.Continue } }