Compare commits
11
Commits
v5.1-153
...
v5.1.1-164
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e74ebbe82 | ||
|
|
315f41f434 | ||
|
|
9be0874e93 | ||
|
|
25dbddf733 | ||
|
|
9a7011eb5e | ||
|
|
b63570a3e2 | ||
|
|
3d7fd427a6 | ||
|
|
b2bf0ce599 | ||
|
|
63789ba29d | ||
|
|
40c7b6bd15 | ||
|
|
1df30b9345 |
@@ -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 ->
|
||||
|
||||
+9
-4
@@ -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,
|
||||
)
|
||||
|
||||
+83
-12
@@ -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)
|
||||
|
||||
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(
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
+56
-34
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user