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 {
|
kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }
|
||||||
compilerOptions {
|
|
||||||
jvmTarget.set(JvmTarget.JVM_21)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":stub"))
|
compileOnly(project(":stub"))
|
||||||
@@ -79,17 +75,22 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Rust native cert gen build task ---
|
// --- 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"
|
group = "TEESimulator-RS Native Build"
|
||||||
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
|
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"))
|
||||||
@@ -98,8 +99,11 @@ val buildRustCertgen by tasks.registering(Exec::class) {
|
|||||||
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,7 +114,8 @@ 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
|
||||||
|
tasks.registering {
|
||||||
group = "TEESimulator-RS Module Packaging"
|
group = "TEESimulator-RS Module Packaging"
|
||||||
description = "Rewrite module/update.json to match current verName and gitCommitCount."
|
description = "Rewrite module/update.json to match current verName and gitCommitCount."
|
||||||
|
|
||||||
@@ -134,7 +139,7 @@ val refreshUpdateJson by tasks.registering {
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
onVariants(selector().all()) { variant ->
|
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"
|
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
|
||||||
} else {
|
} else {
|
||||||
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
"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,7 +44,8 @@ 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 =
|
||||||
|
keyDescription.joinToString(separator = ", ") {
|
||||||
AttestationPatcher.formatAsn1Primitive(it)
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
}
|
}
|
||||||
"Forged attestation data: $formattedString"
|
"Forged attestation data: $formattedString"
|
||||||
@@ -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,9 +298,11 @@ 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 ->
|
||||||
|
file?.let { loadTargetPackages(it) }
|
||||||
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
|
?: 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.")
|
?: 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.
|
||||||
|
|||||||
@@ -110,14 +110,18 @@ 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 =
|
||||||
|
try {
|
||||||
when (code) {
|
when (code) {
|
||||||
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
|
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
|
||||||
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
|
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
|
||||||
else -> return super.onTransact(code, data, reply, flags)
|
else -> return super.onTransact(code, data, reply, flags)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} 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
|
TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
writeResultToReply(result, reply!!)
|
writeResultToReply(result, reply!!)
|
||||||
@@ -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,7 +37,8 @@ object InterceptorUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
val parcel = Parcel.obtain().apply {
|
val parcel =
|
||||||
|
Parcel.obtain().apply {
|
||||||
writeInt(EX_SERVICE_SPECIFIC)
|
writeInt(EX_SERVICE_SPECIFIC)
|
||||||
writeString(synthesizeSseMessage(errorCode))
|
writeString(synthesizeSseMessage(errorCode))
|
||||||
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
||||||
|
|||||||
+106
-47
@@ -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,10 +262,14 @@ 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,
|
||||||
|
)
|
||||||
|
?.let { info ->
|
||||||
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||||
.find { it.value.nspace == info.nspace && it.key.uid == callingUid }
|
.find {
|
||||||
|
it.value.nspace == info.nspace && it.key.uid == callingUid
|
||||||
|
}
|
||||||
?.key
|
?.key
|
||||||
}
|
}
|
||||||
} else null
|
} else null
|
||||||
@@ -289,8 +302,10 @@ 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(
|
||||||
@@ -298,8 +313,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
)
|
)
|
||||||
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(
|
||||||
@@ -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,7 +445,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
it.uid == callingUid
|
it.uid == callingUid
|
||||||
}
|
}
|
||||||
val totalCount = hardwareCount + softwareCount
|
val totalCount = hardwareCount + softwareCount
|
||||||
val parcel = Parcel.obtain().apply {
|
val parcel =
|
||||||
|
Parcel.obtain().apply {
|
||||||
writeNoException()
|
writeNoException()
|
||||||
writeInt(totalCount)
|
writeInt(totalCount)
|
||||||
}
|
}
|
||||||
@@ -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,7 +587,8 @@ 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 =
|
||||||
|
response.metadata?.let { md ->
|
||||||
runCatching {
|
runCatching {
|
||||||
val parcel = android.os.Parcel.obtain()
|
val parcel = android.os.Parcel.obtain()
|
||||||
try {
|
try {
|
||||||
@@ -554,7 +597,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
} finally {
|
} finally {
|
||||||
parcel.recycle()
|
parcel.recycle()
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}
|
||||||
|
.getOrNull()
|
||||||
}
|
}
|
||||||
GeneratedKeyPersistence.save(
|
GeneratedKeyPersistence.save(
|
||||||
keyId = keyId,
|
keyId = keyId,
|
||||||
@@ -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 =
|
||||||
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
?: return TransactionResult.ContinueAndSkipPost
|
?: 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
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-7
@@ -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
|
||||||
|
|||||||
+10
-7
@@ -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,7 +24,8 @@ 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 (
|
||||||
|
(algo == Algorithm.EC || algo == Algorithm.RSA) &&
|
||||||
(purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT)
|
(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)
|
||||||
|
|||||||
+55
-56
@@ -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")
|
||||||
@@ -109,7 +102,8 @@ object GeneratedKeyPersistence {
|
|||||||
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(FORMAT_VERSION)
|
||||||
out.writeInt(securityLevel)
|
out.writeInt(securityLevel)
|
||||||
out.writeInt(keyId.uid)
|
out.writeInt(keyId.uid)
|
||||||
@@ -160,10 +154,13 @@ object GeneratedKeyPersistence {
|
|||||||
throw e
|
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)) {
|
if (!tmpFile.renameTo(finalFile)) {
|
||||||
tmpFile.delete()
|
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
|
// Verify write succeeded - catches disk-full or filesystem errors
|
||||||
@@ -172,9 +169,8 @@ object GeneratedKeyPersistence {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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")
|
||||||
@@ -194,9 +190,8 @@ object GeneratedKeyPersistence {
|
|||||||
} else {
|
} else {
|
||||||
SystemLogger.debug("No persisted file to delete for: $keyId")
|
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() {
|
fun deleteAll() {
|
||||||
@@ -218,9 +213,8 @@ object GeneratedKeyPersistence {
|
|||||||
}
|
}
|
||||||
fileLocks.clear()
|
fileLocks.clear()
|
||||||
SystemLogger.info("Deleted $count persisted key files")
|
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> {
|
fun loadAll(securityLevel: Int): List<PersistedKeyData> {
|
||||||
@@ -281,7 +275,8 @@ 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 =
|
||||||
|
(0 until certCount).map {
|
||||||
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
||||||
val certBytes = ByteArray(certLen)
|
val certBytes = ByteArray(certLen)
|
||||||
input.readFully(certBytes)
|
input.readFully(certBytes)
|
||||||
@@ -289,15 +284,12 @@ object GeneratedKeyPersistence {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
|
val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen")
|
||||||
val metadataBytes = ByteArray(metaLen).also {
|
val metadataBytes =
|
||||||
if (metaLen > 0) input.readFully(it)
|
ByteArray(metaLen).also { 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (storedSecLevel == securityLevel) {
|
if (storedSecLevel == securityLevel) {
|
||||||
result.add(
|
result.add(
|
||||||
@@ -321,7 +313,8 @@ object GeneratedKeyPersistence {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
}
|
||||||
|
.onFailure { e ->
|
||||||
SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e)
|
SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,11 +338,14 @@ object GeneratedKeyPersistence {
|
|||||||
}
|
}
|
||||||
val secLevel = metadata.keySecurityLevel
|
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
|
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 =
|
||||||
|
runCatching {
|
||||||
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
|
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
|
||||||
val version = input.readInt()
|
val version = input.readInt()
|
||||||
if (version != FORMAT_VERSION) {
|
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
|
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,7 +392,8 @@ 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 =
|
||||||
|
runCatching {
|
||||||
android.os.Parcel.obtain().let { parcel ->
|
android.os.Parcel.obtain().let { parcel ->
|
||||||
try {
|
try {
|
||||||
metadata.writeToParcel(parcel, 0)
|
metadata.writeToParcel(parcel, 0)
|
||||||
@@ -401,7 +402,8 @@ object GeneratedKeyPersistence {
|
|||||||
parcel.recycle()
|
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,7 +457,8 @@ 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 =
|
||||||
|
(0 until certCount).map {
|
||||||
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
||||||
val certBytes = ByteArray(certLen)
|
val certBytes = ByteArray(certLen)
|
||||||
input.readFully(certBytes)
|
input.readFully(certBytes)
|
||||||
@@ -463,15 +466,11 @@ object GeneratedKeyPersistence {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
+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.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
|
||||||
@@ -242,21 +253,27 @@ class SoftwareOperation(
|
|||||||
primitive =
|
primitive =
|
||||||
when (purpose) {
|
when (purpose) {
|
||||||
KeyPurpose.SIGN -> {
|
KeyPurpose.SIGN -> {
|
||||||
val kp = keyPair ?: throw ServiceSpecificException(
|
val kp =
|
||||||
|
keyPair
|
||||||
|
?: throw ServiceSpecificException(
|
||||||
KeystoreErrorCodes.invalidArgument,
|
KeystoreErrorCodes.invalidArgument,
|
||||||
"[SoftwareOp TX_ID: $txId] SIGN requested but keyPair is null",
|
"[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 =
|
||||||
|
keyPair
|
||||||
|
?: throw ServiceSpecificException(
|
||||||
KeystoreErrorCodes.invalidArgument,
|
KeystoreErrorCodes.invalidArgument,
|
||||||
"[SoftwareOp TX_ID: $txId] VERIFY requested but keyPair is null",
|
"[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 =
|
||||||
|
secretKey
|
||||||
|
?: keyPair?.public
|
||||||
?: throw ServiceSpecificException(
|
?: throw ServiceSpecificException(
|
||||||
KeystoreErrorCodes.unsupportedPurpose,
|
KeystoreErrorCodes.unsupportedPurpose,
|
||||||
"[SoftwareOp TX_ID: $txId] ENCRYPT requires either secretKey or keyPair.public",
|
"[SoftwareOp TX_ID: $txId] ENCRYPT requires either secretKey or keyPair.public",
|
||||||
@@ -264,7 +281,9 @@ class SoftwareOperation(
|
|||||||
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 =
|
||||||
|
secretKey
|
||||||
|
?: keyPair?.private
|
||||||
?: throw ServiceSpecificException(
|
?: throw ServiceSpecificException(
|
||||||
KeystoreErrorCodes.unsupportedPurpose,
|
KeystoreErrorCodes.unsupportedPurpose,
|
||||||
"[SoftwareOp TX_ID: $txId] DECRYPT requires either secretKey or keyPair.private",
|
"[SoftwareOp TX_ID: $txId] DECRYPT requires either secretKey or keyPair.private",
|
||||||
@@ -272,7 +291,9 @@ class SoftwareOperation(
|
|||||||
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 =
|
||||||
|
keyPair
|
||||||
|
?: throw ServiceSpecificException(
|
||||||
KeystoreErrorCodes.invalidArgument,
|
KeystoreErrorCodes.invalidArgument,
|
||||||
"[SoftwareOp TX_ID: $txId] AGREE_KEY requested but keyPair is null",
|
"[SoftwareOp TX_ID: $txId] AGREE_KEY requested but keyPair is null",
|
||||||
)
|
)
|
||||||
@@ -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,11 +389,16 @@ 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 ->
|
||||||
|
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)
|
else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,9 +465,8 @@ 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
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ 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 {
|
||||||
|
"[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}"
|
||||||
|
}
|
||||||
return listOf(buildSelfSignedCertificate(subjectKeyPair, params))
|
return listOf(buildSelfSignedCertificate(subjectKeyPair, params))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +108,8 @@ object CertificateGenerator {
|
|||||||
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 =
|
||||||
@@ -138,9 +140,7 @@ 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 =
|
val newKeyPair =
|
||||||
generateSoftwareKeyPair(params)
|
generateSoftwareKeyPair(params)
|
||||||
?: throw Exception("Failed to generate underlying software key pair.")
|
?: throw Exception("Failed to generate underlying software key pair.")
|
||||||
@@ -149,9 +149,7 @@ object CertificateGenerator {
|
|||||||
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)
|
Pair(newKeyPair, chain)
|
||||||
} catch (e: android.os.ServiceSpecificException) {
|
} catch (e: android.os.ServiceSpecificException) {
|
||||||
throw e
|
throw e
|
||||||
@@ -205,7 +203,9 @@ 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 =
|
||||||
|
bits or
|
||||||
|
when (purpose) {
|
||||||
KeyPurpose.SIGN -> KeyUsage.digitalSignature
|
KeyPurpose.SIGN -> KeyUsage.digitalSignature
|
||||||
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
|
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
|
||||||
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
|
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
|
||||||
@@ -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,7 +278,8 @@ 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 =
|
||||||
|
JcaX509v3CertificateBuilder(
|
||||||
subject,
|
subject,
|
||||||
params.certificateSerial ?: BigInteger.ONE,
|
params.certificateSerial ?: BigInteger.ONE,
|
||||||
notBefore,
|
notBefore,
|
||||||
@@ -288,12 +293,16 @@ object CertificateGenerator {
|
|||||||
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) {
|
||||||
|
"EC",
|
||||||
|
"ECDSA" -> "SHA256withECDSA"
|
||||||
"RSA" -> "SHA256withRSA"
|
"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)
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
.build(keyPair.private)
|
.build(keyPair.private)
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +114,10 @@ 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) {
|
||||||
|
"EC",
|
||||||
|
"ECDSA" -> "EC"
|
||||||
"RSA" -> "RSA"
|
"RSA" -> "RSA"
|
||||||
else -> certs[0].publicKey.algorithm
|
else -> certs[0].publicKey.algorithm
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,8 @@ 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 =
|
||||||
|
when (propertyName) {
|
||||||
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
|
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
|
||||||
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
|
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
|
||||||
else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.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,
|
// 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,19 +400,19 @@ 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"
|
||||||
@@ -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) ->
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ object AndroidPermissionUtils {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,23 +33,25 @@ object AndroidPermissionUtils {
|
|||||||
|
|
||||||
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()
|
||||||
|
?: run {
|
||||||
|
SystemLogger.warning(
|
||||||
|
"AndroidPermissionUtils: Context is null, failing permission check safely."
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user