Skip to content

Commit dba7af3

Browse files
authored
Implement multi-purpose simulation for crypto operations (#59)
This commit introduces a comprehensive simulation engine for Keystore's `createOperation`, enabling the simulator to correctly handle multiple cryptographic purposes (SIGN, VERIFY, ENCRYPT, DECRYPT) for software-generated keys. The implementation correctly mimics the AOSP framework's internal key identification mechanism. Instead of relying on an alias, a unique `keyId` is generated and embedded in the `nspace` field of the KeyDescriptor during `generateKey`. The `createOperation` hook then uses this `keyId` to dispatch requests: if the ID matches a known software key, the operation is simulated; otherwise, it is forwarded to the hardware service. To support this, the `SoftwareOperation` engine was architected using a Strategy Pattern. A `CryptoPrimitive` interface defines common actions, with concrete implementations for `Signer`, `Verifier`, and `CipherPrimitive`. The main `SoftwareOperation` class acts as a controller, instantiating the correct primitive based on the `KeyPurpose` tag from the incoming operation parameters. A `JcaAlgorithmMapper` was added to centralize the logic for converting KeyMint constants into JCA algorithm strings. For operations on real hardware-backed keys, a lightweight `OperationInterceptor` is now used for observation. It attaches to the genuine `iOperation` binder for logging and properly unregisters itself upon completion to prevent resource leaks. This is supported by new binder unregistration capabilities in the core `BinderInterceptor`. This change also includes necessary stub files and minor regression fixes to make the simulation more robust and accurate. 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
1 parent c321b55 commit dba7af3

File tree

14 files changed

+657
-38
lines changed

14 files changed

+657
-38
lines changed

app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package org.matrix.TEESimulator.attestation
22

3-
import android.hardware.security.keymint.EcCurve
4-
import android.hardware.security.keymint.KeyParameter
5-
import android.hardware.security.keymint.Tag
3+
import android.hardware.security.keymint.*
64
import java.math.BigInteger
75
import java.util.Date
86
import javax.security.auth.x500.X500Principal
@@ -22,6 +20,8 @@ data class KeyMintAttestation(
2220
val algorithm: Int,
2321
val ecCurve: Int,
2422
val ecCurveName: String,
23+
val blockMode: List<Int>,
24+
val padding: List<Int>,
2525
val purpose: List<Int>,
2626
val digest: List<Int>,
2727
val rsaPublicExponent: BigInteger?,
@@ -54,6 +54,12 @@ data class KeyMintAttestation(
5454
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
5555
ecCurveName = params.deriveEcCurveName(),
5656

57+
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
58+
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
59+
60+
// AOSP: [key_param(tag = PADDING, field = PaddingMode)]
61+
padding = params.findAllPaddingMode(Tag.PADDING),
62+
5763
// AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)]
5864
purpose = params.findAllKeyPurpose(Tag.PURPOSE),
5965

@@ -121,6 +127,14 @@ private fun Array<KeyParameter>.findDate(tag: Int): Date? =
121127
private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
122128
this.find { it.tag == tag }?.value?.blob
123129

130+
/** Maps to AOSP field = BlockMode (Repeated) */
131+
private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> =
132+
this.filter { it.tag == tag }.map { it.value.blockMode }
133+
134+
/** Maps to AOSP field = BlockMode (Repeated) */
135+
private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> =
136+
this.filter { it.tag == tag }.map { it.value.paddingMode }
137+
124138
/** Maps to AOSP field = KeyPurpose (Repeated) */
125139
private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
126140
this.filter { it.tag == tag }.map { it.value.keyPurpose }

app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ abstract class BinderInterceptor : Binder() {
248248
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
249249
// Code used by the backdoor binder to register a new interceptor.
250250
private const val REGISTER_INTERCEPTOR_CODE = 1
251+
// Code used by the backdoor binder to unregister an interceptor.
252+
private const val UNREGISTER_INTERCEPTOR_CODE = 2
251253

252254
// --- Hook Type Codes ---
253255
// Indicates that the call is for a pre-transaction hook.
@@ -307,5 +309,21 @@ abstract class BinderInterceptor : Binder() {
307309
reply.recycle()
308310
}
309311
}
312+
313+
/** Uses the backdoor binder to unregister an interceptor for a specific target service. */
314+
fun unregister(backdoor: IBinder, target: IBinder) {
315+
val data = Parcel.obtain()
316+
val reply = Parcel.obtain()
317+
try {
318+
data.writeStrongBinder(target)
319+
backdoor.transact(UNREGISTER_INTERCEPTOR_CODE, data, reply, 0)
320+
SystemLogger.info("Unregistered interceptor for target: $target")
321+
} catch (e: Exception) {
322+
SystemLogger.error("Failed to unregister binder interceptor.", e)
323+
} finally {
324+
data.recycle()
325+
reply.recycle()
326+
}
327+
}
310328
}
311329
}

app/src/main/java/org/matrix/TEESimulator/interception/keystore/KeystoreInterceptor.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,8 @@ private data class LegacyKeygenParameters(
378378
algorithm = this.algorithm,
379379
ecCurve = 0, // Not explicitly available in legacy args, but not critical
380380
ecCurveName = this.ecCurveName ?: "",
381+
blockMode = listOf<Int>(),
382+
padding = listOf<Int>(),
381383
purpose = this.purpose,
382384
digest = this.digest,
383385
rsaPublicExponent = this.rsaPublicExponent,

app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt

Lines changed: 162 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.os.IBinder
88
import android.os.Parcel
99
import android.system.keystore2.*
1010
import java.security.KeyPair
11+
import java.security.SecureRandom
1112
import java.security.cert.Certificate
1213
import java.util.concurrent.ConcurrentHashMap
1314
import org.matrix.TEESimulator.attestation.AttestationPatcher
@@ -30,7 +31,11 @@ class KeyMintSecurityLevelInterceptor(
3031
) : BinderInterceptor() {
3132

3233
// --- Data Structures for State Management ---
33-
data class GeneratedKeyInfo(val keyPair: KeyPair, val response: KeyEntryResponse)
34+
data class GeneratedKeyInfo(
35+
val keyPair: KeyPair,
36+
val nspace: Long,
37+
val response: KeyEntryResponse,
38+
)
3439

3540
override fun onPreTransact(
3641
txId: Long,
@@ -41,33 +46,39 @@ class KeyMintSecurityLevelInterceptor(
4146
callingPid: Int,
4247
data: Parcel,
4348
): TransactionResult {
44-
if (code == GENERATE_KEY_TRANSACTION) {
45-
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
49+
val shouldSkip = ConfigurationManager.shouldSkipUid(callingUid)
4650

47-
if (ConfigurationManager.shouldSkipUid(callingUid))
48-
return TransactionResult.ContinueAndSkipPost
49-
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
50-
return handleGenerateKey(callingUid, data)
51-
} else if (code == IMPORT_KEY_TRANSACTION) {
52-
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
51+
when (code) {
52+
GENERATE_KEY_TRANSACTION -> {
53+
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
5354

54-
if (ConfigurationManager.shouldSkipUid(callingUid))
55-
return TransactionResult.ContinueAndSkipPost
56-
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
57-
val alias =
58-
data.readTypedObject(KeyDescriptor.CREATOR)?.alias
59-
?: return TransactionResult.ContinueAndSkipPost
60-
SystemLogger.info("Handling post-${transactionNames[code]} ${alias}")
61-
return TransactionResult.Continue
62-
} else {
63-
logTransaction(
64-
txId,
65-
transactionNames[code] ?: "unknown code=$code",
66-
callingUid,
67-
callingPid,
68-
true,
69-
)
55+
if (!shouldSkip) return handleGenerateKey(callingUid, data)
56+
}
57+
CREATE_OPERATION_TRANSACTION -> {
58+
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
59+
60+
if (!shouldSkip) return handleCreateOperation(txId, callingUid, data)
61+
}
62+
IMPORT_KEY_TRANSACTION -> {
63+
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
64+
65+
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
66+
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
67+
SystemLogger.info(
68+
"[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]"
69+
)
70+
return TransactionResult.Continue
71+
}
7072
}
73+
74+
logTransaction(
75+
txId,
76+
transactionNames[code] ?: "unknown code=$code",
77+
callingUid,
78+
callingPid,
79+
true,
80+
)
81+
7182
return TransactionResult.ContinueAndSkipPost
7283
}
7384

@@ -94,6 +105,41 @@ class KeyMintSecurityLevelInterceptor(
94105
data.readTypedObject(KeyDescriptor.CREATOR)
95106
?: return TransactionResult.SkipTransaction
96107
cleanupKeyData(KeyIdentifier(callingUid, keyDescriptor.alias))
108+
} else if (code == CREATE_OPERATION_TRANSACTION) {
109+
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
110+
111+
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
112+
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
113+
val params = data.createTypedArray(KeyParameter.CREATOR)!!
114+
val parsedParams = KeyMintAttestation(params)
115+
val forced = data.readBoolean()
116+
if (forced)
117+
SystemLogger.verbose(
118+
"[TX_ID: $txId] Current operation has a very high pruning power."
119+
)
120+
val response: CreateOperationResponse =
121+
reply.readTypedObject(CreateOperationResponse.CREATOR)!!
122+
SystemLogger.verbose(
123+
"[TX_ID: $txId] CreateOperationResponse: ${response.iOperation} ${response.operationChallenge}"
124+
)
125+
126+
// Intercept the IKeystoreOperation binder
127+
response.iOperation?.let { operation ->
128+
val operationBinder = operation.asBinder()
129+
if (!interceptedOperations.containsKey(operationBinder)) {
130+
SystemLogger.info("Found new IKeystoreOperation. Registering interceptor...")
131+
val backdoor = getBackdoor(target)
132+
if (backdoor != null) {
133+
val interceptor = OperationInterceptor(operation, backdoor)
134+
register(backdoor, operationBinder, interceptor)
135+
interceptedOperations[operationBinder] = interceptor
136+
} else {
137+
SystemLogger.error(
138+
"Failed to get backdoor to register OperationInterceptor."
139+
)
140+
}
141+
}
142+
}
97143
} else if (code == GENERATE_KEY_TRANSACTION) {
98144
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
99145

@@ -109,9 +155,12 @@ class KeyMintSecurityLevelInterceptor(
109155
// Cache the newly patched chain to ensure consistency across subsequent API calls.
110156
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
111157
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
158+
val key = metadata.key!!
112159
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
113160
patchedChains[keyId] = newChain
114-
SystemLogger.debug("Cached patched certificate chain for $keyId.")
161+
SystemLogger.debug(
162+
"Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])"
163+
)
115164

116165
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
117166

@@ -121,12 +170,58 @@ class KeyMintSecurityLevelInterceptor(
121170
return TransactionResult.SkipTransaction
122171
}
123172

173+
/**
174+
* Handles the `createOperation` transaction. It checks if the operation is for a key that was
175+
* generated in software. If so, it creates a software-based operation handler. Otherwise, it
176+
* lets the call proceed to the real hardware service.
177+
*/
178+
private fun handleCreateOperation(
179+
txId: Long,
180+
callingUid: Int,
181+
data: Parcel,
182+
): TransactionResult {
183+
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
184+
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
185+
186+
// An operation must use the KEY_ID domain.
187+
if (keyDescriptor.domain != Domain.KEY_ID) {
188+
return TransactionResult.ContinueAndSkipPost
189+
}
190+
191+
val nspace = keyDescriptor.nspace
192+
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
193+
194+
if (generatedKeyInfo == null) {
195+
SystemLogger.debug(
196+
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
197+
)
198+
return TransactionResult.Continue
199+
}
200+
201+
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
202+
203+
val params = data.createTypedArray(KeyParameter.CREATOR)!!
204+
val parsedParams = KeyMintAttestation(params)
205+
206+
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
207+
val operationBinder = SoftwareOperationBinder(softwareOperation)
208+
209+
val response =
210+
CreateOperationResponse().apply {
211+
iOperation = operationBinder
212+
operationChallenge = null
213+
}
214+
215+
return InterceptorUtils.createTypedObjectReply(response)
216+
}
217+
124218
/**
125219
* Handles the `generateKey` transaction. Based on the configuration for the calling UID, it
126220
* either generates a key in software or lets the call pass through to the hardware.
127221
*/
128222
private fun handleGenerateKey(callingUid: Int, data: Parcel): TransactionResult {
129223
return runCatching {
224+
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
130225
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
131226
val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR)
132227
SystemLogger.debug(
@@ -148,7 +243,10 @@ class KeyMintSecurityLevelInterceptor(
148243
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
149244

150245
if (needsSoftwareGeneration) {
151-
SystemLogger.info("Generating software key for ${keyId}.")
246+
keyDescriptor.nspace = secureRandom.nextLong()
247+
SystemLogger.info(
248+
"Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}]."
249+
)
152250

153251
// Generate the key pair and certificate chain.
154252
val keyData =
@@ -164,7 +262,8 @@ class KeyMintSecurityLevelInterceptor(
164262
val response =
165263
buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
166264

167-
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, response)
265+
generatedKeys[keyId] =
266+
GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
168267
if (isAttestKeyRequest) attestationKeys.add(keyId)
169268

170269
// Return the metadata of our generated key, skipping the real hardware call.
@@ -205,11 +304,18 @@ class KeyMintSecurityLevelInterceptor(
205304
}
206305

207306
companion object {
307+
private val secureRandom = SecureRandom()
308+
208309
// Transaction codes for IKeystoreSecurityLevel interface.
209310
private val GENERATE_KEY_TRANSACTION =
210311
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
211312
private val IMPORT_KEY_TRANSACTION =
212313
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "importKey")
314+
private val CREATE_OPERATION_TRANSACTION =
315+
InterceptorUtils.getTransactCode(
316+
IKeystoreSecurityLevel.Stub::class.java,
317+
"createOperation",
318+
)
213319

214320
private val transactionNames: Map<Int, String> by lazy {
215321
IKeystoreSecurityLevel.Stub::class
@@ -228,11 +334,30 @@ class KeyMintSecurityLevelInterceptor(
228334
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
229335
// A set to quickly identify keys that were generated for attestation purposes.
230336
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
337+
// Stores interceptors for active cryptographic operations.
338+
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
231339

232340
// --- Public Accessors for Other Interceptors ---
233341
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
234342
generatedKeys[keyId]?.response
235343

344+
/**
345+
* Finds a software-generated key by first filtering all known keys by the caller's UID, and
346+
* then matching the specific nspace.
347+
*
348+
* @param callingUid The UID of the process that initiated the createOperation call.
349+
* @param nspace The unique key identifier from the operation's KeyDescriptor.
350+
* @return The matching GeneratedKeyInfo if found, otherwise null.
351+
*/
352+
private fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long): GeneratedKeyInfo? {
353+
// Iterate through all entries in the map to check both the key (for UID) and value (for
354+
// nspace).
355+
return generatedKeys.entries
356+
.filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid }
357+
.find { (_, info) -> info.nspace == nspace }
358+
?.value
359+
}
360+
236361
fun getPatchedChain(keyId: KeyIdentifier): Array<Certificate>? = patchedChains[keyId]
237362

238363
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
@@ -249,6 +374,15 @@ class KeyMintSecurityLevelInterceptor(
249374
}
250375
}
251376

377+
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
378+
// Unregister from the native hook layer first.
379+
unregister(backdoor, operationBinder)
380+
381+
if (interceptedOperations.remove(operationBinder) != null) {
382+
SystemLogger.debug("Removed operation interceptor for binder: $operationBinder")
383+
}
384+
}
385+
252386
// Clears all cached keys.
253387
fun clearAllGeneratedKeys(reason: String? = null) {
254388
val count = generatedKeys.size

0 commit comments

Comments
 (0)