style: apply ktfmt formatting pass

Run the project ktfmt kotlinLangStyle formatter over app/ to bring the
tree into canonical form. Formatting only -- no logic change.

Verified semantic-neutral: ktfmt(working tree) is byte-identical to
ktfmt(committed HEAD) across all of app/src, so the prior uncommitted
WIP carried zero behavioral change.
This commit is contained in:
Enginex0
2026-06-04 12:55:07 +01:00
parent 5bd563d8db
commit 5c300ff47b
20 changed files with 1688 additions and 1209 deletions
+25 -14
View File
@@ -66,11 +66,7 @@ android {
}
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
}
}
kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }
dependencies {
compileOnly(project(":stub"))
@@ -79,17 +75,22 @@ dependencies {
}
// --- Rust native cert gen build task ---
val buildRustCertgen by tasks.registering(Exec::class) {
val buildRustCertgen by
tasks.registering(Exec::class) {
group = "TEESimulator-RS Native Build"
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
workingDir = rootProject.projectDir.resolve("native-certgen")
commandLine(
"cargo", "ndk",
"-t", "arm64-v8a",
"-o", rootProject.projectDir.resolve("app/src/main/jniLibs").absolutePath,
"build", "--release"
"cargo",
"ndk",
"-t",
"arm64-v8a",
"-o",
rootProject.projectDir.resolve("app/src/main/jniLibs").absolutePath,
"build",
"--release",
)
inputs.dir(rootProject.projectDir.resolve("native-certgen/src"))
@@ -98,7 +99,10 @@ val buildRustCertgen by tasks.registering(Exec::class) {
outputs.dir(rootProject.projectDir.resolve("app/src/main/jniLibs"))
environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath)
environment("PATH", "${System.getProperty("user.home")}/.cargo/bin:${System.getenv("PATH") ?: ""}")
environment(
"PATH",
"${System.getProperty("user.home")}/.cargo/bin:${System.getenv("PATH") ?: ""}",
)
}
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
@@ -110,7 +114,8 @@ tasks.configureEach {
// Auto-rewrite module/update.json on every packaging build so versionCode and
// zipUrl track gitCommitCount automatically, matching module.prop.
val refreshUpdateJson by tasks.registering {
val refreshUpdateJson by
tasks.registering {
group = "TEESimulator-RS Module Packaging"
description = "Rewrite module/update.json to match current verName and gitCommitCount."
@@ -177,14 +182,20 @@ androidComponents {
}
}
val nativeLibsDir = if (isDebug) {
val nativeLibsDir =
if (isDebug) {
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
} else {
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
}
from(project.layout.buildDirectory.dir(nativeLibsDir)) {
into("lib")
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
include(
"**/libinject.so",
"**/libTEESimulator.so",
"**/libsupervisor.so",
"**/libcertgen.so",
)
}
// Now, copy and process the files from 'module' directory.
@@ -74,9 +74,9 @@ object App {
}
/**
* Release builds never emit diagnostics. Sweep any `.bin` dumps a prior
* debug install left in the world-readable temp dir so they can't act as a
* detection artifact for apps that probe /data/local/tmp.
* Release builds never emit diagnostics. Sweep any `.bin` dumps a prior debug install left in
* the world-readable temp dir so they can't act as a detection artifact for apps that probe
* /data/local/tmp.
*/
private fun purgeDebugDiagnostics() {
if (SystemLogger.isDebugBuild) return
@@ -88,7 +88,9 @@ object App {
if (stale.isNotEmpty()) {
// warning() bypasses the rate limiter, so this once-per-boot audit
// line survives the noisy startup window.
SystemLogger.warning("Purged ${stale.size} stale debug diagnostic(s) from /data/local/tmp")
SystemLogger.warning(
"Purged ${stale.size} stale debug diagnostic(s) from /data/local/tmp"
)
}
}
@@ -44,7 +44,8 @@ object AttestationBuilder {
): Extension {
val keyDescription = buildKeyDescription(params, uid, securityLevel)
SystemLogger.verbose {
val formattedString = keyDescription.joinToString(separator = ", ") {
val formattedString =
keyDescription.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it)
}
"Forged attestation data: $formattedString"
@@ -116,7 +117,9 @@ object AttestationBuilder {
}
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch")
SystemLogger.info(
"Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch"
)
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
if (bootPatch != DO_NOT_REPORT) {
DERTaggedObject(
@@ -268,7 +271,11 @@ object AttestationBuilder {
if (params.rollbackResistance == true && attestVersion >= 3) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_ROLLBACK_RESISTANCE, DERNull.INSTANCE)
DERTaggedObject(
true,
AttestationConstants.TAG_ROLLBACK_RESISTANCE,
DERNull.INSTANCE,
)
)
}
@@ -286,19 +293,31 @@ object AttestationBuilder {
if (params.allowWhileOnBody == true) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_ALLOW_WHILE_ON_BODY, DERNull.INSTANCE)
DERTaggedObject(
true,
AttestationConstants.TAG_ALLOW_WHILE_ON_BODY,
DERNull.INSTANCE,
)
)
}
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED, DERNull.INSTANCE)
DERTaggedObject(
true,
AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED,
DERNull.INSTANCE,
)
)
}
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED, DERNull.INSTANCE)
DERTaggedObject(
true,
AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED,
DERNull.INSTANCE,
)
)
}
@@ -449,33 +468,51 @@ object AttestationBuilder {
}
if (params.callerNonce == true) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)
)
list.add(DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE))
}
params.activeDateTime?.let {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
DERTaggedObject(
true,
AttestationConstants.TAG_ACTIVE_DATETIME,
ASN1Integer(it.time),
)
)
}
params.originationExpireDateTime?.let {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME, ASN1Integer(it.time))
DERTaggedObject(
true,
AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME,
ASN1Integer(it.time),
)
)
}
params.usageExpireDateTime?.let {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_USAGE_EXPIRE_DATETIME, ASN1Integer(it.time))
DERTaggedObject(
true,
AttestationConstants.TAG_USAGE_EXPIRE_DATETIME,
ASN1Integer(it.time),
)
)
}
params.usageCountLimit?.let {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_USAGE_COUNT_LIMIT, ASN1Integer(it.toLong()))
DERTaggedObject(
true,
AttestationConstants.TAG_USAGE_COUNT_LIMIT,
ASN1Integer(it.toLong()),
)
)
}
if (params.unlockedDeviceRequired == true) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED, DERNull.INSTANCE)
DERTaggedObject(
true,
AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED,
DERNull.INSTANCE,
)
)
}
@@ -4,6 +4,7 @@ import android.security.keystore.KeyProperties
import java.nio.charset.StandardCharsets
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import java.util.Date
import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.cert.X509CertificateHolder
@@ -16,7 +17,6 @@ import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.KeyBox
import org.matrix.TEESimulator.pki.KeyBoxManager
import org.matrix.TEESimulator.util.toHex
import java.util.Date
/**
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
@@ -287,7 +287,8 @@ object AttestationPatcher {
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
SystemLogger.verbose {
val formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
val formattedString =
allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
"Original attestation data: $formattedString"
}
@@ -317,7 +318,8 @@ object AttestationPatcher {
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
val patchedSequence = DERSequence(allFields)
SystemLogger.verbose {
val formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
val formattedString =
patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
"Patched attestation data: $formattedString"
}
val patchedOctets = DEROctetString(patchedSequence)
@@ -142,7 +142,8 @@ data class KeyMintAttestation(
fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
fun isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
fun isImportKey(): Boolean =
origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
}
// --- Private helper extension functions for parsing KeyParameter arrays ---
@@ -96,7 +96,8 @@ object ConfigurationManager {
fun isAutoMode(uid: Int): Boolean {
for (pkg in getPackagesForUid(uid)) {
when (packageModes[pkg]) {
Mode.GENERATE, Mode.PATCH -> return false
Mode.GENERATE,
Mode.PATCH -> return false
Mode.AUTO -> return true
null -> continue
}
@@ -112,7 +113,9 @@ object ConfigurationManager {
when (packageModes[pkg]) {
Mode.GENERATE -> return Mode.GENERATE
Mode.PATCH -> return Mode.PATCH
Mode.AUTO -> return if (DeviceAttestationService.isTeeFunctional) Mode.PATCH else Mode.GENERATE
Mode.AUTO ->
return if (DeviceAttestationService.isTeeFunctional) Mode.PATCH
else Mode.GENERATE
null -> continue
}
}
@@ -260,7 +263,9 @@ object ConfigurationManager {
// resolves to the real device prop — force boot/vendor through the same path
// to prevent cross-component date mismatches on non-Pixel devices.
if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) {
SystemLogger.info("system=prop: forcing boot/vendor to derive from device props (were: boot=${newGlobalLevel?.boot}, vendor=${newGlobalLevel?.vendor})")
SystemLogger.info(
"system=prop: forcing boot/vendor to derive from device props (were: boot=${newGlobalLevel?.boot}, vendor=${newGlobalLevel?.vendor})"
)
newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop")
}
contextLines.remove("") // Remove global context to iterate over packages next
@@ -293,9 +298,11 @@ object ConfigurationManager {
val file = if (event != DELETE) File(configRoot, path) else null
when (path) {
TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) }
TARGET_PACKAGES_FILE ->
file?.let { loadTargetPackages(it) }
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
PATCH_LEVEL_FILE -> file?.let { loadPatchLevelConfig(it) }
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.
@@ -110,14 +110,18 @@ abstract class BinderInterceptor : Binder() {
*/
final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
val txId = data.readLong()
val result = try {
val result =
try {
when (code) {
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
else -> return super.onTransact(code, data, reply, flags)
}
} catch (e: Throwable) {
SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e)
SystemLogger.error(
"[TX_ID: $txId] Interceptor exception, falling through to HAL",
e,
)
TransactionResult.ContinueAndSkipPost
}
writeResultToReply(result, reply!!)
@@ -307,7 +311,9 @@ abstract class BinderInterceptor : Binder() {
data.writeInt(filteredCodes.size)
for (code in filteredCodes) data.writeInt(code)
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
SystemLogger.info("Registered interceptor for target: $target (${filteredCodes.size} filtered codes)")
SystemLogger.info(
"Registered interceptor for target: $target (${filteredCodes.size} filtered codes)"
)
} catch (e: Exception) {
SystemLogger.error("Failed to register binder interceptor.", e)
} finally {
@@ -37,7 +37,8 @@ object InterceptorUtils {
}
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
val parcel = Parcel.obtain().apply {
val parcel =
Parcel.obtain().apply {
writeInt(EX_SERVICE_SPECIFIC)
writeString(synthesizeSseMessage(errorCode))
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
@@ -120,7 +120,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
Keystore2MaintenanceInterceptor,
Keystore2MaintenanceInterceptor.interceptedCodes,
)
} ?: SystemLogger.warning("Maintenance binder not found; skipping lifecycle parity.")
}
?: SystemLogger.warning(
"Maintenance binder not found; skipping lifecycle parity."
)
}
.onFailure { SystemLogger.error("Failed to intercept maintenance binder.", it) }
}
@@ -219,17 +222,23 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
?: return TransactionResult.ContinueAndSkipPost
// Domain.GRANT read (Android 16+ KeyStoreManager grant). Served for ANY grantee uid —
// including isolated services (bindIsolatedService) with no package mapping — so resolve
// it before the package-scoped skip; caller-binding in resolveGrant() is the real access
// gate. On Android <= 15 no grants are ever issued (grant() denies), so softwareGrants is
// including isolated services (bindIsolatedService) with no package mapping — so
// resolve
// it before the package-scoped skip; caller-binding in resolveGrant() is the real
// access
// gate. On Android <= 15 no grants are ever issued (grant() denies), so softwareGrants
// is
// empty and this falls through to the real keystore2.
if (code == GET_KEY_ENTRY_TRANSACTION && descriptor.domain == Domain.GRANT) {
val grant =
KeyMintSecurityLevelInterceptor.resolveGrant(descriptor.nspace, callingUid)
if (grant == null) {
// Ours but wrong caller -> KEY_NOT_FOUND (caller-binding); not ours -> real keystore2.
// Ours but wrong caller -> KEY_NOT_FOUND (caller-binding); not ours -> real
// keystore2.
return if (
KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(descriptor.nspace)
KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(
descriptor.nspace
)
)
InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
else TransactionResult.ContinueAndSkipPost
@@ -253,10 +262,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
KeyIdentifier(callingUid, descriptor.alias)
} else if (descriptor.domain == Domain.KEY_ID) {
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
callingUid, descriptor.nspace
)?.let { info ->
callingUid,
descriptor.nspace,
)
?.let { info ->
KeyMintSecurityLevelInterceptor.generatedKeys.entries
.find { it.value.nspace == info.nspace && it.key.uid == callingUid }
.find {
it.value.nspace == info.nspace && it.key.uid == callingUid
}
?.key
}
} else null
@@ -289,8 +302,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
// "Captured private binder exception during timing skip".
// Resolving by KEY_ID and returning the cached response keeps
// the call on the happy path, eliminating the warmup signal.
val info = KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
callingUid, descriptor.nspace
val info =
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
callingUid,
descriptor.nspace,
)
if (info?.response != null) {
SystemLogger.info(
@@ -298,8 +313,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
)
return InterceptorUtils.createTypedObjectReply(info.response)
}
val teeResp = KeyMintSecurityLevelInterceptor.findTeeResponseByKeyId(
callingUid, descriptor.nspace
val teeResp =
KeyMintSecurityLevelInterceptor.findTeeResponseByKeyId(
callingUid,
descriptor.nspace,
)
if (teeResp != null) {
SystemLogger.info(
@@ -309,7 +326,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
}
}
// Domain.GRANT is handled earlier (before the package-scoped skip); an alias-less
// read reaching here is KEY_ID or unknown, so it falls through to the real keystore2.
// read reaching here is KEY_ID or unknown, so it falls through to the real
// keystore2.
return TransactionResult.ContinueAndSkipPost
}
val keyId = KeyIdentifier(callingUid, descriptor.alias)
@@ -317,7 +335,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
if (response == null) {
if (deletedSoftwareKeys.remove(keyId)) {
SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}")
SystemLogger.info(
"[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}"
)
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
}
return TransactionResult.Continue
@@ -339,14 +359,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
?: return TransactionResult.ContinueAndSkipPost
val granteeUid = data.readInt()
val accessVector = data.readInt()
// Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must grant
// coherently so the Domain.GRANT readback returns the same chain the owner read returns.
// Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must
// grant
// coherently so the Domain.GRANT readback returns the same chain the owner read
// returns.
// Real hardware keys fall through to the real keystore2, which applies the same SELinux
// gate the platform would.
val ownerKeyId =
resolveOwnerKeyId(key, callingUid)
?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
?: return TransactionResult.ContinueAndSkipPost
resolveOwnerKeyId(key, callingUid)?.takeIf {
KeyMintSecurityLevelInterceptor.ownsKeyResponse(it)
} ?: return TransactionResult.ContinueAndSkipPost
// Version-gated to mirror the real TEE 1:1. Pre-Android-16, grant was a hidden API and
// SELinux denied untrusted_app, so keystore2 returns PERMISSION_DENIED. Android 16
// (API 36) exposes KeyStoreManager.grantKeyAccess(), so an app grants its own key:
@@ -373,9 +395,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
?: return TransactionResult.ContinueAndSkipPost
val granteeUid = data.readInt()
val ownerKeyId =
resolveOwnerKeyId(key, callingUid)
?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
?: return TransactionResult.ContinueAndSkipPost
resolveOwnerKeyId(key, callingUid)?.takeIf {
KeyMintSecurityLevelInterceptor.ownsKeyResponse(it)
} ?: return TransactionResult.ContinueAndSkipPost
// Same version gate as grant(): denied pre-36, revoke the virtualized grant on 36+.
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED)
@@ -423,7 +445,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
it.uid == callingUid
}
val totalCount = hardwareCount + softwareCount
val parcel = Parcel.obtain().apply {
val parcel =
Parcel.obtain().apply {
writeNoException()
writeInt(totalCount)
}
@@ -469,8 +492,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
if (userUpdatedKeys.remove(keyId)) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch" }
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
SystemLogger.trace {
"[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch"
}
SystemLogger.debug(
"[TX_ID: $txId] Skipping cert patch for user-updated key $keyId."
)
return TransactionResult.SkipTransaction
}
@@ -480,18 +507,29 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
)
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: isImport=${parsedParameters.isImportKey()} origin=${parsedParameters.origin} inImportedKeys=${KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)} hasPatchedChain=${KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) != null} isAttestKey=${parsedParameters.isAttestKey()}" }
SystemLogger.trace {
"[TRACE-$txId] getKeyEntry $keyId: isImport=${parsedParameters.isImportKey()} origin=${parsedParameters.origin} inImportedKeys=${KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)} hasPatchedChain=${KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) != null} isAttestKey=${parsedParameters.isAttestKey()}"
}
if (parsedParameters.isImportKey()) {
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
if (retainedChain == null) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip" }
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
SystemLogger.trace {
"[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip"
}
SystemLogger.info(
"[TX_ID: $txId] Skip patching for imported key (no prior attestation)."
)
return TransactionResult.SkipTransaction
}
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)" }
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
SystemLogger.trace {
"[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)"
}
SystemLogger.info(
"[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId"
)
CertificateHelper.updateCertificateChain(response.metadata, retainedChain)
.getOrThrow()
response.metadata.authorizations =
InterceptorUtils.patchAuthorizations(
response.metadata.authorizations,
@@ -501,8 +539,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
}
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" }
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
SystemLogger.trace {
"[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip"
}
SystemLogger.debug(
"[TX_ID: $txId] Skipping attest-key override for imported key $keyId"
)
return TransactionResult.SkipTransaction
}
@@ -545,7 +587,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
// Snapshot metadata bytes for the same reason as the
// primary doSoftwareKeyGen path — loss-less restore
// after reboot.
val metadataBytesForPersist = response.metadata?.let { md ->
val metadataBytesForPersist =
response.metadata?.let { md ->
runCatching {
val parcel = android.os.Parcel.obtain()
try {
@@ -554,7 +597,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
} finally {
parcel.recycle()
}
}.getOrNull()
}
.getOrNull()
}
GeneratedKeyPersistence.save(
keyId = keyId,
@@ -623,18 +667,23 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
}
/**
* Resolves the owner [KeyIdentifier] a grant/ungrant call targets. APP/alias keys map
* directly; KEY_ID keys are looked up by nspace (mirrors the deleteKey resolver). Returns
* null for anything not addressable, so callers fall through to the real keystore2.
* Resolves the owner [KeyIdentifier] a grant/ungrant call targets. APP/alias keys map directly;
* KEY_ID keys are looked up by nspace (mirrors the deleteKey resolver). Returns null for
* anything not addressable, so callers fall through to the real keystore2.
*/
private fun resolveOwnerKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? =
when {
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
descriptor.domain == Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor.nspace)
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
callingUid,
descriptor.nspace,
)
?.let { info ->
KeyMintSecurityLevelInterceptor.generatedKeys.entries
.firstOrNull { it.value.nspace == info.nspace && it.key.uid == callingUid }
.firstOrNull {
it.value.nspace == info.nspace && it.key.uid == callingUid
}
?.key
}
else -> null
@@ -642,14 +691,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
val descriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
val generatedKeyInfo =
when (descriptor.domain) {
Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
callingUid, descriptor.nspace
callingUid,
descriptor.nspace,
)
Domain.APP ->
descriptor.alias?.let {
@@ -659,22 +710,30 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
}
if (generatedKeyInfo == null) {
// Patch-mode key (cached in teeResponses, not generatedKeys): the real keystore2 applies
// Patch-mode key (cached in teeResponses, not generatedKeys): the real keystore2
// applies
// the update, so drop our stale cached chain. Otherwise getKeyEntry replays the
// pre-update generated attestation (duck STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE).
when (descriptor.domain) {
Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.evictTeeResponseByKeyId(callingUid, descriptor.nspace)
KeyMintSecurityLevelInterceptor.evictTeeResponseByKeyId(
callingUid,
descriptor.nspace,
)
Domain.APP ->
descriptor.alias?.let {
KeyMintSecurityLevelInterceptor.evictTeeResponse(KeyIdentifier(callingUid, it))
KeyMintSecurityLevelInterceptor.evictTeeResponse(
KeyIdentifier(callingUid, it)
)
}
else -> {}
}
descriptor.alias?.let {
val kid = KeyIdentifier(callingUid, it)
userUpdatedKeys.add(kid)
SystemLogger.trace { "[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys" }
SystemLogger.trace {
"[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys"
}
}
return TransactionResult.ContinueAndSkipPost
}
@@ -7,18 +7,18 @@ import android.system.keystore2.Domain
import android.system.keystore2.KeyDescriptor
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
/**
* Intercepts the keystore2 daemon's `android.security.maintenance` binder so our synthetic key
* state follows the same lifecycle events the platform applies to real keys.
*
* This is a pure side-effect hook: every handled transaction mutates only our own synthetic state
* and then returns [TransactionResult.ContinueAndSkipPost], so the real keystore2 still performs the
* real operation. We never fabricate a maintenance reply, so real key lifecycle is never disturbed.
* and then returns [TransactionResult.ContinueAndSkipPost], so the real keystore2 still performs
* the real operation. We never fabricate a maintenance reply, so real key lifecycle is never
* disturbed.
*
* Mounted via `register()` from [Keystore2Interceptor.onInterceptorReady]; the maintenance binder is
* hosted by the same keystore2 process, so the already-injected native hook reaches it too.
* Mounted via `register()` from [Keystore2Interceptor.onInterceptorReady]; the maintenance binder
* is hosted by the same keystore2 process, so the already-injected native hook reaches it too.
*/
object Keystore2MaintenanceInterceptor : BinderInterceptor() {
private val stubClass = IKeystoreMaintenance.Stub::class.java
@@ -93,13 +93,18 @@ object Keystore2MaintenanceInterceptor : BinderInterceptor() {
descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias)
descriptor.domain == Domain.KEY_ID ->
KeyMintSecurityLevelInterceptor.generatedKeys.entries
.firstOrNull { it.key.uid == callingUid && it.value.nspace == descriptor.nspace }
.firstOrNull {
it.key.uid == callingUid && it.value.nspace == descriptor.nspace
}
?.key
else -> null
}
/** Destination must be an addressable Domain.APP alias for us to keep tracking the key. */
private fun resolveDestinationKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? {
private fun resolveDestinationKeyId(
descriptor: KeyDescriptor,
callingUid: Int,
): KeyIdentifier? {
val alias = descriptor.alias ?: return null
if (descriptor.domain != Domain.APP) return null
val uid = if (descriptor.nspace > 0) descriptor.nspace.toInt() else callingUid
@@ -1,8 +1,8 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.Tag
import org.matrix.TEESimulator.attestation.KeyMintAttestation
@@ -24,7 +24,8 @@ object AuthorizeCreate {
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
val algo = keyParams.algorithm
if ((algo == Algorithm.EC || algo == Algorithm.RSA) &&
if (
(algo == Algorithm.EC || algo == Algorithm.RSA) &&
(purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT)
) {
return KeystoreErrorCodes.unsupportedPurpose
@@ -35,10 +36,8 @@ object AuthorizeCreate {
}
private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
if (purpose == KeyPurpose.WRAP_KEY)
return KeystoreErrorCodes.incompatiblePurpose
if (purpose !in keyParams.purpose)
return KeystoreErrorCodes.incompatiblePurpose
if (purpose == KeyPurpose.WRAP_KEY) return KeystoreErrorCodes.incompatiblePurpose
if (purpose !in keyParams.purpose) return KeystoreErrorCodes.incompatiblePurpose
return null
}
@@ -64,7 +63,11 @@ object AuthorizeCreate {
return null
}
private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array<KeyParameter>?): Int? {
private fun checkCallerNonce(
keyParams: KeyMintAttestation,
purpose: Int,
rawOpParams: Array<KeyParameter>?,
): Int? {
if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null
if (keyParams.callerNonce == true) return null
if (rawOpParams?.any { it.tag == Tag.NONCE } == true)
@@ -33,19 +33,17 @@ data class PersistedKeyData(
val privateKeyBytes: ByteArray,
val certChainBytes: List<ByteArray>,
/**
* Byte-identical KeyMetadata parcel snapshot. Restoring authorizations
* directly from these bytes preserves tag count, order, and exact
* security-level annotations across reboots — the kind of structural
* details apps fingerprint to decide whether the alias is still
* "the same key".
* Byte-identical KeyMetadata parcel snapshot. Restoring authorizations directly from these
* bytes preserves tag count, order, and exact security-level annotations across reboots — the
* kind of structural details apps fingerprint to decide whether the alias is still "the same
* key".
*/
val metadataBytes: ByteArray,
/**
* Raw secret material for symmetric records (AES, HMAC, 3DES). Empty
* for asymmetric. Critical for AndroidX security crypto MasterKey
* (AES-GCM-256) — without this every reboot regenerates a fresh AES
* key and EncryptedSharedPreferences becomes undecryptable, which is
* what banking apps interpret as session expiry and force a relogin.
* Raw secret material for symmetric records (AES, HMAC, 3DES). Empty for asymmetric. Critical
* for AndroidX security crypto MasterKey (AES-GCM-256) — without this every reboot regenerates
* a fresh AES key and EncryptedSharedPreferences becomes undecryptable, which is what banking
* apps interpret as session expiry and force a relogin.
*/
val symmetricKeyBytes: ByteArray,
val symmetricAlgorithm: String,
@@ -54,20 +52,15 @@ data class PersistedKeyData(
object GeneratedKeyPersistence {
/**
* Single source of truth for the on-disk format. Bump this every time
* the layout changes; older numbers are silently skipped on read so
* stale dev artifacts and pre-fix upstream files can't be partially
* rehydrated into broken in-memory state.
* Single source of truth for the on-disk format. Bump this every time the layout changes; older
* numbers are silently skipped on read so stale dev artifacts and pre-fix upstream files can't
* be partially rehydrated into broken in-memory state.
*
* History:
* 1 — original upstream layout (no metadata snapshot, no symmetric
* block; restored keys lose authorization tags and AES master
* keys altogether — apps relying on persisted keystore state
* across reboots get logged out)
* 2 — transitional dev-only format that added metadata but still
* missed the symmetric block; never shipped
* 3 — current: byte-identical KeyMetadata snapshot + raw symmetric
* key material so AES/HMAC keys survive reboots
* History: 1 — original upstream layout (no metadata snapshot, no symmetric block; restored
* keys lose authorization tags and AES master keys altogether — apps relying on persisted
* keystore state across reboots get logged out) 2 — transitional dev-only format that added
* metadata but still missed the symmetric block; never shipped 3 — current: byte-identical
* KeyMetadata snapshot + raw symmetric key material so AES/HMAC keys survive reboots
*/
private const val FORMAT_VERSION = 3
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
@@ -109,7 +102,8 @@ object GeneratedKeyPersistence {
val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp")
try {
DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out ->
DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out
->
out.writeInt(FORMAT_VERSION)
out.writeInt(securityLevel)
out.writeInt(keyId.uid)
@@ -160,10 +154,13 @@ object GeneratedKeyPersistence {
throw e
}
// Atomic rename — if this fails the tmp is left behind and cleaned on next deleteAll
// Atomic rename — if this fails the tmp is left behind and cleaned on next
// deleteAll
if (!tmpFile.renameTo(finalFile)) {
tmpFile.delete()
throw IllegalStateException("Failed to atomically rename $tmpFile -> $finalFile")
throw IllegalStateException(
"Failed to atomically rename $tmpFile -> $finalFile"
)
}
// Verify write succeeded - catches disk-full or filesystem errors
@@ -172,9 +169,8 @@ object GeneratedKeyPersistence {
}
SystemLogger.debug("Persisted key: $keyId")
}.onFailure { e ->
SystemLogger.error("Failed to persist key $keyId", e)
}
.onFailure { e -> SystemLogger.error("Failed to persist key $keyId", e) }
} finally {
lock.unlock()
SystemLogger.debug("[Persistence] Lock released for $filename")
@@ -194,9 +190,8 @@ object GeneratedKeyPersistence {
} else {
SystemLogger.debug("No persisted file to delete for: $keyId")
}
}.onFailure { e ->
SystemLogger.error("Failed to delete persisted key $keyId", e)
}
.onFailure { e -> SystemLogger.error("Failed to delete persisted key $keyId", e) }
}
fun deleteAll() {
@@ -218,9 +213,8 @@ object GeneratedKeyPersistence {
}
fileLocks.clear()
SystemLogger.info("Deleted $count persisted key files")
}.onFailure { e ->
SystemLogger.error("Failed to delete all persisted keys", e)
}
.onFailure { e -> SystemLogger.error("Failed to delete all persisted keys", e) }
}
fun loadAll(securityLevel: Int): List<PersistedKeyData> {
@@ -281,7 +275,8 @@ object GeneratedKeyPersistence {
if (pkLen > 0) input.readFully(pkBytes)
val certCount = requireBounds(input.readInt(), 10, "certCount")
val certChainBytes = (0 until certCount).map {
val certChainBytes =
(0 until certCount).map {
val certLen = requireBounds(input.readInt(), 65536, "certLen")
val certBytes = ByteArray(certLen)
input.readFully(certBytes)
@@ -289,15 +284,12 @@ object GeneratedKeyPersistence {
}
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
val metadataBytes = ByteArray(metaLen).also {
if (metaLen > 0) input.readFully(it)
}
val metadataBytes =
ByteArray(metaLen).also { if (metaLen > 0) input.readFully(it) }
val skAlgo = input.readUTF()
val skLen = requireBounds(input.readInt(), 8192, "skLen")
val skBytes = ByteArray(skLen).also {
if (skLen > 0) input.readFully(it)
}
val skBytes = ByteArray(skLen).also { if (skLen > 0) input.readFully(it) }
if (storedSecLevel == securityLevel) {
result.add(
@@ -321,7 +313,8 @@ object GeneratedKeyPersistence {
)
}
}
}.onFailure { e ->
}
.onFailure { e ->
SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e)
}
}
@@ -345,11 +338,14 @@ object GeneratedKeyPersistence {
}
val secLevel = metadata.keySecurityLevel
val entry = KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) ->
val entry =
KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) ->
id.uid == callingUid && info.nspace == generatedKeyInfo.nspace
}
if (entry == null) {
SystemLogger.debug("rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}")
SystemLogger.debug(
"rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}"
)
return
}
@@ -368,16 +364,20 @@ object GeneratedKeyPersistence {
return
}
val persisted = runCatching {
val persisted =
runCatching {
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
val version = input.readInt()
if (version != FORMAT_VERSION) {
SystemLogger.warning("rePersist: legacy format version $version for $keyId, will not re-persist (next generateKey replaces it)")
SystemLogger.warning(
"rePersist: legacy format version $version for $keyId, will not re-persist (next generateKey replaces it)"
)
return
}
readPersistedKeyData(input)
}
}.getOrNull()
}
.getOrNull()
if (persisted == null) {
SystemLogger.warning("rePersist: failed to read existing data for $keyId")
return
@@ -392,7 +392,8 @@ object GeneratedKeyPersistence {
// Serialize the live KeyMetadata (now contains the user-installed cert
// chain via updateSubcomponent) so the next boot restores byte-identical
// metadata. KeyMetadata is binder-free, so marshall() is safe here.
val metadataBytes = runCatching {
val metadataBytes =
runCatching {
android.os.Parcel.obtain().let { parcel ->
try {
metadata.writeToParcel(parcel, 0)
@@ -401,7 +402,8 @@ object GeneratedKeyPersistence {
parcel.recycle()
}
}
}.getOrNull()
}
.getOrNull()
save(
keyId = keyId,
keyPair = keyPair,
@@ -427,8 +429,8 @@ object GeneratedKeyPersistence {
}
private fun keyFileName(uid: Int, alias: String): String {
val digest = MessageDigest.getInstance("SHA-256")
.digest("$uid:$alias".toByteArray(Charsets.UTF_8))
val digest =
MessageDigest.getInstance("SHA-256").digest("$uid:$alias".toByteArray(Charsets.UTF_8))
return digest.joinToString("") { "%02x".format(it) } + ".bin"
}
@@ -455,7 +457,8 @@ object GeneratedKeyPersistence {
if (pkLen > 0) input.readFully(pkBytes)
val certCount = requireBounds(input.readInt(), 10, "certCount")
val certChainBytes = (0 until certCount).map {
val certChainBytes =
(0 until certCount).map {
val certLen = requireBounds(input.readInt(), 65536, "certLen")
val certBytes = ByteArray(certLen)
input.readFully(certBytes)
@@ -463,15 +466,11 @@ object GeneratedKeyPersistence {
}
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
val metadataBytes = ByteArray(metaLen).also {
if (metaLen > 0) input.readFully(it)
}
val metadataBytes = ByteArray(metaLen).also { if (metaLen > 0) input.readFully(it) }
val skAlgo = input.readUTF()
val skLen = requireBounds(input.readInt(), 8192, "skLen")
val skBytes = ByteArray(skLen).also {
if (skLen > 0) input.readFully(it)
}
val skBytes = ByteArray(skLen).also { if (skLen > 0) input.readFully(it) }
return PersistedKeyData(
uid = uid,
@@ -9,12 +9,12 @@ import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.PaddingMode
import android.hardware.security.keymint.Tag
import android.os.ServiceSpecificException
import java.util.concurrent.locks.LockSupport
import android.system.keystore2.IKeystoreOperation
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.Cipher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
@@ -24,9 +24,13 @@ private sealed interface CryptoPrimitive {
fun updateAad(aadInput: ByteArray?) {
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
}
fun update(data: ByteArray?): ByteArray?
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
fun abort()
fun getBeginParameters(): Array<KeyParameter>? = null
}
@@ -118,10 +122,16 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
if (data != null) update(data)
if (signature == null) {
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature to verify is null")
throw ServiceSpecificException(
KeystoreErrorCodes.verificationFailed,
"Signature to verify is null",
)
}
if (!this.signature.verify(signature)) {
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature verification failed")
throw ServiceSpecificException(
KeystoreErrorCodes.verificationFailed,
"Signature verification failed",
)
}
return null
}
@@ -201,7 +211,8 @@ class SoftwareOperation(
private val latencyFloorMs: Long = 0L,
) {
private val primitive: CryptoPrimitive
@Volatile var finalized = false
@Volatile
var finalized = false
private set
var onFinishCallback: (() -> Unit)? = null
@@ -242,21 +253,27 @@ class SoftwareOperation(
primitive =
when (purpose) {
KeyPurpose.SIGN -> {
val kp = keyPair ?: throw ServiceSpecificException(
val kp =
keyPair
?: throw ServiceSpecificException(
KeystoreErrorCodes.invalidArgument,
"[SoftwareOp TX_ID: $txId] SIGN requested but keyPair is null",
)
Signer(kp, params)
}
KeyPurpose.VERIFY -> {
val kp = keyPair ?: throw ServiceSpecificException(
val kp =
keyPair
?: throw ServiceSpecificException(
KeystoreErrorCodes.invalidArgument,
"[SoftwareOp TX_ID: $txId] VERIFY requested but keyPair is null",
)
Verifier(kp, params)
}
KeyPurpose.ENCRYPT -> {
val key: java.security.Key = secretKey ?: keyPair?.public
val key: java.security.Key =
secretKey
?: keyPair?.public
?: throw ServiceSpecificException(
KeystoreErrorCodes.unsupportedPurpose,
"[SoftwareOp TX_ID: $txId] ENCRYPT requires either secretKey or keyPair.public",
@@ -264,7 +281,9 @@ class SoftwareOperation(
CipherPrimitive(key, params, Cipher.ENCRYPT_MODE)
}
KeyPurpose.DECRYPT -> {
val key: java.security.Key = secretKey ?: keyPair?.private
val key: java.security.Key =
secretKey
?: keyPair?.private
?: throw ServiceSpecificException(
KeystoreErrorCodes.unsupportedPurpose,
"[SoftwareOp TX_ID: $txId] DECRYPT requires either secretKey or keyPair.private",
@@ -272,7 +291,9 @@ class SoftwareOperation(
CipherPrimitive(key, params, Cipher.DECRYPT_MODE)
}
KeyPurpose.AGREE_KEY -> {
val kp = keyPair ?: throw ServiceSpecificException(
val kp =
keyPair
?: throw ServiceSpecificException(
KeystoreErrorCodes.invalidArgument,
"[SoftwareOp TX_ID: $txId] AGREE_KEY requested but keyPair is null",
)
@@ -288,29 +309,39 @@ class SoftwareOperation(
private fun checkActive() {
if (finalized) {
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
SystemLogger.debug(
"[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)"
)
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
}
}
private fun checkInputLength(data: ByteArray?) {
if (data != null && data.size > MAX_RECEIVE_DATA) {
SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})")
SystemLogger.info(
"[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})"
)
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
}
}
fun updateAad(aadInput: ByteArray?) {
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() ENTRY inputSize=${aadInput?.size ?: 0} primitive=${primitive::class.simpleName}")
SystemLogger.info(
"[SoftwareOp TX_ID: $txId] updateAad() ENTRY inputSize=${aadInput?.size ?: 0} primitive=${primitive::class.simpleName}"
)
checkActive()
checkInputLength(aadInput)
try {
primitive.updateAad(aadInput)
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() RETURNED_NORMALLY (unexpected for non-AEAD)")
SystemLogger.info(
"[SoftwareOp TX_ID: $txId] updateAad() RETURNED_NORMALLY (unexpected for non-AEAD)"
)
} catch (throwable: Throwable) {
val top = throwable.stackTrace.firstOrNull()?.toString() ?: "<no-frame>"
val code = (throwable as? ServiceSpecificException)?.errorCode
SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() THREW class=${throwable::class.java.name} code=$code msg=${throwable.message} top=$top")
SystemLogger.info(
"[SoftwareOp TX_ID: $txId] updateAad() THREW class=${throwable::class.java.name} code=$code msg=${throwable.message} top=$top"
)
throw throwable
}
}
@@ -358,11 +389,16 @@ class SoftwareOperation(
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
}
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) {
is SignatureException -> ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message)
is javax.crypto.BadPaddingException -> ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message)
is javax.crypto.IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message)
is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message)
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException =
when (e) {
is SignatureException ->
ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message)
is javax.crypto.BadPaddingException ->
ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message)
is javax.crypto.IllegalBlockSizeException ->
ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message)
is java.security.InvalidKeyException ->
ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message)
else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message)
}
@@ -429,9 +465,8 @@ internal object KeystoreErrorCodes {
}
fun resolveField(className: String, fieldName: String, fallback: Int): Int =
runCatching {
Class.forName(className).getField(fieldName).getInt(null)
}.getOrElse {
runCatching { Class.forName(className).getField(fieldName).getInt(null) }
.getOrElse {
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
fallback
}
@@ -442,13 +477,17 @@ class SoftwareOperationBinder(private val operation: SoftwareOperation) :
@Synchronized
override fun updateAad(aadInput: ByteArray?) {
SystemLogger.info("[SoftwareOpBinder] updateAad() ENTRY callingUid=${android.os.Binder.getCallingUid()} size=${aadInput?.size ?: 0}")
SystemLogger.info(
"[SoftwareOpBinder] updateAad() ENTRY callingUid=${android.os.Binder.getCallingUid()} size=${aadInput?.size ?: 0}"
)
try {
operation.updateAad(aadInput)
SystemLogger.info("[SoftwareOpBinder] updateAad() RETURNED_NORMALLY")
} catch (throwable: Throwable) {
val code = (throwable as? ServiceSpecificException)?.errorCode
SystemLogger.info("[SoftwareOpBinder] updateAad() PROPAGATING class=${throwable::class.java.name} code=$code msg=${throwable.message}")
SystemLogger.info(
"[SoftwareOpBinder] updateAad() PROPAGATING class=${throwable::class.java.name} code=$code msg=${throwable.message}"
)
throw throwable
}
}
@@ -26,10 +26,11 @@ object SystemLogger {
private val suppressedCount = AtomicInteger(0)
/**
* Returns true if this message should be emitted. Resets the window if expired
* and emits a suppression summary for the previous window.
* Returns true if this message should be emitted. Resets the window if expired and emits a
* suppression summary for the previous window.
*/
@PublishedApi internal fun acquireLogPermit(): Boolean {
@PublishedApi
internal fun acquireLogPermit(): Boolean {
val now = System.currentTimeMillis()
val start = windowStart.get()
if (now - start > RATE_LIMIT_WINDOW_MS) {
@@ -38,7 +39,10 @@ object SystemLogger {
val suppressed = suppressedCount.getAndSet(0)
windowCount.set(1) // this call counts as #1 in the new window
if (suppressed > 0) {
Log.i(TAG, "[rate-limit] suppressed $suppressed log messages in previous window")
Log.i(
TAG,
"[rate-limit] suppressed $suppressed log messages in previous window",
)
}
return true
}
@@ -49,9 +53,7 @@ object SystemLogger {
return false
}
/**
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
*/
/** Logs a debug message. Use this for fine-grained information that is useful for debugging. */
fun debug(message: String) {
if (!isDebugBuild) return
if (!acquireLogPermit()) return
@@ -65,9 +67,7 @@ object SystemLogger {
Log.d(TAG, message())
}
/**
* Logs an informational message. Use this to report major application lifecycle events.
*/
/** Logs an informational message. Use this to report major application lifecycle events. */
fun info(message: String) {
if (!acquireLogPermit()) return
Log.i(TAG, message)
@@ -79,9 +79,7 @@ object SystemLogger {
Log.i(TAG, message())
}
/**
* Logs a warning message. Warnings are never rate-limited.
*/
/** Logs a warning message. Warnings are never rate-limited. */
fun warning(message: String, throwable: Throwable? = null) {
if (throwable != null) {
Log.w(TAG, message, throwable)
@@ -90,9 +88,7 @@ object SystemLogger {
}
}
/**
* Logs an error message. Errors are never rate-limited.
*/
/** Logs an error message. Errors are never rate-limited. */
fun error(message: String, throwable: Throwable? = null) {
if (throwable != null) {
Log.e(TAG, message, throwable)
@@ -95,7 +95,9 @@ object CertificateGenerator {
return try {
// AOSP ta/src/keys.rs:451-478: no challenge + no attestKey = self-signed, depth 1
if (challenge == null && attestKeyAlias == null) {
SystemLogger.trace { "[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}" }
SystemLogger.trace {
"[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}"
}
return listOf(buildSelfSignedCertificate(subjectKeyPair, params))
}
@@ -106,8 +108,8 @@ object CertificateGenerator {
getAttestationKeyInfo(uid, attestKeyAlias)
} else null
val (signingKey, issuer) = attestKeyInfo
?.let { it.first to it.second }
val (signingKey, issuer) =
attestKeyInfo?.let { it.first to it.second }
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
val leafCert =
@@ -138,9 +140,7 @@ object CertificateGenerator {
securityLevel: Int,
): Pair<KeyPair, List<Certificate>>? {
return try {
SystemLogger.info(
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
)
SystemLogger.info("Generating new attested key pair for alias: '$alias' (UID: $uid)")
val newKeyPair =
generateSoftwareKeyPair(params)
?: throw Exception("Failed to generate underlying software key pair.")
@@ -149,9 +149,7 @@ object CertificateGenerator {
generateCertificateChain(uid, newKeyPair, attestKeyAlias, params, securityLevel)
?: throw Exception("Failed to generate certificate chain for new key pair.")
SystemLogger.info(
"Successfully generated new certificate chain for alias: '$alias'."
)
SystemLogger.info("Successfully generated new certificate chain for alias: '$alias'.")
Pair(newKeyPair, chain)
} catch (e: android.os.ServiceSpecificException) {
throw e
@@ -205,7 +203,9 @@ object CertificateGenerator {
private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int {
var bits = 0
for (purpose in purposes) {
bits = bits or when (purpose) {
bits =
bits or
when (purpose) {
KeyPurpose.SIGN -> KeyUsage.digitalSignature
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
@@ -253,9 +253,13 @@ object CertificateGenerator {
val signerAlgorithm =
when (signingKeyPair.private.algorithm) {
"EC", "ECDSA" -> "SHA256withECDSA"
"EC",
"ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
else ->
throw IllegalArgumentException(
"Unsupported signing key: ${signingKeyPair.private.algorithm}"
)
}
val contentSigner =
JcaContentSignerBuilder(signerAlgorithm)
@@ -274,7 +278,8 @@ object CertificateGenerator {
val notBefore = params.certificateNotBefore ?: Date(0)
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
val builder = JcaX509v3CertificateBuilder(
val builder =
JcaX509v3CertificateBuilder(
subject,
params.certificateSerial ?: BigInteger.ONE,
notBefore,
@@ -288,12 +293,16 @@ object CertificateGenerator {
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
}
val signerAlgorithm = when (keyPair.private.algorithm) {
"EC", "ECDSA" -> "SHA256withECDSA"
val signerAlgorithm =
when (keyPair.private.algorithm) {
"EC",
"ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}")
else ->
throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}")
}
val contentSigner = JcaContentSignerBuilder(signerAlgorithm)
val contentSigner =
JcaContentSignerBuilder(signerAlgorithm)
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keyPair.private)
@@ -69,7 +69,10 @@ object NativeCertGen {
isAvailable = true
SystemLogger.info("NativeCertGen: loaded libcertgen.so successfully")
} catch (e: UnsatisfiedLinkError) {
SystemLogger.error("NativeCertGen: failed to load libcertgen.so, falling back to BouncyCastle", e)
SystemLogger.error(
"NativeCertGen: failed to load libcertgen.so, falling back to BouncyCastle",
e,
)
}
}
@@ -111,8 +114,10 @@ object NativeCertGen {
throw IllegalStateException("No certificates in native result")
}
val algorithmName = when (certs[0].publicKey.algorithm) {
"EC", "ECDSA" -> "EC"
val algorithmName =
when (certs[0].publicKey.algorithm) {
"EC",
"ECDSA" -> "EC"
"RSA" -> "RSA"
else -> certs[0].publicKey.algorithm
}
@@ -186,7 +186,8 @@ object AndroidDeviceUtils {
private val PERSIST_DIR = File("/data/adb/tricky_store")
private fun fileForProperty(propertyName: String): File = when (propertyName) {
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")
@@ -294,7 +295,10 @@ object AndroidDeviceUtils {
// Resolve from live system prop — matches what detectors see via getprop,
// even when PIF has spoofed ro.build.version.security_patch via resetprop
resolvedValue.equals("prop", ignoreCase = true) ->
parsePatchLevelValue(SystemProperties.get("ro.build.version.security_patch", ""), isLong)
parsePatchLevelValue(
SystemProperties.get("ro.build.version.security_patch", ""),
isLong,
)
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
else -> parsePatchLevelValue(resolvedValue, isLong)
}
@@ -396,19 +400,19 @@ object AndroidDeviceUtils {
/**
* Retrieves the attestation version for the given security level. The value follows the device
* OS: cached attestation data wins, then attestVersionMap[SDK_INT], then 400 as last resort.
* A static StrongBox=300 floor would force a major-version mismatch with the TEE chain on
* Android 16 devices that report keymaster 400 across both security levels.
* OS: cached attestation data wins, then attestVersionMap[SDK_INT], then 400 as last resort. A
* static StrongBox=300 floor would force a major-version mismatch with the TEE chain on Android
* 16 devices that report keymaster 400 across both security levels.
*
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
* @return The appropriate attestation version number.
*/
fun getAttestVersion(securityLevel: Int): Int {
val cached = DeviceAttestationService.CachedAttestationData?.attestVersion
val version = cached
?: attestVersionMap[Build.VERSION.SDK_INT]
?: 400 // Default to a recent version
val source = when {
val version =
cached ?: attestVersionMap[Build.VERSION.SDK_INT] ?: 400 // Default to a recent version
val source =
when {
cached != null -> "cache"
attestVersionMap.containsKey(Build.VERSION.SDK_INT) -> "map"
else -> "default"
@@ -519,10 +523,7 @@ object AndroidDeviceUtils {
val moduleHash: ByteArray by lazy {
DeviceAttestationService.CachedAttestationData?.moduleHash
?: runCatching {
data class ModuleEntry(
val nameEncoded: ByteArray,
val fullEncoded: ByteArray,
)
data class ModuleEntry(val nameEncoded: ByteArray, val fullEncoded: ByteArray)
val modules =
apexInfos.map { (packageName, versionCode) ->
@@ -14,12 +14,15 @@ object AndroidPermissionUtils {
val activityThreadClass = Class.forName("android.app.ActivityThread")
// 2. Invoke the static currentActivityThread() method
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
val currentActivityThreadMethod =
activityThreadClass.getDeclaredMethod("currentActivityThread")
currentActivityThreadMethod.isAccessible = true
val activityThread = currentActivityThreadMethod.invoke(null)
if (activityThread == null) {
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
SystemLogger.warning(
"Reflection: ActivityThread.currentActivityThread() returned null"
)
return null
}
@@ -30,23 +33,25 @@ object AndroidPermissionUtils {
if (application != null) return application
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
// 4. Fallback to getSystemContext() if application is null (often happens in
// system_server)
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
getSystemContextMethod.isAccessible = true
getSystemContextMethod.invoke(activityThread) as? Context
} catch (e: Exception) {
SystemLogger.error("Reflection failed to get global context for permission check", e)
null
}
}
/**
* Core permission check.
*/
/** Core permission check. */
fun hasPermission(uid: Int, permission: String): Boolean {
val context = getGlobalContext() ?: run {
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
val context =
getGlobalContext()
?: run {
SystemLogger.warning(
"AndroidPermissionUtils: Context is null, failing permission check safely."
)
return false
}
@@ -7,10 +7,7 @@ package org.matrix.TEESimulator.util
* @return A new string with each line individually trimmed.
*/
fun String.trimLines(): String =
this.trim()
.lines()
.filter { !it.trim().startsWith("<!--") }
.joinToString("\n") { it.trim() }
this.trim().lines().filter { !it.trim().startsWith("<!--") }.joinToString("\n") { it.trim() }
/**
* Converts a ByteArray to its hexadecimal string representation.