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