Compare commits

..
11 Commits
Author SHA1 Message Date
Enginex0 6e74ebbe82 docs(release): add v5.1.1 changelog for pre-stash restoration fixes 2026-03-22 01:50:02 +01:00
Enginex0 315f41f434 fix(interception): align attest key nspace update with upstream #169 2026-03-22 01:12:25 +01:00
Enginex0 9be0874e93 fix(operation): fix missed finalized rename in abort() 2026-03-22 00:54:33 +01:00
Enginex0 25dbddf733 fix(interception): reject EC+DECRYPT in createOperation
EC keys don't support DECRYPT (only AGREE_KEY for key derivation).
Without this guard, an EC DECRYPT operation creates a CipherPrimitive
that fails with a confusing JCA error instead of returning
UNSUPPORTED_PURPOSE upfront.
2026-03-22 00:42:48 +01:00
Enginex0 9a7011eb5e fix(interception): restore key lifecycle tracking and cache invalidation
Restores pre-PR157 custom features:
- deletedSoftwareKeys tracking in Keystore2Interceptor (returns
  KEY_NOT_FOUND for getKeyEntry after software key deletion instead
  of falling through to hardware)
- invalidatePatchedChains() for bulk cert chain cache clearing
- Per-UID LRU operation pruning (MAX_CONCURRENT_OPS_PER_UID=15)
  prevents resource exhaustion from concurrent software operations
- SoftwareOperation.isFinalized made public for LRU eviction checks
2026-03-22 00:29:46 +01:00
Enginex0 b63570a3e2 fix(hardening): restore StrongBox simulation and binder buffer protection
Restores pre-PR157 custom hardening that was lost during the reset:
- StrongBox capability check (RSA<=2048, EC=P256)
- StrongBox keygen latency floor (250ms) and op latency floor (80ms)
- StrongBox concurrent op limit (4 ops in 10s sliding window)
- MAX_ALIAS_LENGTH (256KB) binder buffer guard in handleGenerateKey

Real StrongBox hardware has these constraints. Without simulation,
detectors identify the software shim by its unrealistic performance.
2026-03-22 00:17:10 +01:00
Enginex0 3d7fd427a6 fix(operation): restore CTR mode, AEAD guard, error code resolution, and latency floor
Restores pre-PR157 custom fixes to SoftwareOperation:
- CTR block mode in cipher algorithm mapping
- isAead guard on CipherPrimitive.updateAad (non-GCM throws INVALID_TAG)
- Reflection-based error code resolution for cross-HAL compatibility
- Granular exception mapping (SignatureException, BadPaddingException, etc.)
- latencyFloorMs constructor parameter with LockSupport.parkNanos in finish()
2026-03-22 00:08:41 +01:00
Enginex0 b2bf0ce599 fix(interception): restore noAuthRequired default and callerNonce attestation
NO_AUTH_REQUIRED should be added to authorizations when not explicitly
disabled (!= false), matching AOSP default behavior. The != null check
from PR157 drops the tag for keys where noAuthRequired was parsed as
null (e.g. persisted keys), creating an attestation/authorization
mismatch that detectors can spot.

Also restores CALLER_NONCE in softwareEnforced attestation list.
2026-03-22 00:06:28 +01:00
Enginex0 63789ba29d fix(attestation): restore presence-based findBoolean for KeyMint tags
KeyMint boolean tags (NO_AUTH_REQUIRED, CALLER_NONCE, etc.) use
presence-based semantics: tag exists = true, tag absent = null.
The .value?.boolValue approach fails on some AIDL implementations
where the boolValue field isn't populated despite the tag being
present, silently dropping boolean tags from attestations.
2026-03-22 00:05:34 +01:00
Enginex0 40c7b6bd15 fix(config): restore null-safe FileObserver and system=prop consistency
FileObserver DELETE events pass null for the file parameter. The
force-unwrap (file!!) from PR157 crashes the daemon when config
files are deleted. Restores safe-call with warning log.

Also restores system=prop cross-component consistency: when system
patch level is set to "prop", boot and vendor are forced to derive
from the same device property to prevent date mismatches.
2026-03-22 00:05:11 +01:00
Enginex0 1df30b9345 fix(persistence): restore boot hash file-based persistence
Boot hash was changing every reboot because the file-based fallback
from pre-PR157 was lost during the reset to upstream. Restores the
4-step resolution chain: sysprop -> TEE attestation -> persistent
file -> random (with persistence at each step). Also restores the
"prop" keyword for patch level resolution from system properties.
2026-03-22 00:04:06 +01:00
9 changed files with 236 additions and 81 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v5.1"
val verName = "v5.1.1"
android {
namespace = "org.matrix.TEESimulator"
@@ -516,6 +516,11 @@ object AttestationBuilder {
)
)
}
if (params.callerNonce == true) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)
)
}
if (params.unlockedDeviceRequired == true) {
list.add(
DERTaggedObject(
@@ -157,7 +157,7 @@ data class KeyMintAttestation(
/** Maps to AOSP field = Integer */
private fun Array<KeyParameter>.findBoolean(tag: Int): Boolean? =
this.find { it.tag == tag }?.value?.boolValue
if (this.any { it.tag == tag }) true else null
/** Maps to AOSP field = Integer */
private fun Array<KeyParameter>.findInteger(tag: Int): Int? =
@@ -250,9 +250,14 @@ object ConfigurationManager {
)
}
// Parse global and per-package configurations.
val newGlobalLevel = parseLines(contextLines[""])
contextLines.remove("") // Remove global context to iterate over packages next
var newGlobalLevel = parseLines(contextLines[""])
contextLines.remove("")
// system=prop means all components should derive from device props
if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) {
SystemLogger.info("system=prop: forcing boot/vendor to derive from device props")
newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop")
}
for ((pkg, lines) in contextLines) {
parseLines(lines)?.let { newPackageLevels[pkg] = it }
@@ -282,8 +287,10 @@ object ConfigurationManager {
val file = if (event != DELETE) File(configRoot, path) else null
when (path) {
TARGET_PACKAGES_FILE -> loadTargetPackages(file!!)
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!)
TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) }
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
PATCH_LEVEL_FILE -> file?.let { loadPatchLevelConfig(it) }
?: SystemLogger.warning("$PATCH_LEVEL_FILE was deleted.")
// Any change to an XML file is assumed to be a keybox.
// The cache in KeyBoxManager will handle reloading it on its next use.
else ->
@@ -61,8 +61,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
// Keys whose certs were updated via updateSubcomponent; skip re-patching on getKeyEntry.
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
private val deletedSoftwareKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
// Backdoor binder for registering new interceptors at runtime.
private var backdoorBinder: IBinder? = null
@@ -225,6 +225,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId)
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
if (isSoftwareKey) {
deletedSoftwareKeys.add(keyId)
SystemLogger.info(
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
)
@@ -239,6 +240,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
}
val keyId = KeyIdentifier(callingUid, descriptor.alias)
if (deletedSoftwareKeys.remove(keyId)) {
return InterceptorUtils.createErrorReply(7) // KEY_NOT_FOUND
}
val response =
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
?: return TransactionResult.Continue
@@ -387,13 +392,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
callingUid,
)
val newNspace = SecureRandom().nextLong()
response.metadata.key?.let { it.nspace = newNspace }
val key = response.metadata.key!!
key.nspace = SecureRandom().nextLong()
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
keyData.first,
null,
newNspace,
key.nspace,
response,
parsedParameters,
)
@@ -1,6 +1,7 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue
@@ -20,7 +21,10 @@ import java.security.KeyFactory
import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.LockSupport
import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.AttestationConstants
import org.matrix.TEESimulator.attestation.AttestationPatcher
@@ -47,7 +51,9 @@ class KeyMintSecurityLevelInterceptor(
private val securityLevel: Int,
) : BinderInterceptor() {
// --- Data Structures for State Management ---
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
data class GeneratedKeyInfo(
val keyPair: KeyPair?,
val secretKey: javax.crypto.SecretKey?,
@@ -233,11 +239,31 @@ 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 trackAndEnforceOpLimit(callingUid: Int, securityLevel: Int) {
if (securityLevel != SecurityLevel.STRONGBOX) return
val timestamps = recentOps.computeIfAbsent(callingUid) { ConcurrentLinkedDeque() }
val cutoff = System.nanoTime() - STRONGBOX_OP_WINDOW_NS
timestamps.removeIf { it < cutoff }
if (timestamps.size >= STRONGBOX_MAX_CONCURRENT_OPS) {
throw android.os.ServiceSpecificException(
KEYMINT_TOO_MANY_OPERATIONS,
"StrongBox op limit reached for uid=$callingUid"
)
}
timestamps.addLast(System.nanoTime())
}
private fun pruneOpsForUid(callingUid: Int, newOp: SoftwareOperation) {
val ops = activeOps.computeIfAbsent(callingUid) { ConcurrentLinkedDeque() }
ops.removeIf { it.isFinalized }
while (ops.size >= MAX_CONCURRENT_OPS_PER_UID) {
val oldest = ops.pollFirst() ?: break
oldest.abort()
SystemLogger.debug("Pruned oldest op for uid=$callingUid (LRU eviction)")
}
ops.addLast(newOp)
}
private fun handleCreateOperation(
txId: Long,
callingUid: Int,
@@ -279,6 +305,8 @@ class KeyMintSecurityLevelInterceptor(
"[TX_ID: $txId] Creating SOFTWARE operation for key ${generatedKeyInfo.nspace}."
)
trackAndEnforceOpLimit(callingUid, securityLevel)
val opParams = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedOpParams = KeyMintAttestation(opParams)
val forced = data.readBoolean()
@@ -300,7 +328,8 @@ class KeyMintSecurityLevelInterceptor(
(isAsymmetric &&
(requestedPurpose == KeyPurpose.VERIFY ||
requestedPurpose == KeyPurpose.ENCRYPT)) ||
(requestedPurpose == KeyPurpose.AGREE_KEY && algorithm != Algorithm.EC)
(requestedPurpose == KeyPurpose.AGREE_KEY && algorithm != Algorithm.EC) ||
(algorithm == Algorithm.EC && requestedPurpose == KeyPurpose.DECRYPT)
if (unsupported) {
return InterceptorUtils.createServiceSpecificErrorReply(
KeystoreErrorCode.UNSUPPORTED_PURPOSE
@@ -371,13 +400,16 @@ class KeyMintSecurityLevelInterceptor(
// override purpose from the operation params.
val effectiveParams =
keyParams.copy(purpose = parsedOpParams.purpose, digest = parsedOpParams.digest.ifEmpty { keyParams.digest })
val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L
val softwareOperation =
SoftwareOperation(
txId,
generatedKeyInfo.keyPair,
generatedKeyInfo.secretKey,
effectiveParams,
opLatency,
)
pruneOpsForUid(callingUid, softwareOperation)
// Decrement usage counter on finish; delete key when exhausted.
if (keyParams.usageCountLimit != null && resolvedKeyId != null) {
@@ -424,6 +456,11 @@ class KeyMintSecurityLevelInterceptor(
* either generates a key in software or lets the call pass through to the hardware.
*/
private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult {
if (data.dataSize() > MAX_ALIAS_LENGTH) {
SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes")
return InterceptorUtils.createErrorReply(KEYMINT_INVALID_INPUT_LENGTH)
}
return runCatching {
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
@@ -494,6 +531,11 @@ class KeyMintSecurityLevelInterceptor(
val isStrongBox = securityLevel == SecurityLevel.STRONGBOX
if (isStrongBox && !isStrongBoxCapable(parsedParams)) {
SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}), forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
when {
forceGenerate -> doSoftwareGeneration(
callingUid, keyDescriptor, attestationKey, parsedParams, isAttestKeyRequest
@@ -571,9 +613,15 @@ class KeyMintSecurityLevelInterceptor(
}
generatedKeys[keyId] =
GeneratedKeyInfo(null, secretKey, keyDescriptor.nspace, response, parsedParams)
TeeLatencySimulator.simulateGenerateKeyDelay(
parsedParams.algorithm, System.nanoTime() - genStartNanos
)
if (securityLevel == SecurityLevel.STRONGBOX) {
val delayMs = STRONGBOX_KEYGEN_LATENCY_FLOOR_MS - (System.nanoTime() - genStartNanos) / 1_000_000
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
} else {
TeeLatencySimulator.simulateGenerateKeyDelay(
parsedParams.algorithm, System.nanoTime() - genStartNanos
)
}
return InterceptorUtils.createTypedObjectReply(metadata)
}
@@ -613,9 +661,14 @@ class KeyMintSecurityLevelInterceptor(
)
}
TeeLatencySimulator.simulateGenerateKeyDelay(
parsedParams.algorithm, System.nanoTime() - genStartNanos
)
if (securityLevel == SecurityLevel.STRONGBOX) {
val delayMs = STRONGBOX_KEYGEN_LATENCY_FLOOR_MS - (System.nanoTime() - genStartNanos) / 1_000_000
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
} else {
TeeLatencySimulator.simulateGenerateKeyDelay(
parsedParams.algorithm, System.nanoTime() - genStartNanos
)
}
return InterceptorUtils.createTypedObjectReply(response.metadata)
}
@@ -900,11 +953,25 @@ class KeyMintSecurityLevelInterceptor(
private const val KEYMINT_INVALID_INPUT_LENGTH = -21
private const val KEYMINT_INVALID_ARGUMENT = -38
private const val KEYMINT_TOO_MANY_OPERATIONS = -29
private const val INVALID_ARGUMENT = 20
private const val PERMISSION_DENIED = 6
private const val SECURE_HW_COMMUNICATION_FAILED = -49
private const val CANNOT_ATTEST_IDS = -66
private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L
private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L
private const val STRONGBOX_MAX_CONCURRENT_OPS = 4
private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L
private const val MAX_ALIAS_LENGTH = 256 * 1024
private const val MAX_CONCURRENT_OPS_PER_UID = 15
private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) {
Algorithm.RSA -> params.keySize <= 2048
Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256
else -> true
}
// Transaction codes for IKeystoreSecurityLevel interface.
private val GENERATE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
@@ -998,7 +1065,13 @@ class KeyMintSecurityLevelInterceptor(
}
}
// Clears all cached keys.
fun invalidatePatchedChains(reason: String? = null) {
val count = patchedChains.size
if (count == 0) return
patchedChains.clear()
SystemLogger.info("Invalidated $count patched cert chains${reason?.let { " due to $it" } ?: ""}.")
}
fun clearAllGeneratedKeys(reason: String? = null) {
val count = generatedKeys.size
val reasonMessage = reason?.let { " due to $it" } ?: ""
@@ -1069,10 +1142,8 @@ private fun KeyMintAttestation.toAuthorizations(
)
}
if (this.noAuthRequired != null) {
authList.add(
createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(this.noAuthRequired))
)
if (this.noAuthRequired != false) {
authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true)))
}
if (this.callerNonce == true) {
@@ -15,36 +15,38 @@ import android.system.keystore2.KeyParameters
import java.security.KeyPair
import java.security.Signature
import java.security.SignatureException
import java.util.concurrent.locks.LockSupport
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger
/** Keystore2 error codes for ServiceSpecificException. Negative = KeyMint, positive = Keystore. */
internal object KeystoreErrorCode {
const val INVALID_OPERATION_HANDLE = -28
const val VERIFICATION_FAILED = -30
const val UNSUPPORTED_PURPOSE = -2
const val INCOMPATIBLE_PURPOSE = -3
const val SYSTEM_ERROR = 4
const val TOO_MUCH_DATA = 21
const val KEY_EXPIRED = -25
const val KEY_NOT_YET_VALID = -24
val INVALID_OPERATION_HANDLE: Int by lazy { resolve("ErrorCode", "INVALID_OPERATION_HANDLE", -28) }
val VERIFICATION_FAILED: Int by lazy { resolve("ErrorCode", "VERIFICATION_FAILED", -30) }
val UNSUPPORTED_PURPOSE: Int by lazy { resolve("ErrorCode", "UNSUPPORTED_PURPOSE", -2) }
val INCOMPATIBLE_PURPOSE: Int by lazy { resolve("ErrorCode", "INCOMPATIBLE_PURPOSE", -3) }
val INVALID_ARGUMENT: Int by lazy { resolve("ErrorCode", "INVALID_ARGUMENT", -38) }
val INVALID_TAG: Int by lazy { resolve("ErrorCode", "INVALID_TAG", -40) }
val INVALID_INPUT_LENGTH: Int by lazy { resolve("ErrorCode", "INVALID_INPUT_LENGTH", -21) }
val INCOMPATIBLE_KEY: Int by lazy { resolve("ErrorCode", "INCOMPATIBLE_KEY", -31) }
val INCOMPATIBLE_ALGORITHM: Int by lazy { resolve("ErrorCode", "INCOMPATIBLE_ALGORITHM", -18) }
val KEY_EXPIRED: Int by lazy { resolve("ErrorCode", "KEY_EXPIRED", -25) }
val KEY_NOT_YET_VALID: Int by lazy { resolve("ErrorCode", "KEY_NOT_YET_VALID", -24) }
val CALLER_NONCE_PROHIBITED: Int by lazy { resolve("ErrorCode", "CALLER_NONCE_PROHIBITED", -55) }
val UNKNOWN_ERROR: Int by lazy { resolve("ErrorCode", "UNKNOWN_ERROR", -1000) }
val SYSTEM_ERROR: Int by lazy { resolve("ResponseCode", "SYSTEM_ERROR", 4, keystore = true) }
val TOO_MUCH_DATA: Int by lazy { resolve("ResponseCode", "TOO_MUCH_DATA", 21, keystore = true) }
val PERMISSION_DENIED: Int by lazy { resolve("ResponseCode", "PERMISSION_DENIED", 6, keystore = true) }
val KEY_NOT_FOUND: Int by lazy { resolve("ResponseCode", "KEY_NOT_FOUND", 7, keystore = true) }
/** KeyMint ErrorCode::CALLER_NONCE_PROHIBITED */
const val CALLER_NONCE_PROHIBITED = -55
/** KeyMint ErrorCode::INVALID_ARGUMENT */
const val INVALID_ARGUMENT = -38
/** KeyMint ErrorCode::INVALID_TAG */
const val INVALID_TAG = -40
/** Keystore2 ResponseCode::PERMISSION_DENIED */
const val PERMISSION_DENIED = 6
/** Keystore2 ResponseCode::KEY_NOT_FOUND */
const val KEY_NOT_FOUND = 7
private fun resolve(enumName: String, field: String, fallback: Int, keystore: Boolean = false): Int {
val pkg = if (keystore) "android.system.keystore2" else "android.hardware.security.keymint"
return runCatching { Class.forName("$pkg.$enumName").getField(field).getInt(null) }
.getOrDefault(fallback)
}
}
// A sealed interface to represent the different cryptographic operations we can perform.
@@ -99,8 +101,9 @@ private object JcaAlgorithmMapper {
when (params.blockMode.firstOrNull()) {
BlockMode.ECB -> "ECB"
BlockMode.CBC -> "CBC"
BlockMode.CTR -> "CTR"
BlockMode.GCM -> "GCM"
else -> "ECB" // Default for RSA
else -> "ECB"
}
val padding =
when (params.padding.firstOrNull()) {
@@ -183,8 +186,10 @@ private class CipherPrimitive(
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
init(opMode, cryptoKey)
}
private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM
override fun updateAad(data: ByteArray?) {
if (!isAead) throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
if (data != null) cipher.updateAAD(data)
}
@@ -247,11 +252,13 @@ class SoftwareOperation(
keyPair: KeyPair?,
secretKey: javax.crypto.SecretKey?,
params: KeyMintAttestation,
private val latencyFloorMs: Long = 0L,
var onFinishCallback: (() -> Unit)? = null,
) {
private val primitive: CryptoPrimitive
@Volatile private var finalized = false
@Volatile var isFinalized = false
private set
init {
val purpose = params.purpose.firstOrNull()
@@ -288,7 +295,7 @@ class SoftwareOperation(
}
private fun checkActive() {
if (finalized)
if (isFinalized)
throw ServiceSpecificException(
KeystoreErrorCode.INVALID_OPERATION_HANDLE,
"Operation already finalized.",
@@ -300,10 +307,10 @@ class SoftwareOperation(
try {
primitive.updateAad(data)
} catch (e: ServiceSpecificException) {
finalized = true
isFinalized = true
throw e
} catch (e: Exception) {
finalized = true
isFinalized = true
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to updateAad.", e)
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
}
@@ -314,35 +321,50 @@ class SoftwareOperation(
try {
return primitive.update(data)
} catch (e: ServiceSpecificException) {
finalized = true
isFinalized = true
throw e
} catch (e: Exception) {
finalized = true
isFinalized = true
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
throw mapToServiceSpecificException(e)
}
}
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
checkActive()
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
try {
val result = primitive.finish(data, signature)
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
if (latencyFloorMs > 0) {
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
val delayMs = latencyFloorMs - elapsedMs
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
}
onFinishCallback?.invoke()
return result
} catch (e: ServiceSpecificException) {
throw e
} catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
throw mapToServiceSpecificException(e)
} finally {
finalized = true
isFinalized = true
}
}
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) {
is ServiceSpecificException -> e
is SignatureException -> ServiceSpecificException(KeystoreErrorCode.VERIFICATION_FAILED, e.message)
is BadPaddingException -> ServiceSpecificException(KeystoreErrorCode.INVALID_ARGUMENT, e.message)
is IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCode.INVALID_INPUT_LENGTH, e.message)
is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCode.INCOMPATIBLE_KEY, e.message)
else -> ServiceSpecificException(KeystoreErrorCode.UNKNOWN_ERROR, e.message)
}
fun abort() {
checkActive()
finalized = true
isFinalized = true
primitive.abort()
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
}
@@ -76,42 +76,38 @@ object AndroidDeviceUtils {
SystemLogger.debug("Boot key and hash initialization complete.")
}
/**
* Generic initializer for boot properties like the key and hash. It attempts to read from a
* system property first, then from a TEE attestation, and finally falls back to a random value
* if neither is available.
*
* @param propertyName The name of the system property (e.g., "ro.boot.vbmeta.digest").
* @param attestationValueProvider A function that supplies the value from a cached attestation.
* @param expectedSize The expected length of the byte array (e.g., 32 for a SHA-256 digest).
* @return The resulting byte array for the property.
*/
private fun initializeBootProperty(
propertyName: String,
attestationValueProvider: () -> ByteArray?,
expectedSize: Int,
): ByteArray {
// 1. Attempt to get the value from the system property.
getProperty(propertyName, expectedSize)?.let {
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
persistToFile(propertyName, it)
return it
}
// 2. Fallback to the value from a cached TEE attestation.
try {
attestationValueProvider()?.let {
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
setProperty(propertyName, it) // Persist for consistency
setProperty(propertyName, it)
persistToFile(propertyName, it)
return it
}
} catch (e: Exception) {
SystemLogger.error("Failed to get $propertyName from attestation.", e)
}
// 3. As a final fallback, generate a random value.
readFromFile(propertyName, expectedSize)?.let {
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
setProperty(propertyName, it)
return it
}
return generateRandomBytes(expectedSize).also {
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
setProperty(propertyName, it)
persistToFile(propertyName, it)
}
}
@@ -158,10 +154,37 @@ object AndroidDeviceUtils {
}
}
/** Generates a cryptographically random byte array of a specified length. */
private fun generateRandomBytes(size: Int): ByteArray =
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
private val PERSIST_DIR = File("/data/adb/tricky_store")
private fun fileForProperty(propertyName: String): File = when (propertyName) {
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.bin")
}
private fun persistToFile(propertyName: String, bytes: ByteArray) {
try {
fileForProperty(propertyName).writeBytes(bytes)
} catch (e: Exception) {
SystemLogger.error("Failed to persist $propertyName to file.", e)
}
}
private fun readFromFile(propertyName: String, expectedSize: Int): ByteArray? {
return try {
val file = fileForProperty(propertyName)
if (!file.exists()) return null
val bytes = file.readBytes()
if (bytes.size == expectedSize) bytes else null
} catch (e: Exception) {
SystemLogger.error("Failed to read $propertyName from file.", e)
null
}
}
// --- Patch Level Properties ---
fun getPatchLevel(uid: Int): Int {
@@ -240,11 +263,10 @@ object AndroidDeviceUtils {
val resolvedValue = resolveDateKeywords(value)
return when {
// "device_default" indicates falling back to the system property.
resolvedValue.equals("device_default", ignoreCase = true) -> null
// "no" indicates this value should not be reported.
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
// Otherwise, parse the resolved date string.
resolvedValue.equals("prop", ignoreCase = true) ->
parsePatchLevelValue(SystemProperties.get("ro.build.version.security_patch", ""), isLong)
else -> parsePatchLevelValue(resolvedValue, isLong)
}
}
+23
View File
@@ -1,3 +1,26 @@
## TEESimulator-RS v5.1.1: Pre-Stash Restoration
Restores all custom hardening fixes that were lost during the PR #157 migration. These were working in pre-stash builds but never carried over to the post-stash codebase, causing user-reported regressions (boot hash instability, DuckDetector score regression, config crash on file deletion).
- **Boot hash persistence** restored: 4-step fallback (sysprop, TEE, file, random) with file writes at every step. Fixes "Boot: Unavailable" where boot hash randomized every reboot on devices without `ro.boot.vbmeta.digest`
- **Presence-based findBoolean** for KeyMint tags: boolean tags are presence-based per AIDL spec, `.boolValue` field isn't reliably populated across Android versions
- **noAuthRequired** defaults to true when not explicitly false, matching AOSP KeyMint behavior
- **callerNonce** tag now flows through to software-enforced attestation list
- **CTR block mode** restored in cipher algorithm mapping (was dropped in PR #157)
- **AEAD guard** on updateAad: non-GCM operations throw INVALID_TAG
- **Error code resolution** via lazy reflection with correct KeyMint AIDL fallback values
- **Latency floor** on SoftwareOperation.finish() for StrongBox timing simulation
- **FileObserver NPE** fixed: null-safe handling on config file DELETE events
- **system=prop** consistency: forces boot/vendor patch levels to derive from device props
- **StrongBox simulation** restored: capability checks (RSA<=2048, EC=P256), concurrent op limits (4 max), keygen latency floor (250ms), op latency floor (80ms)
- **Binder buffer guard**: MAX_ALIAS_LENGTH (256KB) rejects oversized aliases before processing
- **Key lifecycle tracking**: deletedSoftwareKeys set prevents ghost key responses after deletion
- **Per-UID operation limits**: 15 TEE, 4 StrongBox with LRU eviction
- **EC+DECRYPT rejection** in createOperation, matching AOSP unsupported purpose check
- **Attest key nspace update** aligned with upstream PR #169
---
## TEESimulator-RS v5.1: Interception Architecture Rewrite
Major release. 27 files changed, 2300 lines rewritten. The entire Kotlin interception layer has been rebuilt with a clean architecture, proper AIDL alignment, and significantly lower binder overhead.