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