Skip to content

Commit c209b4b

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 c209b4b

File tree

9 files changed

+495
-34
lines changed

9 files changed

+495
-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: 158 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,33 @@ 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(txId, transactionNames[code] ?: "unknown code=$code", callingUid, callingPid, true)
75+
7176
return TransactionResult.ContinueAndSkipPost
7277
}
7378

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

@@ -109,9 +149,12 @@ class KeyMintSecurityLevelInterceptor(
109149
// Cache the newly patched chain to ensure consistency across subsequent API calls.
110150
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
111151
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
152+
val key = metadata.key!!
112153
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
113154
patchedChains[keyId] = newChain
114-
SystemLogger.debug("Cached patched certificate chain for $keyId.")
155+
SystemLogger.debug(
156+
"Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])"
157+
)
115158

116159
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
117160

@@ -121,12 +164,59 @@ class KeyMintSecurityLevelInterceptor(
121164
return TransactionResult.SkipTransaction
122165
}
123166

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

150240
if (needsSoftwareGeneration) {
151-
SystemLogger.info("Generating software key for ${keyId}.")
241+
keyDescriptor.nspace = secureRandom.nextLong()
242+
SystemLogger.info(
243+
"Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}]."
244+
)
152245

153246
// Generate the key pair and certificate chain.
154247
val keyData =
@@ -164,16 +257,12 @@ class KeyMintSecurityLevelInterceptor(
164257
val response =
165258
buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
166259

167-
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, response)
260+
generatedKeys[keyId] =
261+
GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
168262
if (isAttestKeyRequest) attestationKeys.add(keyId)
169263

170264
// 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)
265+
return InterceptorUtils.createTypedObjectReply(response.metadata, 0)
177266
} else if (parsedParams.attestationChallenge != null) {
178267
return TransactionResult.Continue
179268
}
@@ -210,11 +299,18 @@ class KeyMintSecurityLevelInterceptor(
210299
}
211300

212301
companion object {
302+
private val secureRandom = SecureRandom()
303+
213304
// Transaction codes for IKeystoreSecurityLevel interface.
214305
private val GENERATE_KEY_TRANSACTION =
215306
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
216307
private val IMPORT_KEY_TRANSACTION =
217308
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "importKey")
309+
private val CREATE_OPERATION_TRANSACTION =
310+
InterceptorUtils.getTransactCode(
311+
IKeystoreSecurityLevel.Stub::class.java,
312+
"createOperation",
313+
)
218314

219315
private val transactionNames: Map<Int, String> by lazy {
220316
IKeystoreSecurityLevel.Stub::class
@@ -233,11 +329,30 @@ class KeyMintSecurityLevelInterceptor(
233329
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
234330
// A set to quickly identify keys that were generated for attestation purposes.
235331
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
332+
// Stores interceptors for active cryptographic operations.
333+
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
236334

237335
// --- Public Accessors for Other Interceptors ---
238336
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
239337
generatedKeys[keyId]?.response
240338

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

243358
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
@@ -254,6 +369,15 @@ class KeyMintSecurityLevelInterceptor(
254369
}
255370
}
256371

372+
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
373+
// Unregister from the native hook layer first.
374+
unregister(backdoor, operationBinder)
375+
376+
if (interceptedOperations.remove(operationBinder) != null) {
377+
SystemLogger.debug("Removed operation interceptor for binder: $operationBinder")
378+
}
379+
}
380+
257381
// Clears all cached keys.
258382
fun clearAllGeneratedKeys(reason: String? = null) {
259383
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)