From 2181157cb64044d8056ce42389fe06c55d10901a Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Thu, 19 Mar 2026 07:33:30 +0100 Subject: [PATCH] feat(operation): add AOSP-compliant error handling, authorize_create, and GCM IV SoftwareOperation now throws ServiceSpecificException for all error paths instead of raw Java exceptions, matching AIDL wire format. updateAad on non-AEAD operations returns INVALID_TAG (-76) per AOSP operation.rs. SoftwareOperationBinder methods are @Synchronized to match AOSP Mutex semantics. GCM encrypt operations return the generated IV in CreateOperationResponse.parameters. AuthorizeCreate enforces PURPOSE validation, algorithm-purpose compatibility (EC rejects ENCRYPT/DECRYPT, RSA rejects AGREE_KEY), temporal constraints (ACTIVE_DATETIME, ORIGINATION_EXPIRE, USAGE_EXPIRE), and CALLER_NONCE prohibition. GeneratedKeyInfo carries keyParams for authorize_create enforcement on software createOperation. --- .../keystore/shim/AuthorizeCreate.kt | 77 ++++++++++++ .../shim/KeyMintSecurityLevelInterceptor.kt | 60 ++++++++- .../keystore/shim/SoftwareOperation.kt | 114 ++++++++++++++---- 3 files changed, 226 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt new file mode 100644 index 0000000..b2f8d40 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt @@ -0,0 +1,77 @@ +package org.matrix.TEESimulator.interception.keystore.shim + +import android.hardware.security.keymint.Algorithm +import android.hardware.security.keymint.KeyPurpose +import android.hardware.security.keymint.KeyParameter +import android.hardware.security.keymint.Tag +import org.matrix.TEESimulator.attestation.KeyMintAttestation + +object AuthorizeCreate { + + fun check( + keyParams: KeyMintAttestation?, + opParams: KeyMintAttestation, + rawOpParams: Array? = null, + ): Int? { + if (keyParams == null) return null + return checkPurpose(keyParams, opParams) + ?: checkAlgorithmPurpose(keyParams, opParams) + ?: checkTemporalValidity(keyParams, opParams) + ?: checkCallerNonce(keyParams, rawOpParams) + } + + private fun checkPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? { + val requestedPurpose = opParams.purpose.firstOrNull() ?: return null + if (requestedPurpose == KeyPurpose.WRAP_KEY) + return KeystoreErrorCodes.incompatiblePurpose + if (requestedPurpose !in keyParams.purpose) + return KeystoreErrorCodes.incompatiblePurpose + return null + } + + private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? { + val purpose = opParams.purpose.firstOrNull() ?: return null + return when (keyParams.algorithm) { + Algorithm.EC -> when (purpose) { + KeyPurpose.ENCRYPT, KeyPurpose.DECRYPT -> KeystoreErrorCodes.unsupportedPurpose + KeyPurpose.AGREE_KEY -> null + else -> null + } + Algorithm.RSA -> when (purpose) { + KeyPurpose.AGREE_KEY -> KeystoreErrorCodes.unsupportedPurpose + else -> null + } + else -> null + } + } + + private fun checkTemporalValidity(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? { + val now = System.currentTimeMillis() + val purpose = opParams.purpose.firstOrNull() + + keyParams.activeDateTime?.let { activeDate -> + if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid + } + + keyParams.originationExpireDateTime?.let { expireDate -> + if (purpose == KeyPurpose.SIGN || purpose == KeyPurpose.ENCRYPT) { + if (now > expireDate.time) return KeystoreErrorCodes.keyExpired + } + } + + keyParams.usageExpireDateTime?.let { expireDate -> + if (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.DECRYPT) { + if (now > expireDate.time) return KeystoreErrorCodes.keyExpired + } + } + + return null + } + + private fun checkCallerNonce(keyParams: KeyMintAttestation, rawOpParams: Array?): Int? { + if (keyParams.callerNonce == true) return null + val hasNonce = rawOpParams?.any { it.tag == Tag.NONCE } == true + if (hasNonce) return KeystoreErrorCodes.callerNonceProhibited + return null + } +} 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 2d28dcc..69f3d57 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 @@ -1,8 +1,10 @@ package org.matrix.TEESimulator.interception.keystore.shim import android.hardware.security.keymint.Algorithm +import android.hardware.security.keymint.BlockMode import android.hardware.security.keymint.EcCurve import android.hardware.security.keymint.KeyParameter +import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.KeyParameterValue import android.hardware.security.keymint.KeyOrigin import android.hardware.security.keymint.SecurityLevel @@ -47,6 +49,7 @@ class KeyMintSecurityLevelInterceptor( val keyPair: KeyPair, val nspace: Long, val response: KeyEntryResponse, + val keyParams: KeyMintAttestation? = null, ) private val activeOps = ConcurrentHashMap>() @@ -132,6 +135,7 @@ class KeyMintSecurityLevelInterceptor( GeneratedKeyPersistence.delete(keyId) } attestationKeys.remove(keyId) + importedKeys.add(keyId) } else if (code == CREATE_OPERATION_TRANSACTION) { logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) @@ -158,7 +162,7 @@ class KeyMintSecurityLevelInterceptor( val backdoor = getBackdoor(target) if (backdoor != null) { val interceptor = OperationInterceptor(operation, backdoor) - register(backdoor, operationBinder, interceptor) + register(backdoor, operationBinder, interceptor, OperationInterceptor.INTERCEPTED_CODES) interceptedOperations[operationBinder] = interceptor } else { SystemLogger.error( @@ -281,6 +285,11 @@ class KeyMintSecurityLevelInterceptor( }) } + AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { errorCode -> + SystemLogger.info("[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode") + return InterceptorUtils.createErrorReply(errorCode) + } + val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams, opLatency) val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID @@ -291,6 +300,16 @@ class KeyMintSecurityLevelInterceptor( CreateOperationResponse().apply { iOperation = operationBinder operationChallenge = null + softwareOperation.iv?.let { iv -> + parameters = KeyParameters().apply { + keyParameter = arrayOf( + KeyParameter().apply { + tag = Tag.NONCE + value = KeyParameterValue.blob(iv) + } + ) + } + } } return InterceptorUtils.createTypedObjectReply(response) @@ -423,7 +442,7 @@ class KeyMintSecurityLevelInterceptor( cleanupKeyData(keyId) val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor) - generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response) + generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response, parsedParams) if (isAttestKeyRequest) attestationKeys.add(keyId) GeneratedKeyPersistence.save( @@ -610,10 +629,27 @@ class KeyMintSecurityLevelInterceptor( manufacturer = null, model = null, secondImei = null, + activeDateTime = null, + originationExpireDateTime = null, + usageExpireDateTime = null, + usageCountLimit = null, + callerNonce = null, + unlockedDeviceRequired = null, + includeUniqueId = null, + rollbackResistance = null, + earlyBootOnly = null, + allowWhileOnBody = null, + trustedUserPresenceRequired = null, + trustedConfirmationRequired = null, + noAuthRequired = null, + maxUsesPerBoot = null, + maxBootLevel = null, + minMacLength = null, + rsaOaepMgfDigest = emptyList(), ) val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor) - generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response) + generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response, attestation) if (record.isAttestationKey) attestationKeys.add(keyId) SystemLogger.debug("Restored persisted key: $keyId") @@ -688,6 +724,9 @@ class KeyMintSecurityLevelInterceptor( "createOperation", ) + val INTERCEPTED_CODES = + intArrayOf(GENERATE_KEY_TRANSACTION, IMPORT_KEY_TRANSACTION, CREATE_OPERATION_TRANSACTION) + private val transactionNames: Map by lazy { IKeystoreSecurityLevel.Stub::class .java @@ -702,6 +741,7 @@ class KeyMintSecurityLevelInterceptor( val generatedKeys = ConcurrentHashMap() val patchedChains = ConcurrentHashMap>() val attestationKeys: MutableSet = ConcurrentHashMap.newKeySet() + val importedKeys: MutableSet = ConcurrentHashMap.newKeySet() private val interceptedOperations = ConcurrentHashMap() fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? = @@ -730,6 +770,7 @@ class KeyMintSecurityLevelInterceptor( if (attestationKeys.remove(keyId)) { SystemLogger.debug("Remove cached attestaion key ${keyId}") } + importedKeys.remove(keyId) } fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { @@ -754,6 +795,7 @@ class KeyMintSecurityLevelInterceptor( generatedKeys.clear() patchedChains.clear() attestationKeys.clear() + importedKeys.clear() GeneratedKeyPersistence.deleteAll() SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.") } @@ -783,6 +825,7 @@ private fun KeyMintAttestation.toAuthorizations( authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve))) } this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) } + this.blockMode.forEach { authList.add(createAuth(Tag.BLOCK_MODE, KeyParameterValue.blockMode(it))) } this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) } this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) } authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize))) @@ -806,7 +849,16 @@ private fun KeyMintAttestation.toAuthorizations( authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch))) } authList.add(createAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis()))) - authList.add(createAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000))) + authList.add( + Authorization().apply { + this.keyParameter = + KeyParameter().apply { + this.tag = Tag.USER_ID + this.value = KeyParameterValue.integer(callingUid / 100000) + } + this.securityLevel = SecurityLevel.SOFTWARE + } + ) return authList.toTypedArray() } 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 f1a8ed2..bd3b32a 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 @@ -5,7 +5,6 @@ import android.hardware.security.keymint.BlockMode import android.hardware.security.keymint.Digest import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.PaddingMode -import android.os.RemoteException import android.os.ServiceSpecificException import android.system.keystore2.IKeystoreOperation import java.security.KeyPair @@ -16,15 +15,16 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.logging.KeyMintParameterLogger import org.matrix.TEESimulator.logging.SystemLogger -// A sealed interface to represent the different cryptographic operations we can perform. private sealed interface CryptoPrimitive { - fun updateAad(aadInput: ByteArray?) {} + fun updateAad(aadInput: ByteArray?) { + throw ServiceSpecificException(KeystoreErrorCodes.invalidTag) + } fun update(data: ByteArray?): ByteArray? fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? fun abort() + fun getIv(): ByteArray? = null } -// Helper object to map KeyMint constants to JCA algorithm strings. private object JcaAlgorithmMapper { fun mapSignatureAlgorithm(params: KeyMintAttestation): String { val digest = @@ -41,8 +41,9 @@ private object JcaAlgorithmMapper { if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA" } else -> - throw IllegalArgumentException( - "Unsupported signature algorithm: ${params.algorithm}" + throw ServiceSpecificException( + KeystoreErrorCodes.incompatibleAlgorithm, + "Unsupported signature algorithm: ${params.algorithm}", ) } } @@ -53,8 +54,9 @@ private object JcaAlgorithmMapper { Algorithm.RSA -> "RSA" Algorithm.AES -> "AES" else -> - throw IllegalArgumentException( - "Unsupported cipher algorithm: ${params.algorithm}" + throw ServiceSpecificException( + KeystoreErrorCodes.incompatibleAlgorithm, + "Unsupported cipher algorithm: ${params.algorithm}", ) } val blockMode = @@ -78,7 +80,6 @@ private object JcaAlgorithmMapper { } } -// Concrete implementation for Signing. private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive { private val signature: Signature = Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply { @@ -98,7 +99,6 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi override fun abort() {} } -// Concrete implementation for Verification. private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive { private val signature: Signature = Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply { @@ -112,36 +112,43 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { if (data != null) update(data) - if (signature == null) throw SignatureException("Signature to verify is null") + if (signature == null) { + throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature to verify is null") + } if (!this.signature.verify(signature)) { - // Throwing an exception is how Keystore signals verification failure. - throw SignatureException("Signature verification failed") + throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "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 isAead = params.blockMode.firstOrNull() == BlockMode.GCM 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 updateAad(aadInput: ByteArray?) { + if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag) + if (aadInput != null) cipher.updateAAD(aadInput) + } + 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 getIv(): ByteArray? = if (isAead) cipher.iv else null + override fun abort() {} } @@ -155,6 +162,9 @@ class SoftwareOperation( @Volatile var finalized = false private set + val iv: ByteArray? + get() = primitive.getIv() + init { val purpose = params.purpose.firstOrNull() val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN" @@ -167,7 +177,10 @@ class SoftwareOperation( KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE) KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE) else -> - throw UnsupportedOperationException("Unsupported operation purpose: $purpose") + throw ServiceSpecificException( + KeystoreErrorCodes.unsupportedPurpose, + "Unsupported operation purpose: $purpose", + ) } } @@ -202,7 +215,7 @@ class SoftwareOperation( throw e } catch (e: Exception) { SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e) - throw e + throw mapToServiceSpecificException(e) } } @@ -224,7 +237,7 @@ class SoftwareOperation( throw e } catch (e: Exception) { SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e) - throw e + throw mapToServiceSpecificException(e) } } @@ -234,13 +247,20 @@ class SoftwareOperation( SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.") } + private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) { + is SignatureException -> ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message) + is javax.crypto.BadPaddingException -> ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message) + is javax.crypto.IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message) + is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message) + else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message) + } + companion object { - // AOSP keystore2 operation.rs: const MAX_RECEIVE_DATA: usize = 0x8000 private const val MAX_RECEIVE_DATA = 0x8000 } } -private object KeystoreErrorCodes { +internal object KeystoreErrorCodes { val tooMuchData: Int by lazy { resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21) } @@ -249,7 +269,55 @@ private object KeystoreErrorCodes { resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28) } - private fun resolveField(className: String, fieldName: String, fallback: Int): Int = + val invalidTag: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_TAG", -76) + } + + val verificationFailed: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "VERIFICATION_FAILED", -30) + } + + val invalidArgument: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_ARGUMENT", -38) + } + + val invalidInputLength: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_INPUT_LENGTH", -21) + } + + val incompatibleKey: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_KEY", -31) + } + + val incompatiblePurpose: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_PURPOSE", -13) + } + + val unsupportedPurpose: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "UNSUPPORTED_PURPOSE", -14) + } + + val incompatibleAlgorithm: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_ALGORITHM", -18) + } + + val keyNotYetValid: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "KEY_NOT_YET_VALID", -39) + } + + val keyExpired: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "KEY_EXPIRED", -40) + } + + val callerNonceProhibited: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "CALLER_NONCE_PROHIBITED", -55) + } + + val unknownError: Int by lazy { + resolveField("android.hardware.security.keymint.ErrorCode", "UNKNOWN_ERROR", -1000) + } + + fun resolveField(className: String, fieldName: String, fallback: Int): Int = runCatching { Class.forName(className).getField(fieldName).getInt(null) }.getOrElse { @@ -261,18 +329,22 @@ private object KeystoreErrorCodes { class SoftwareOperationBinder(private val operation: SoftwareOperation) : IKeystoreOperation.Stub() { + @Synchronized override fun updateAad(aadInput: ByteArray?) { operation.updateAad(aadInput) } + @Synchronized override fun update(input: ByteArray?): ByteArray? { return operation.update(input) } + @Synchronized override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? { return operation.finish(input, signature) } + @Synchronized override fun abort() { operation.abort() }