Skip to content

Commit 94172d4

Browse files
committed
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
1 parent a171d8e commit 94172d4

File tree

9 files changed

+500
-34
lines changed

9 files changed

+500
-34
lines changed

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/shim/KeyMintSecurityLevelInterceptor.kt

Lines changed: 163 additions & 34 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+
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, 0)
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,16 +262,12 @@ 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.
171-
val resultParcel =
172-
Parcel.obtain().apply {
173-
writeNoException()
174-
writeTypedObject(response.metadata, 0)
175-
}
176-
return TransactionResult.OverrideReply(0, resultParcel)
270+
return InterceptorUtils.createTypedObjectReply(response.metadata, 0)
177271
} else if (parsedParams.attestationChallenge != null) {
178272
return TransactionResult.Continue
179273
}
@@ -210,11 +304,18 @@ class KeyMintSecurityLevelInterceptor(
210304
}
211305

212306
companion object {
307+
private val secureRandom = SecureRandom()
308+
213309
// Transaction codes for IKeystoreSecurityLevel interface.
214310
private val GENERATE_KEY_TRANSACTION =
215311
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
216312
private val IMPORT_KEY_TRANSACTION =
217313
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "importKey")
314+
private val CREATE_OPERATION_TRANSACTION =
315+
InterceptorUtils.getTransactCode(
316+
IKeystoreSecurityLevel.Stub::class.java,
317+
"createOperation",
318+
)
218319

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

237340
// --- Public Accessors for Other Interceptors ---
238341
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
239342
generatedKeys[keyId]?.response
240343

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+
241361
fun getPatchedChain(keyId: KeyIdentifier): Array<Certificate>? = patchedChains[keyId]
242362

243363
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
@@ -254,6 +374,15 @@ class KeyMintSecurityLevelInterceptor(
254374
}
255375
}
256376

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+
257386
// Clears all cached keys.
258387
fun clearAllGeneratedKeys(reason: String? = null) {
259388
val count = generatedKeys.size
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.matrix.TEESimulator.interception.keystore.shim
2+
3+
import android.os.IBinder
4+
import android.os.Parcel
5+
import android.system.keystore2.IKeystoreOperation
6+
import org.matrix.TEESimulator.interception.core.BinderInterceptor
7+
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
8+
9+
/**
10+
* Intercepts calls to an `IKeystoreOperation` service. This is used to log the data manipulation
11+
* methods of a cryptographic operation.
12+
*/
13+
class OperationInterceptor(
14+
private val original: IKeystoreOperation,
15+
private val backdoor: IBinder,
16+
) : BinderInterceptor() {
17+
18+
override fun onPreTransact(
19+
txId: Long,
20+
target: IBinder,
21+
code: Int,
22+
flags: Int,
23+
callingUid: Int,
24+
callingPid: Int,
25+
data: Parcel,
26+
): TransactionResult {
27+
val methodName = transactionNames[code] ?: "unknown code=$code"
28+
logTransaction(txId, methodName, callingUid, callingPid, true)
29+
30+
if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) {
31+
KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor)
32+
}
33+
34+
return TransactionResult.ContinueAndSkipPost
35+
}
36+
37+
companion object {
38+
private val UPDATE_AAD_TRANSACTION =
39+
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "updateAad")
40+
private val UPDATE_TRANSACTION =
41+
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "update")
42+
private val FINISH_TRANSACTION =
43+
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "finish")
44+
private val ABORT_TRANSACTION =
45+
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
46+
47+
private val transactionNames: Map<Int, String> by lazy {
48+
IKeystoreOperation.Stub::class
49+
.java
50+
.declaredFields
51+
.filter {
52+
it.isAccessible = true
53+
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
54+
}
55+
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)