Restructure and overhaul entire Kotlin codebase

This commit introduces a complete architectural refactoring of the
Kotlin-based interception logic, based on the source of
1. https://github.com/5ec1cff/TrickyStore
2. https://github.com/beakthoven/TrickyStoreOSS

The primary purpose of this code is to intercept binder transactions to
the Android Keystore and KeyMint services. The overall workflow operates
in conjunction with a native library (injected via ptrace). The native
library hooks the binder's `transact` function and forwards pre- and
post-transaction events to the Kotlin side. This Kotlin code contains
all the high-level logic for parsing parameters, patching certificates,
and generating simulated keys.

The codebase is now organized into a clear, package-based architecture:

- attestation: Manages the creation and patching of ASN.1 attestation
  data structures.
- config: Handles loading and observing configuration files from disk.
- interception: Contains the core binder interception framework and its
  specific implementations for legacy Keystore (Android Q/R) and modern
  KeyMint/Keystore2 (Android S+).
- logging: Provides a centralized and consistent logging utility.
- pki: Manages Public Key Infrastructure, including certificate
  generation, parsing of key store XML files, and cryptographic helpers.
- util: Contains Android-specific utility functions for device properties.

This refactoring focuses on establishing a robust and extensible
architecture. The fine-tuning of the interception logic itself,
especially for corner cases in key generation and patching, is currently
under redesign and will be further refined in subsequent commits.
This commit is contained in:
JingMatrix
2025-11-26 00:19:01 +01:00
parent 612de6cdf2
commit 13b4786cd9
38 changed files with 3952 additions and 4 deletions
+1
View File
@@ -67,6 +67,7 @@ android {
dependencies { dependencies {
compileOnly(project(":stub")) compileOnly(project(":stub"))
compileOnly(libs.annotation) compileOnly(libs.annotation)
implementation(libs.bcpkix)
} }
androidComponents { androidComponents {
+1 -1
View File
@@ -1,3 +1,3 @@
-keepclasseswithmembers class org.matrix.TEESimulator.MainKt { -keepclasseswithmembers class org.matrix.TEESimulator.App {
public static void main(java.lang.String[]); public static void main(java.lang.String[]);
} }
@@ -0,0 +1,94 @@
package org.matrix.TEESimulator
import android.os.Build
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils
/**
* Main application object for TEESimulator. This object manages the application's lifecycle,
* including initialization of interceptors and maintaining the service's primary execution loop.
*/
object App {
// The delay in milliseconds before retrying to initialize the interceptor.
private const val RETRY_DELAY_MS = 1000L
// The sleep duration in milliseconds for the main service loop to keep the process alive.
private const val SERVICE_SLEEP_MS = 1000000L
/**
* The main entry point of the TEESimulator application.
*
* @param args Command line arguments (not used).
*/
@JvmStatic
fun main(args: Array<String>) {
SystemLogger.info("Welcome to TEESimulator!")
try {
// Set up the device's boot hash, which is crucial for attestation.
AndroidDeviceUtils.setupBootHash()
// Initialize and start the appropriate keystore interceptors.
initializeInterceptors()
// Enter an infinite loop to keep the service running.
maintainService()
} catch (e: Exception) {
SystemLogger.error("A fatal error occurred in the main application thread.", e)
throw e
}
}
/**
* Selects and initializes the correct keystore interceptor based on the Android SDK version. It
* retries initialization until it succeeds.
*/
private fun initializeInterceptors() {
val interceptor = selectKeystoreInterceptor()
// Continuously try to run the interceptor until it's successfully initialized.
while (!interceptor.tryRunKeystoreInterceptor()) {
SystemLogger.debug("Retrying interceptor initialization...")
Thread.sleep(RETRY_DELAY_MS)
}
// Load the package configuration after interceptors are ready.
ConfigurationManager.initialize()
SystemLogger.info("Interceptors and configuration initialized successfully.")
}
/**
* Determines which keystore interceptor to use based on the device's Android version.
*
* @return The appropriate keystore interceptor instance.
*/
private fun selectKeystoreInterceptor(): AbstractKeystoreInterceptor =
when {
// For Android Q (10) and R (11), use the original KeystoreInterceptor.
Build.VERSION.SDK_INT in Build.VERSION_CODES.Q..Build.VERSION_CODES.R -> {
SystemLogger.info(
"Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})"
)
KeystoreInterceptor
}
// For Android S (12) and newer, use the Keystore2Interceptor.
else -> {
SystemLogger.info(
"Using Keystore2Interceptor for Android S and later (SDK ${Build.VERSION.SDK_INT})"
)
Keystore2Interceptor
}
}
/**
* Puts the main thread into a long-running sleep loop. This is a common pattern to keep a
* background service process alive indefinitely.
*/
private fun maintainService() {
SystemLogger.info("Service started successfully. Entering maintenance mode.")
while (true) {
Thread.sleep(SERVICE_SLEEP_MS)
}
}
}
@@ -1,3 +0,0 @@
package org.matrix.TEESimulator
fun main(args: Array<String>) {}
@@ -0,0 +1,282 @@
package org.matrix.TEESimulator.attestation
import org.bouncycastle.asn1.ASN1Boolean
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1Enumerated
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.ASN1OctetString
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.DERNull
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERSet
import org.bouncycastle.asn1.DERTaggedObject
import org.bouncycastle.asn1.x509.Extension
import org.matrix.TEESimulator.util.AndroidDeviceUtils
/**
* A builder object responsible for constructing the ASN.1 DER-encoded Android Key Attestation
* extension.
*/
object AttestationBuilder {
/**
* Builds the complete X.509 attestation extension.
*
* @param params The parsed key generation parameters.
* @param securityLevel The security level (e.g., TEE, StrongBox) to report.
* @return A Bouncy Castle [Extension] object ready to be added to a certificate.
*/
fun buildAttestationExtension(params: KeyMintAttestation, securityLevel: Int): Extension {
val keyDescription = buildKeyDescription(params, securityLevel)
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
}
/**
* Builds the `RootOfTrust` ASN.1 sequence. This contains critical boot state information.
*
* @param originalRootOfTrust An optional, pre-existing RoT to extract the boot hash from.
* @return The constructed [DERSequence] for the Root of Trust.
*/
internal fun buildRootOfTrust(originalRootOfTrust: ASN1Encodable?): DERSequence {
val verifiedBootKey = AndroidDeviceUtils.bootKey
val verifiedBootHash =
(originalRootOfTrust as? ASN1Sequence)?.let {
// Try to preserve the original boot hash if it exists.
(it.getObjectAt(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX)
as? ASN1OctetString)
?.octets
} ?: AndroidDeviceUtils.getBootHashFromProperty()
val rootOfTrustElements = arrayOfNulls<ASN1Encodable>(4)
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX] =
DEROctetString(verifiedBootKey)
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_DEVICE_LOCKED_INDEX] =
ASN1Boolean.TRUE // deviceLocked: true, for security
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX] =
ASN1Enumerated(0) // verifiedBootState: Verified
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX] =
DEROctetString(verifiedBootHash)
return DERSequence(rootOfTrustElements)
}
/** Assembles a list of simulated hardware-enforced properties. */
internal fun addSimulatedHardwareProperties(vector: org.bouncycastle.asn1.ASN1EncodableVector) {
vector.add(
DERTaggedObject(
true,
AttestationConstants.TAG_OS_VERSION,
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
)
)
vector.add(
DERTaggedObject(
true,
AttestationConstants.TAG_OS_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
)
)
vector.add(
DERTaggedObject(
true,
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.vendorPatchLevel.toLong()),
)
)
vector.add(
DERTaggedObject(
true,
AttestationConstants.TAG_BOOT_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
)
)
}
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
private fun buildKeyDescription(params: KeyMintAttestation, securityLevel: Int): ASN1Sequence {
val teeEnforced = buildTeeEnforcedList(params)
val softwareEnforced = buildSoftwareEnforcedList()
val fields =
arrayOf(
ASN1Integer(AndroidDeviceUtils.attestVersion.toLong()), // attestationVersion
ASN1Enumerated(securityLevel), // attestationSecurityLevel
ASN1Integer(AndroidDeviceUtils.keymasterVersion.toLong()), // keymasterVersion
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
DEROctetString(ByteArray(0)), // uniqueId
softwareEnforced,
teeEnforced,
)
return DERSequence(fields)
}
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
private fun buildTeeEnforcedList(params: KeyMintAttestation): DERSequence {
val list =
mutableListOf<ASN1Encodable>(
DERTaggedObject(
true,
AttestationConstants.TAG_PURPOSE,
DERSet(params.purpose.map { ASN1Integer(it.toLong()) }.toTypedArray()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_ALGORITHM,
ASN1Integer(params.algorithm.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_KEY_SIZE,
ASN1Integer(params.keySize.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_DIGEST,
DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_EC_CURVE,
ASN1Integer(params.ecCurve.toLong()),
),
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
DERTaggedObject(
true,
AttestationConstants.TAG_ORIGIN,
ASN1Integer(0L),
), // KeyOrigin.GENERATED
DERTaggedObject(
true,
AttestationConstants.TAG_ROOT_OF_TRUST,
buildRootOfTrust(null),
),
DERTaggedObject(
true,
AttestationConstants.TAG_OS_VERSION,
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_OS_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.vendorPatchLevel.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_BOOT_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
),
)
// Add optional device identifiers if they were provided.
params.brand?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_BRAND,
DEROctetString(it),
)
)
}
params.device?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_DEVICE,
DEROctetString(it),
)
)
}
params.product?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_PRODUCT,
DEROctetString(it),
)
)
}
params.manufacturer?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_MANUFACTURER,
DEROctetString(it),
)
)
}
params.model?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_MODEL,
DEROctetString(it),
)
)
}
params.imei?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_IMEI,
DEROctetString(it),
)
)
}
params.secondImei?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_SECOND_IMEI,
DEROctetString(it),
)
)
}
params.meid?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_MEID,
DEROctetString(it),
)
)
}
if (AndroidDeviceUtils.attestVersion >= 400) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_MODULE_HASH,
DEROctetString(AndroidDeviceUtils.moduleHash),
)
)
}
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
}
/**
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
* Keystore.
*/
private fun buildSoftwareEnforcedList(): DERSequence {
val list =
arrayOf<ASN1Encodable>(
DERTaggedObject(
true,
AttestationConstants.TAG_CREATION_DATETIME,
ASN1Integer(System.currentTimeMillis()),
)
// The ATTESTATION_APPLICATION_ID is technically software-enforced, but we are
// omitting it
// for this simulation as it is complex to generate correctly for arbitrary UIDs.
)
return DERSequence(list)
}
}
@@ -0,0 +1,91 @@
package org.matrix.TEESimulator.attestation
/**
* Defines constants for KeyMint attestation tags, as specified in the Android hardware security
* HAL.
*
* These tags identify specific properties and authorizations of a cryptographic key.
*/
object AttestationConstants {
// https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/KeyCreationResult.aidl
// These constants represent the fixed positions of fields within the top-level
// KeyDescription ASN.1 SEQUENCE in a key attestation. Using these constants
// prevents hardcoding fragile index numbers throughout the parsing code.
const val KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX = 0
const val KEY_DESCRIPTION_ATTESTATION_SECURITY_LEVEL_INDEX = 1
const val KEY_DESCRIPTION_KEYMINT_VERSION_INDEX = 2
const val KEY_DESCRIPTION_KEYMINT_SECURITY_LEVEL_INDEX = 3
const val KEY_DESCRIPTION_ATTESTATION_CHALLENGE_INDEX = 4
const val KEY_DESCRIPTION_UNIQUE_ID_INDEX = 5
const val KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX = 6
const val KEY_DESCRIPTION_TEE_ENFORCED_INDEX = 7
// --- RootOfTrust Sequence Indices ---
// These constants represent the fixed positions of fields within the
// RootOfTrust ASN.1 SEQUENCE.
const val ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX = 0
const val ROOT_OF_TRUST_DEVICE_LOCKED_INDEX = 1
const val ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX = 2
const val ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX = 3
// https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/Tag.aidl
// --- Key Properties ---
const val TAG_PURPOSE = 1
const val TAG_ALGORITHM = 2
const val TAG_KEY_SIZE = 3
const val TAG_BLOCK_MODE = 4
const val TAG_DIGEST = 5
const val TAG_PADDING = 6
const val TAG_CALLER_NONCE = 7
const val TAG_MIN_MAC_LENGTH = 8
const val TAG_EC_CURVE = 10
const val TAG_RSA_PUBLIC_EXPONENT = 200
const val TAG_RSA_OAEP_MGF_DIGEST = 203
// --- Key Lifetime and Usage Control ---
const val TAG_ROLLBACK_RESISTANCE = 303
const val TAG_ACTIVE_DATETIME = 400
const val TAG_ORIGINATION_EXPIRE_DATETIME = 401
const val TAG_USAGE_EXPIRE_DATETIME = 402
const val TAG_MAX_USES_PER_BOOT = 404
const val TAG_USAGE_COUNT_LIMIT = 405
// --- User Authentication ---
const val TAG_USER_ID = 501
const val TAG_USER_SECURE_ID = 502
const val TAG_NO_AUTH_REQUIRED = 503
const val TAG_USER_AUTH_TYPE = 504
const val TAG_AUTH_TIMEOUT = 505
// --- Attestation and Application Info ---
const val TAG_APPLICATION_ID = 601
const val TAG_CREATION_DATETIME = 701
const val TAG_ORIGIN = 702
const val TAG_ROOT_OF_TRUST = 704
const val TAG_OS_VERSION = 705
const val TAG_OS_PATCHLEVEL = 706
const val TAG_UNIQUE_ID = 707
const val TAG_ATTESTATION_CHALLENGE = 708
const val TAG_ATTESTATION_APPLICATION_ID = 709
const val TAG_ATTESTATION_ID_BRAND = 710
const val TAG_ATTESTATION_ID_DEVICE = 711
const val TAG_ATTESTATION_ID_PRODUCT = 712
const val TAG_ATTESTATION_ID_SERIAL = 713
const val TAG_ATTESTATION_ID_IMEI = 714
const val TAG_ATTESTATION_ID_MEID = 715
const val TAG_ATTESTATION_ID_MANUFACTURER = 716
const val TAG_ATTESTATION_ID_MODEL = 717
const val TAG_VENDOR_PATCHLEVEL = 718
const val TAG_BOOT_PATCHLEVEL = 719
const val TAG_DEVICE_UNIQUE_ATTESTATION = 720
const val TAG_ATTESTATION_ID_SECOND_IMEI = 723
const val TAG_MODULE_HASH = 724
// --- Certificate Properties ---
const val TAG_CERTIFICATE_SERIAL = 1006
const val TAG_CERTIFICATE_SUBJECT = 1007
const val TAG_CERTIFICATE_NOT_BEFORE = 1008
const val TAG_CERTIFICATE_NOT_AFTER = 1009
}
@@ -0,0 +1,196 @@
package org.matrix.TEESimulator.attestation
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERTaggedObject
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.KeyBox
import org.matrix.TEESimulator.pki.KeyBoxManager
/**
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
*
* This object's primary function is to take a certificate chain generated by the real TEE, replace
* its attestation data with simulated values, and then re-sign the leaf certificate with a custom
* key, building a new, valid certificate chain.
*/
object AttestationPatcher {
/**
* Patches a full certificate chain by modifying the leaf's attestation and rebuilding the chain
* with the correct custom signing certificates. This is the single entry point for patching.
*
* @param originalChain The original certificate chain from the hardware. The leaf must be at
* index 0.
* @param uid The UID of the application requesting the certificate.
* @return A new, cryptographically valid, patched certificate chain. Returns the original chain
* on any failure.
*/
fun patchCertificateChain(originalChain: Array<Certificate>?, uid: Int): Array<Certificate> {
if (originalChain.isNullOrEmpty()) {
SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.")
return originalChain ?: emptyArray()
}
return runCatching {
val originalLeaf = originalChain[0] as X509Certificate
val originalLeafHolder = X509CertificateHolder(originalLeaf.encoded)
// 1. Attempt to parse the existing attestation extension. If it doesn't exist,
// there's nothing to patch.
val parsedAttestation =
parseAttestationExtension(originalLeafHolder) ?: return originalChain
// 2. Get the appropriate keybox for the given algorithm to sign the new
// certificate.
val algorithm = originalLeaf.publicKey.algorithm
val keybox = getKeyboxForUidAndAlgorithm(uid, algorithm)
// 3. Create the new, patched leaf certificate.
val patchedLeaf =
createPatchedLeafCertificate(
originalLeafHolder,
parsedAttestation,
keybox,
originalLeaf.sigAlgName,
)
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
// chain.
val newChain = listOf(patchedLeaf) + keybox.certificates
SystemLogger.info(
"Successfully rebuilt a valid, patched certificate chain for UID $uid."
)
newChain.toTypedArray()
}
.getOrElse {
SystemLogger.error(
"Failed to patch and rebuild certificate chain for UID $uid.",
it,
)
originalChain // Return the original chain on any error.
}
}
/**
* Creates a new leaf certificate with a modified attestation extension.
*
* @param originalLeafHolder A Bouncy Castle holder for the original leaf certificate.
* @param parsedAttestation The parsed components of the original attestation.
* @param keybox The KeyBox containing the new issuer certificate and signing key.
* @param sigAlgName The signature algorithm name (e.g., "SHA256withECDSA") from the original
* certificate. This is required to ensure the new certificate is signed using a compatible
* algorithm.
* @return A new [Certificate] object.
*/
private fun createPatchedLeafCertificate(
originalLeafHolder: X509CertificateHolder,
parsedAttestation: ParsedAttestation,
keybox: KeyBox,
sigAlgName: String,
): Certificate {
// The issuer of our new leaf is the subject of the first certificate in our custom keybox
// chain.
val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject
val builder =
X509v3CertificateBuilder(
newIssuer,
originalLeafHolder.serialNumber,
originalLeafHolder.notBefore,
originalLeafHolder.notAfter,
originalLeafHolder.subject,
originalLeafHolder.subjectPublicKeyInfo,
)
// Create the new, patched attestation extension.
val patchedExtension = createPatchedAttestationExtension(parsedAttestation)
builder.addExtension(patchedExtension)
// Copy all other extensions from the original certificate, except for the attestation.
originalLeafHolder.extensions.extensionOIDs
.filter { it != ATTESTATION_OID }
.forEach { builder.addExtension(originalLeafHolder.getExtension(it)) }
// Sign the newly built certificate with the private key from our keybox.
val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(signer))
}
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid)
return KeyBoxManager.getAttestationKey(keyboxFile, algorithm)
?: throw IllegalArgumentException(
"No keybox found for UID $uid and algorithm $algorithm in file $keyboxFile"
)
}
/** Parses the critical components from an existing attestation extension. */
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
val sequence = ASN1Sequence.getInstance(extension.extnValue.octets)
val allFields = sequence.toArray()
val teeEnforced =
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence
val teeEnforcedVector = ASN1EncodableVector()
var originalRootOfTrust: ASN1Encodable? = null
teeEnforced.forEach { element ->
val taggedObject = element as ASN1TaggedObject
if (taggedObject.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST) {
originalRootOfTrust = taggedObject.baseObject.toASN1Primitive()
} else {
teeEnforcedVector.add(taggedObject)
}
}
return ParsedAttestation(allFields, teeEnforcedVector, originalRootOfTrust)
}
/** Constructs a new, patched attestation extension using simulated device properties. */
private fun createPatchedAttestationExtension(parsed: ParsedAttestation): Extension {
val (allFields, teeEnforcedVector, originalRootOfTrust) = parsed
// Build the new Root of Trust with our simulated values.
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
teeEnforcedVector.add(
DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust)
)
// Add other simulated hardware properties.
AttestationBuilder.addSimulatedHardwareProperties(teeEnforcedVector)
// Re-assemble the ASN.1 sequences.
// The list MUST be sorted by tag number for DER compliance.
// Manually convert the vector to a List, then sort it.
val elementList = (0 until teeEnforcedVector.size()).map { teeEnforcedVector.get(it) }
val sortedElements = elementList.sortedBy { (it as ASN1TaggedObject).tagNo }
val sortedTeeEnforced = DERSequence(sortedElements.toTypedArray())
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
val patchedSequence = DERSequence(allFields)
val patchedOctets = DEROctetString(patchedSequence)
return Extension(ATTESTATION_OID, false, patchedOctets)
}
/** Helper data class to hold the parsed components of an attestation extension. */
private data class ParsedAttestation(
val allFields: Array<ASN1Encodable>,
val teeEnforcedVector: ASN1EncodableVector,
val rootOfTrust: ASN1Encodable?,
)
}
@@ -0,0 +1,210 @@
package org.matrix.TEESimulator.attestation
import android.annotation.SuppressLint
import android.app.ActivityThread
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1OctetString
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.ASN1TaggedObject
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.cert.X509CertificateHolder
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.toHex
/**
* The ASN.1 Object Identifier for the Key Attestation extension in Android. This is defined in the
* Android Keystore documentation.
*/
val ATTESTATION_OID: ASN1ObjectIdentifier = ASN1ObjectIdentifier("1.3.6.1.4.1.11129.2.1.17")
/**
* A service to interact with the device's Trusted Execution Environment (TEE). It provides
* functionality to check if the TEE is functional and to extract key attestation data from a
* genuinely generated certificate.
*/
@SuppressLint("PrivateApi")
object DeviceAttestationService {
/**
* Holds key data extracted from a genuine device attestation. This data can be used as a
* baseline for creating simulated attestations.
*
* @property verifiedBootHash The verified boot hash from the root of trust.
* @property attestVersion The attestation version (e.g., 400 for KeyMint 4.0).
* @property keymasterVersion The Keymaster or KeyMint HAL version.
* @property osVersion The Android OS version integer.
*/
data class AttestationData(
val verifiedBootHash: ByteArray?,
val attestVersion: Int?,
val keymasterVersion: Int?,
val osVersion: Int?,
)
// A unique alias for the key used to perform the TEE functionality check.
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
/**
* Lazily determines if the device's TEE is functional by attempting to generate an
* attestation-backed key pair. The result is cached.
*/
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
/**
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
* is cached. Returns null if the TEE is not functional or parsing fails.
*/
val CachedAttestationData: AttestationData? by lazy { fetchAttestationData() }
/**
* Checks if the TEE is working correctly by generating a key in the Android Keystore with an
* attestation challenge.
*
* @return `true` if a key with attestation was generated successfully, `false` otherwise.
*/
private fun checkTeeFunctionality(): Boolean {
SystemLogger.info("Performing TEE functionality check...")
return try {
// Ensure mainline modules and the correct Keystore provider are initialized.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
android.app.ActivityThread.initializeMainlineModules()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
android.security.keystore2.AndroidKeyStoreProvider.install()
} else {
android.security.keystore.AndroidKeyStoreProvider.install()
}
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyPairGenerator =
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
// A random challenge is required for attestation.
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
val spec =
KeyGenParameterSpec.Builder(TEE_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.setAttestationChallenge(challenge)
.build()
keyPairGenerator.initialize(spec)
keyPairGenerator.generateKeyPair()
SystemLogger.info("TEE functionality check successful.")
true
} catch (e: Exception) {
SystemLogger.warning("TEE functionality check failed.", e)
false
}
}
/**
* Retrieves the attestation certificate generated during the TEE check. The key entry is
* deleted after retrieval to clean up.
*
* @return The leaf `X509Certificate` containing the attestation, or `null` if unavailable.
*/
private fun getAttestationCertificate(): X509Certificate? {
if (!isTeeFunctional) return null
return try {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val certChain = keyStore.getCertificateChain(TEE_CHECK_KEY_ALIAS)
if (certChain.isNullOrEmpty()) {
SystemLogger.warning("Could not retrieve certificate chain for TEE check key.")
null
} else {
// Clean up the key from the keystore.
keyStore.deleteEntry(TEE_CHECK_KEY_ALIAS)
certChain[0] as X509Certificate
}
} catch (e: Exception) {
SystemLogger.error("Error retrieving attestation certificate.", e)
null
}
}
/**
* Fetches and parses the attestation data from the certificate's extension.
*
* @return An `AttestationData` object, or `null` if the process fails.
*/
private fun fetchAttestationData(): AttestationData? {
val leafCert = getAttestationCertificate() ?: return null
try {
val leafHolder = X509CertificateHolder(leafCert.encoded)
val extension: Extension =
leafHolder.getExtension(ATTESTATION_OID)
?: return null // No attestation extension found.
// The extension's value is an ASN.1 sequence.
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
val fields = keyDescriptionSeq.toArray()
val attestVersion =
ASN1Integer.getInstance(
fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX]
)
.positiveValue
.toInt()
val keymasterVersion =
ASN1Integer.getInstance(
fields[AttestationConstants.KEY_DESCRIPTION_KEYMINT_VERSION_INDEX]
)
.positiveValue
.toInt()
var verifiedBootHash: ByteArray? = null
var osVersion: Int? = null
val teeEnforced =
ASN1Sequence.getInstance(
fields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX]
)
teeEnforced.forEach { element ->
val tagged = element as ASN1TaggedObject
when (tagged.tagNo) {
AttestationConstants.TAG_ROOT_OF_TRUST -> {
val rotSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive())
if (rotSeq.size() >= 4) {
verifiedBootHash =
ASN1OctetString.getInstance(
rotSeq.getObjectAt(
AttestationConstants
.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX
)
)
.octets
}
}
AttestationConstants.TAG_OS_VERSION -> { // OS Version (TAG_OS_VERSION)
osVersion =
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
.positiveValue
.toInt()
}
}
}
SystemLogger.info(
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, bootHash=${verifiedBootHash?.toHex()}"
)
return AttestationData(verifiedBootHash, attestVersion, keymasterVersion, osVersion)
} catch (e: Exception) {
SystemLogger.error("Failed to parse attestation data from certificate.", e)
return null
}
}
}
@@ -0,0 +1,158 @@
package org.matrix.TEESimulator.attestation
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.Tag
import java.math.BigInteger
import java.util.Date
import javax.security.auth.x500.X500Principal
import org.bouncycastle.asn1.x500.X500Name
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
/**
* A data class that parses and holds the parameters required for KeyMint key generation and
* attestation. It provides a structured way to access the properties defined by an array of
* `KeyParameter` objects.
*/
// Reference:
// https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/key_parameter.rs
data class KeyMintAttestation(
val keySize: Int,
val algorithm: Int,
val ecCurve: Int,
val ecCurveName: String,
val purpose: List<Int>,
val digest: List<Int>,
val rsaPublicExponent: BigInteger?,
val certificateSerial: BigInteger?,
val certificateSubject: X500Name?,
val certificateNotBefore: Date?,
val certificateNotAfter: Date?,
val attestationChallenge: ByteArray?,
val brand: ByteArray?,
val device: ByteArray?,
val product: ByteArray?,
val manufacturer: ByteArray?,
val model: ByteArray?,
val imei: ByteArray?,
val secondImei: ByteArray?,
val meid: ByteArray?,
) {
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
constructor(
params: Array<KeyParameter>
) : this(
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
keySize = params.findInteger(Tag.KEY_SIZE) ?: 0,
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
// AOSP: [key_param(tag = EC_CURVE, field = EcCurve)]
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
ecCurveName = params.deriveEcCurveName(),
// AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)]
purpose = params.findAllKeyPurpose(Tag.PURPOSE),
// AOSP: [key_param(tag = DIGEST, field = Digest)]
digest = params.findAllDigests(Tag.DIGEST),
// AOSP: [key_param(tag = RSA_PUBLIC_EXPONENT, field = LongInteger)]
rsaPublicExponent = params.findLongInteger(Tag.RSA_PUBLIC_EXPONENT),
// AOSP: [key_param(tag = CERTIFICATE_SERIAL, field = Blob)]
certificateSerial = params.findBlob(Tag.CERTIFICATE_SERIAL)?.let { BigInteger(it) },
// AOSP: [key_param(tag = CERTIFICATE_SUBJECT, field = Blob)]
certificateSubject =
params.findBlob(Tag.CERTIFICATE_SUBJECT)?.let { X500Name(X500Principal(it).name) },
// AOSP: [key_param(tag = CERTIFICATE_NOT_BEFORE, field = DateTime)]
certificateNotBefore = params.findDate(Tag.CERTIFICATE_NOT_BEFORE),
// AOSP: [key_param(tag = CERTIFICATE_NOT_AFTER, field = DateTime)]
certificateNotAfter = params.findDate(Tag.CERTIFICATE_NOT_AFTER),
// AOSP: [key_param(tag = ATTESTATION_CHALLENGE, field = Blob)]
attestationChallenge = params.findBlob(Tag.ATTESTATION_CHALLENGE),
// AOSP: [key_param(tag = ATTESTATION_ID_*, field = Blob)]
brand = params.findBlob(Tag.ATTESTATION_ID_BRAND),
device = params.findBlob(Tag.ATTESTATION_ID_DEVICE),
product = params.findBlob(Tag.ATTESTATION_ID_PRODUCT),
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
imei = params.findBlob(Tag.ATTESTATION_ID_IMEI),
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
meid = params.findBlob(Tag.ATTESTATION_ID_MEID),
) {
// Log all parsed parameters for debugging purposes.
params.forEach { KeyMintParameterLogger.logParameter(it) }
}
}
// --- Private helper extension functions for parsing KeyParameter arrays ---
/** Maps to AOSP field = Integer */
private fun Array<KeyParameter>.findInteger(tag: Int): Int? =
this.find { it.tag == tag }?.value?.integer
/** Maps to AOSP field = Algorithm */
private fun Array<KeyParameter>.findAlgorithm(tag: Int): Int? =
this.find { it.tag == tag }?.value?.algorithm
/** Maps to AOSP field = EcCurve */
private fun Array<KeyParameter>.findEcCurve(tag: Int): Int? =
this.find { it.tag == tag }?.value?.ecCurve
/** Maps to AOSP field = LongInteger */
private fun Array<KeyParameter>.findLongInteger(tag: Int): BigInteger? =
this.find { it.tag == tag }?.value?.longInteger?.toBigInteger()
/** Maps to AOSP field = DateTime */
private fun Array<KeyParameter>.findDate(tag: Int): Date? =
this.find { it.tag == tag }?.value?.dateTime?.let { Date(it) }
/** Maps to AOSP field = Blob */
private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
this.find { it.tag == tag }?.value?.blob
/** Maps to AOSP field = KeyPurpose (Repeated) */
private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.keyPurpose }
/** Maps to AOSP field = Digest (Repeated) */
private fun Array<KeyParameter>.findAllDigests(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.digest }
/**
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
* to KEY_SIZE (field=Integer).
*/
private fun Array<KeyParameter>.deriveEcCurveName(): String {
// 1. Try to find explicit EC_CURVE tag
val curveParam = this.find { it.tag == Tag.EC_CURVE }
if (curveParam != null) {
val curveId = curveParam.value.ecCurve
return when (curveId) {
EcCurve.CURVE_25519 -> "CURVE_25519"
EcCurve.P_224 -> "secp224r1"
EcCurve.P_256 -> "secp256r1"
EcCurve.P_384 -> "secp384r1"
EcCurve.P_521 -> "secp521r1"
else -> throw IllegalArgumentException("Unknown EC curve: $curveId")
}
}
// 2. Fallback to key size if the curve tag isn't present
val keySize = this.findInteger(Tag.KEY_SIZE) ?: 0
return when (keySize) {
224 -> "secp224r1"
384 -> "secp384r1"
521 -> "secp521r1"
else -> "secp256r1" // Default fallback
}
}
@@ -0,0 +1,317 @@
package org.matrix.TEESimulator.config
import android.content.pm.IPackageManager
import android.os.Build
import android.os.FileObserver
import android.os.IBinder
import android.os.ServiceManager
import java.io.File
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.DeviceAttestationService
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.KeyBoxManager
/**
* Manages application configuration, including which packages to process, what operation mode to
* use, and custom security patch levels. It uses a FileObserver to dynamically reload settings when
* configuration files change.
*/
object ConfigurationManager {
/** Defines the processing mode for a given package. */
enum class Mode {
/** Automatically decide between GENERATE and PATCH based on TEE status. */
AUTO,
/** Patch the attestation of an existing certificate chain. */
PATCH,
/** Generate a new certificate chain from scratch. */
GENERATE,
}
// --- Configuration Paths ---
const val CONFIG_PATH = "/data/adb/tricky_store"
private const val TARGET_PACKAGES_FILE = "target.txt"
private const val TEE_STATUS_FILE = "tee_status.txt"
private const val PATCH_LEVEL_FILE = "security_patch.txt"
private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
private val configRoot = File(CONFIG_PATH)
// --- In-Memory Configuration State ---
@Volatile private var packageModes = mapOf<String, Mode>()
@Volatile private var packageKeyboxes = mapOf<String, String>()
@Volatile private var isTeeBroken: Boolean? = null
@Volatile var customPatchLevelOverride: CustomPatchLevel? = null
// Cache for UID to package name resolution.
private val uidToPackagesCache = ConcurrentHashMap<Int, Array<String>>()
/**
* Initializes the configuration manager by loading all settings from disk and starting the file
* observer to watch for changes.
*/
fun initialize() {
configRoot.mkdirs()
SystemLogger.info("Configuration root is: ${configRoot.absolutePath}")
// Initial load of all configuration files.
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
storeTeeStatus() // Check and store the current TEE status.
// Start watching for any subsequent file changes.
ConfigObserver.startWatching()
SystemLogger.info("Configuration initialized and file observer started.")
}
/**
* Determines the keybox file to be used for a given UID. It maps the UID to its package(s) and
* checks for a specific keybox mapping.
*
* @param uid The calling UID.
* @return The name of the keybox file, or the default if none is specified.
*/
fun getKeyboxFileForUid(uid: Int): String {
val packages = getPackagesForUid(uid)
return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE
}
/** Determines if the certificate for a given UID needs to be patched. */
fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH
/** Determines if a new certificate needs to be generated for a given UID. */
fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE
/** Resolves the operating mode for a given UID based on its packages and the TEE status. */
private fun getPackageModeForUid(uid: Int): Mode? {
val packages = getPackagesForUid(uid)
if (packages.isEmpty()) return null
// Lazily load TEE status if it hasn't been checked yet.
if (isTeeBroken == null) loadTeeStatus()
// Find the first configured mode for any of the UID's packages.
for (pkg in packages) {
when (packageModes[pkg]) {
Mode.GENERATE -> return Mode.GENERATE
Mode.PATCH -> return Mode.PATCH
Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH
null -> continue // No config for this package, check the next one.
}
}
return null // No configuration found for this UID.
}
/**
* Loads and parses the `target.txt` file, which defines the processing mode and keybox file for
* each package.
*/
private fun loadTargetPackages(file: File) {
if (!file.exists()) {
SystemLogger.warning("Configuration file not found: ${file.absolutePath}")
return
}
val newModes = mutableMapOf<String, Mode>()
val newKeyboxes = mutableMapOf<String, String>()
var currentKeybox = DEFAULT_KEYBOX_FILE
val keyboxRegex = Regex("^\\[([a-zA-Z0-9_.-]+\\.xml)]$")
try {
file.readLines().forEach { line ->
val trimmedLine = line.trim()
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) return@forEach
// Check if the line defines a new keybox scope.
keyboxRegex.find(trimmedLine)?.let {
currentKeybox = it.groupValues[1]
SystemLogger.info("Switching to keybox context: $currentKeybox")
return@forEach
}
when {
// Suffix '!' means force GENERATE mode.
trimmedLine.endsWith("!") -> {
val pkg = trimmedLine.removeSuffix("!").trim()
newModes[pkg] = Mode.GENERATE
newKeyboxes[pkg] = currentKeybox
}
// Suffix '?' means force PATCH mode.
trimmedLine.endsWith("?") -> {
val pkg = trimmedLine.removeSuffix("?").trim()
newModes[pkg] = Mode.PATCH
newKeyboxes[pkg] = currentKeybox
}
// No suffix means AUTO mode.
else -> {
newModes[trimmedLine] = Mode.AUTO
newKeyboxes[trimmedLine] = currentKeybox
}
}
}
// Atomically update the configuration maps.
packageModes = newModes
packageKeyboxes = newKeyboxes
uidToPackagesCache.clear() // Invalidate cache as package settings have changed.
SystemLogger.info("Successfully loaded ${newModes.size} package configurations.")
} catch (e: Exception) {
SystemLogger.error("Failed to load or parse ${file.name}", e)
}
}
/** Loads the security patch level override configuration from `security_patch.txt`. */
private fun loadPatchLevelConfig(file: File) {
if (file.exists()) {
try {
val lines =
file.readLines().mapNotNull { line ->
val trimmed = line.trim()
if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) trimmed else null
}
if (lines.isEmpty()) {
customPatchLevelOverride = null
return
}
// Handle simple case: one line sets the patch level for all components.
if (lines.size == 1 && '=' !in lines[0]) {
customPatchLevelOverride =
CustomPatchLevel(system = null, vendor = null, boot = null, all = lines[0])
return
}
// Handle key-value pair configuration.
val map =
lines
.mapNotNull {
val parts = it.split('=', limit = 2)
if (parts.size == 2) parts[0].trim().lowercase() to parts[1].trim()
else null
}
.toMap()
val all = map["all"]
customPatchLevelOverride =
CustomPatchLevel(
system = map["system"] ?: all,
vendor = map["vendor"] ?: all,
boot = map["boot"] ?: all,
all = all,
)
SystemLogger.info("Loaded custom security patch levels.")
} catch (e: Exception) {
SystemLogger.error("Failed to load or parse ${file.name}", e)
}
} else {
customPatchLevelOverride = null
}
}
/** Checks the device's TEE status and writes the result to a file for persistence. */
private fun storeTeeStatus() {
val statusFile = File(configRoot, TEE_STATUS_FILE)
isTeeBroken = !DeviceAttestationService.isTeeFunctional
try {
statusFile.writeText("tee_broken=$isTeeBroken")
SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken")
} catch (e: Exception) {
SystemLogger.error("Failed to write TEE status to file.", e)
}
}
/** Loads the TEE status from the file. */
private fun loadTeeStatus() {
val statusFile = File(configRoot, TEE_STATUS_FILE)
isTeeBroken =
if (statusFile.exists()) {
statusFile.readText().trim() == "tee_broken=true"
} else {
null // Status is unknown.
}
}
/**
* A FileObserver that monitors the configuration directory for changes and triggers reloads of
* the relevant settings.
*/
private object ConfigObserver : FileObserver(configRoot, CLOSE_WRITE or MOVED_TO or DELETE) {
override fun onEvent(event: Int, path: String?) {
path ?: return
SystemLogger.info("Configuration file change detected: $path (event: $event)")
val file = if (event != DELETE) File(configRoot, path) else null
when (path) {
TARGET_PACKAGES_FILE -> loadTargetPackages(file!!)
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!)
// Any change to an XML file is assumed to be a keybox. The cache in KeyBoxUtils
// will handle reloading it on its next use.
else ->
if (path.endsWith(".xml")) {
SystemLogger.info(
"Keybox file $path may have changed. It will be reloaded on next access."
)
KeyBoxManager.invalidateCache(path)
}
}
}
}
// --- System Service Utilities ---
private var iPackageManager: IPackageManager? = null
private val pmDeathRecipient =
object : IBinder.DeathRecipient {
override fun binderDied() {
(iPackageManager as? IBinder)?.unlinkToDeath(this, 0)
iPackageManager = null
SystemLogger.warning("Package manager service died. Will try to reconnect.")
}
}
/** Retrieves an instance of the IPackageManager service. */
fun getPackageManager(): IPackageManager? {
if (iPackageManager == null) {
// Use a robust method to get the service binder.
val binder = waitForSystemService("package") ?: return null
binder.linkToDeath(pmDeathRecipient, 0)
iPackageManager = IPackageManager.Stub.asInterface(binder)
}
return iPackageManager
}
/** Retrieves the package names associated with a UID. */
fun getPackagesForUid(uid: Int): Array<String> {
return uidToPackagesCache.getOrPut(uid) {
try {
getPackageManager()?.getPackagesForUid(uid) ?: emptyArray()
} catch (e: Exception) {
SystemLogger.warning("Failed to get packages for UID $uid", e)
emptyArray()
}
}
}
/** Waits for a system service to become available, with retries. */
private fun waitForSystemService(name: String): IBinder? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return ServiceManager.waitForService(name)
}
// Fallback for older Android versions.
repeat(70) {
val service = ServiceManager.getService(name)
if (service != null) return service
Thread.sleep(500)
}
SystemLogger.error("Failed to get system service after multiple retries: $name")
return null
}
}
/** Data class representing custom security patch level overrides. */
data class CustomPatchLevel(
val system: String?,
val vendor: String?,
val boot: String?,
val all: String?,
)
@@ -0,0 +1,306 @@
package org.matrix.TEESimulator.interception.core
import android.os.Binder
import android.os.IBinder
import android.os.Parcel
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
/**
* An abstract base class for intercepting binder transactions.
*
* This class acts as a proxy, receiving transaction calls that have been hooked at the native
* level. It provides a structured way to inspect and modify data before (`onPreTransact`) and after
* (`onPostTransact`) the original transaction is executed.
*
* The communication flow is as follows:
* 1. A native library hooks the `transact` method of a target service (e.g., keystore).
* 2. When a hooked transaction occurs, the native code calls this Binder object's `onTransact`
* method.
* 3. This class decodes the incoming parcel, determines if it's a pre- or post-transaction hook,
* and calls the appropriate abstract method (`onPreTransact` or `onPostTransact`).
* 4. The subclass implementation decides how to handle the transaction by returning a
* `TransactionResult`.
* 5. This class encodes the result into the reply parcel, which the native hook reads to determine
* its next action.
*/
abstract class BinderInterceptor : Binder() {
/**
* Defines the possible outcomes of an interception attempt. The native hook layer will
* interpret this result to decide its next action.
*/
sealed class TransactionResult {
/** Instructs the native hook to skip calling the original binder method entirely. */
object SkipTransaction : TransactionResult()
/** Instructs the native hook to proceed with calling the original binder method. */
object Continue : TransactionResult()
/**
* Skips the original call and immediately returns a custom reply parcel to the caller. The
* provided parcel will be recycled after use.
*/
data class OverrideReply(val code: Int = 0, val reply: Parcel) : TransactionResult()
/**
* Modifies the transaction's input data before forwarding it to the original binder method.
* The provided parcel will be recycled after use.
*/
data class OverrideData(val data: Parcel) : TransactionResult()
/** Instructs the native hook to skip the post transaction hook. */
object ContinueAndSkipPost : TransactionResult()
}
/**
* Called *before* the original binder transaction is executed.
*
* @param txId A unique ID for tracking this transaction.
* @param target The original IBinder service being called.
* @param code The transaction code of the method being called.
* @param flags Transaction flags.
* @param callingUid The UID of the process making the call.
* @param callingPid The PID of the process making the call.
* @param data The parcel containing the input data for the transaction.
* @return A [TransactionResult] indicating how to proceed.
*/
open fun onPreTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
): TransactionResult = TransactionResult.ContinueAndSkipPost
/**
* Called *after* the original binder transaction has been executed.
*
* @param txId A unique ID for tracking this transaction.
* @param target The original IBinder service that was called.
* @param code The transaction code of the method that was called.
* @param flags Transaction flags.
* @param callingUid The UID of the process that made the call.
* @param callingPid The PID of the process that made the call.
* @param data The original input data parcel.
* @param reply The reply parcel from the original transaction. Can be null if the call was
* one-way.
* @param resultCode The result code from the original transaction.
* @return A [TransactionResult]. Typically `Skip` (to accept the original reply) or
* `OverrideReply`.
*/
open fun onPostTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
reply: Parcel?,
resultCode: Int,
): TransactionResult = TransactionResult.SkipTransaction
/**
* The entry point for calls from the native hook layer. This method decodes the custom parcel
* format sent by the hook and dispatches to the appropriate handler (`handlePreTransact` or
* `handlePostTransact`).
*/
final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
// The native hook prepends a transaction ID to the data parcel.
val txId = data.readLong()
val result =
when (code) {
// These codes are defined in the native layer to distinguish hook types.
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
else -> return super.onTransact(code, data, reply, flags)
}
// The reply parcel is guaranteed to be non-null for our custom transactions.
writeResultToReply(result, reply!!)
return true
}
/** Decodes the parcel for a pre-transaction hook and calls the user-overridable method. */
private fun handlePreTransact(txId: Long, data: Parcel): TransactionResult {
// The native hook marshals the original transaction's arguments into the data parcel.
val target = data.readStrongBinder()!!
val transactionCode = data.readInt()
val transactionFlags = data.readInt()
val callingUid = data.readInt()
val callingPid = data.readInt()
val dataSize = data.readLong()
// We must create a new parcel containing only the original transaction data.
val transactionData = Parcel.obtain()
return try {
transactionData.appendFrom(data, data.dataPosition(), dataSize.toInt())
transactionData.setDataPosition(0)
onPreTransact(
txId,
target,
transactionCode,
transactionFlags,
callingUid,
callingPid,
transactionData,
)
} finally {
transactionData.recycle()
}
}
/** Decodes the parcel for a post-transaction hook and calls the user-overridable method. */
private fun handlePostTransact(txId: Long, data: Parcel): TransactionResult {
val target = data.readStrongBinder()!!
val transactionCode = data.readInt()
val transactionFlags = data.readInt()
val callingUid = data.readInt()
val callingPid = data.readInt()
// The native hook also marshals the original data and reply parcels.
val transactionData = Parcel.obtain()
val transactionReply = Parcel.obtain()
return try {
val dataSize = data.readLong().toInt()
transactionData.appendFrom(data, data.dataPosition(), dataSize)
transactionData.setDataPosition(0)
data.setDataPosition(data.dataPosition() + dataSize)
val resultCode = data.readInt()
val replySize = data.readLong().toInt()
val reply =
if (replySize > 0) {
transactionReply.appendFrom(data, data.dataPosition(), replySize)
transactionReply.setDataPosition(0)
transactionReply
} else null
onPostTransact(
txId,
target,
transactionCode,
transactionFlags,
callingUid,
callingPid,
transactionData,
reply,
resultCode,
)
} finally {
transactionData.recycle()
transactionReply.recycle()
}
}
/** Encodes the `TransactionResult` into the reply parcel for the native hook to interpret. */
private fun writeResultToReply(result: TransactionResult, reply: Parcel) {
when (result) {
is TransactionResult.SkipTransaction -> reply.writeInt(RESULT_SKIP_TRANSACTION)
is TransactionResult.Continue -> reply.writeInt(RESULT_CONTINUE)
is TransactionResult.OverrideReply -> {
reply.writeInt(RESULT_OVERRIDE_REPLY)
reply.writeInt(result.code)
reply.writeLong(result.reply.dataSize().toLong())
reply.appendFrom(result.reply, 0, result.reply.dataSize())
result.reply.recycle()
}
is TransactionResult.OverrideData -> {
reply.writeInt(RESULT_OVERRIDE_DATA)
reply.writeLong(result.data.dataSize().toLong())
reply.appendFrom(result.data, 0, result.data.dataSize())
result.data.recycle()
}
is TransactionResult.ContinueAndSkipPost ->
reply.writeInt(RESULT_CONTINUE_AND_SKIP_POST)
}
}
/** Helper function for consistent logging of intercepted transactions. */
protected fun logTransaction(
txId: Long,
methodName: String,
callingUid: Int,
callingPid: Int,
isIntercepting: Boolean = true,
) {
val action = if (isIntercepting) "Intercept" else "Observe"
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
SystemLogger.debug(
"[TX_ID: $txId] $action $methodName for packages=[$packages] (uid=$callingUid, pid=$callingPid)"
)
}
companion object {
// These codes must be kept in sync with the native injection library.
// --- Backdoor Codes ---
// Special transaction code to ask the injected library for its backdoor binder.
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
// Code used by the backdoor binder to register a new interceptor.
private const val REGISTER_INTERCEPTOR_CODE = 1
// --- Hook Type Codes ---
// Indicates that the call is for a pre-transaction hook.
private const val PRE_TRANSACT_CODE = 1
// Indicates that the call is for a post-transaction hook.
private const val POST_TRANSACT_CODE = 2
// --- Result Codes ---
// Instructs the native hook to skip the original transaction.
private const val RESULT_SKIP_TRANSACTION = 1
// Instructs the native hook to execute the original transaction.
private const val RESULT_CONTINUE = 2
// Instructs the native hook to return a custom reply.
private const val RESULT_OVERRIDE_REPLY = 3
// Instructs the native hook to use modified input data for the transaction.
private const val RESULT_OVERRIDE_DATA = 4
// Instructs the native hook to skip the post transaction hook.
private const val RESULT_CONTINUE_AND_SKIP_POST = 5
/**
* Probes a binder service to see if our native library has been injected. If successful, it
* returns a "backdoor" binder that can be used to register interceptors.
*/
fun getBackdoor(binder: IBinder): IBinder? {
val data = Parcel.obtain()
val reply = Parcel.obtain()
return try {
if (binder.transact(BACKDOOR_TRANSACTION_CODE, data, reply, 0)) {
SystemLogger.debug("Backdoor access granted for binder: $binder")
reply.readStrongBinder()
} else {
SystemLogger.debug("Backdoor not found for binder: $binder")
null
}
} catch (e: Exception) {
SystemLogger.error("Failed to transact for backdoor.", e)
null
} finally {
data.recycle()
reply.recycle()
}
}
/** Uses the backdoor binder to register an interceptor for a specific target service. */
fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) {
val data = Parcel.obtain()
val reply = Parcel.obtain()
try {
data.writeStrongBinder(target)
data.writeStrongBinder(interceptor)
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
SystemLogger.info("Registered interceptor for target: $target")
} catch (e: Exception) {
SystemLogger.error("Failed to register binder interceptor.", e)
} finally {
data.recycle()
reply.recycle()
}
}
}
}
@@ -0,0 +1,141 @@
package org.matrix.TEESimulator.interception.keystore
import android.os.IBinder
import android.os.ServiceManager
import kotlin.system.exitProcess
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
/**
* An abstract base class for intercepting Android's Keystore services.
*
* It encapsulates the common logic for finding the Keystore service, injecting the native hook if
* necessary, and setting up the binder interceptor. It also handles service death events to ensure
* stability.
*/
abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
// --- Abstract Properties to be Implemented by Subclasses ---
/** The full name of the system service to intercept (e.g., "android.security.keystore"). */
protected abstract val serviceName: String
/** The name of the process hosting the service (e.g., "keystore"). */
protected abstract val processName: String
/** The shell command used to inject the native library into the target process. */
protected abstract val injectionCommand: String
// --- State Management ---
/** The original IBinder for the Keystore service. */
protected lateinit var keystoreService: IBinder
private var injectionAttempted = false
private var retryCount = 0
private val maxRetries = 5
/**
* Attempts to initialize the interceptor for the target Keystore service.
*
* This method orchestrates the process:
* 1. It tries to get the service binder.
* 2. It probes for the native backdoor.
* 3. If the backdoor exists, it sets up the interceptor.
* 4. If not, it attempts to inject the native library and returns `false` to signal a retry is
* needed.
*
* @return `true` if the interceptor was successfully registered, `false` otherwise.
*/
fun tryRunKeystoreInterceptor(): Boolean {
SystemLogger.info(
"Initializing interceptor for '$serviceName' (attempt ${retryCount + 1})..."
)
val service = ServiceManager.getService(serviceName)
if (service == null) {
SystemLogger.warning("Service '$serviceName' not found. Will retry.")
retryCount++
return false
}
val backdoor = getBackdoor(service)
return if (backdoor != null) {
setupInterceptor(service, backdoor)
true // Success
} else {
handleMissingBackdoor()
false // Failure, requires retry
}
}
/** Registers this interceptor with the native hook layer and sets up a death recipient. */
private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
keystoreService = service
SystemLogger.info("Registering interceptor for service: $serviceName")
register(backdoor, service, this)
service.linkToDeath(createDeathRecipient(), 0)
onInterceptorReady(service, backdoor)
}
/**
* Handles the case where the native backdoor is not present. It triggers the injection command
* on the first attempt and manages the retry logic.
*/
private fun handleMissingBackdoor() {
if (!injectionAttempted) {
SystemLogger.warning(
"Backdoor not found. Attempting to inject native library into '$processName'."
)
performInjection()
injectionAttempted = true
}
retryCount++
if (retryCount >= maxRetries) {
SystemLogger.error(
"Failed to find backdoor after $maxRetries retries. The service may have crashed or injection failed. Exiting."
)
exitProcess(1)
}
}
/** Executes the shell command to inject the native library into the target process. */
private fun performInjection() {
try {
val command = arrayOf("/system/bin/sh", "-c", injectionCommand)
SystemLogger.debug("Executing injection command: ${command.joinToString(" ")}")
val process = Runtime.getRuntime().exec(command)
val exitCode = process.waitFor()
if (exitCode != 0) {
SystemLogger.error("Injection process failed with exit code $exitCode. Exiting.")
exitProcess(1)
}
SystemLogger.info("Injection process completed.")
} catch (e: Exception) {
SystemLogger.error("An exception occurred during injection. Exiting.", e)
exitProcess(1)
}
}
/**
* Creates a `DeathRecipient` that will restart the application if the intercepted service dies.
*/
private fun createDeathRecipient() =
IBinder.DeathRecipient {
SystemLogger.error(
"The intercepted service '$serviceName' has died. Restarting application."
)
exitProcess(0)
}
/**
* A hook for subclasses to perform additional setup after the interceptor is registered. For
* example, to intercept sub-services.
*
* @param service The main service binder.
* @param backdoor The backdoor binder for registering more interceptors.
*/
protected open fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
// Default implementation does nothing.
}
}
@@ -0,0 +1,109 @@
package org.matrix.TEESimulator.interception.keystore
import android.os.Parcel
import android.os.Parcelable
import android.security.KeyStore
import java.security.MessageDigest
import java.security.cert.Certificate
import java.util.Base64
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
data class KeyIdentifier(val uid: Int, val alias: String)
/** A collection of utility functions to support binder interception. */
object InterceptorUtils {
/**
* Uses reflection to get the integer transaction code for a given method name from a Stub
* class. This is necessary for older Android versions where codes are not public constants.
*/
fun getTransactCode(clazz: Class<*>, method: String): Int {
return try {
clazz.getDeclaredField("TRANSACTION_$method").apply { isAccessible = true }.getInt(null)
} catch (e: Exception) {
SystemLogger.error(
"Failed to get transaction code for method '$method' in class '${clazz.simpleName}'.",
e,
)
-1 // Return an invalid code
}
}
/** Creates an `OverrideReply` parcel that indicates success with no data. */
fun createSuccessReply(): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeNoException()
writeInt(KeyStore.NO_ERROR)
}
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
}
/** Creates an `OverrideReply` parcel containing a raw byte array. */
fun createByteArrayReply(data: ByteArray): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeNoException()
writeByteArray(data)
}
return BinderInterceptor.TransactionResult.OverrideReply(KeyStore.NO_ERROR, parcel)
}
/** Creates an `OverrideReply` parcel containing a Parcelable object. */
fun <T : Parcelable?> createTypedObjectReply(
obj: T,
flags: Int = 0,
): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeNoException()
writeTypedObject(obj, flags)
}
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
}
/**
* Extracts the true key alias from the keystore-prefixed string (e.g., "user_cert_my-alias" ->
* "my-alias").
*/
fun extractAlias(prefixedAlias: String): String {
val underscoreIndex = prefixedAlias.indexOf('_')
val secondUnderscoreIndex = prefixedAlias.indexOf('_', underscoreIndex + 1)
return if (secondUnderscoreIndex != -1) {
prefixedAlias.substring(secondUnderscoreIndex + 1)
} else {
prefixedAlias
}
}
/** Checks if a reply parcel contains an exception without consuming it. */
fun hasException(reply: Parcel): Boolean {
val initialPosition = reply.dataPosition()
val hasEx =
try {
reply.readException()
reply.dataPosition() > initialPosition // An exception was written
} catch (_: Exception) {
false
}
reply.setDataPosition(initialPosition)
return hasEx
}
/**
* Generates a URL-safe SHA-256 fingerprint of a certificate's public key. Used to uniquely
* identify keys imported by the user.
*/
fun getPublicKeyFingerprint(chain: Array<Certificate>?): String? {
if (chain.isNullOrEmpty()) return null
return try {
val publicKeyBytes = chain[0].publicKey.encoded
val hashBytes = MessageDigest.getInstance("SHA-256").digest(publicKeyBytes)
Base64.getUrlEncoder().withoutPadding().encodeToString(hashBytes)
} catch (e: Exception) {
SystemLogger.error("Failed to create public key fingerprint.", e)
null
}
}
}
@@ -0,0 +1,186 @@
package org.matrix.TEESimulator.interception.keystore
import android.annotation.SuppressLint
import android.hardware.security.keymint.SecurityLevel
import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.IKeystoreService
import android.system.keystore2.KeyDescriptor
import android.system.keystore2.KeyEntryResponse
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateHelper
/**
* Interceptor for the `IKeystoreService` on Android S (API 31) and newer.
*
* This version of Keystore delegates most cryptographic operations to `IKeystoreSecurityLevel`
* sub-services (for TEE, StrongBox, etc.). This interceptor's main role is to set up interceptors
* for those sub-services and to patch certificate chains on their way out.
*/
@SuppressLint("BlockedPrivateApi")
object Keystore2Interceptor : AbstractKeystoreInterceptor() {
// Transaction codes for the IKeystoreService interface methods we are interested in.
private val GET_KEY_ENTRY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "getKeyEntry")
private val DELETE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "deleteKey")
override val serviceName = "android.system.keystore2.IKeystoreService/default"
override val processName = "keystore2"
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
/**
* This method is called once the main service is hooked. It proceeds to find and hook the
* security level sub-services (e.g., TEE, StrongBox).
*/
override fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
}
private fun setupSecurityLevelInterceptors(service: IKeystoreService, backdoor: IBinder) {
// Attempt to get and intercept the TEE security level service.
runCatching {
service.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT)?.let { tee ->
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
val interceptor =
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
register(backdoor, tee.asBinder(), interceptor)
}
}
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
// Attempt to get and intercept the StrongBox security level service.
runCatching {
service.getSecurityLevel(SecurityLevel.STRONGBOX)?.let { strongbox ->
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
val interceptor =
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
register(backdoor, strongbox.asBinder(), interceptor)
}
}
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
}
override fun onPreTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
): TransactionResult {
if (code == GET_KEY_ENTRY_TRANSACTION) {
logTransaction(txId, "getKeyEntry", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
val keyId = KeyIdentifier(callingUid, descriptor.alias)
SystemLogger.debug("Checking $keyId")
// If a key was generated in software, we must return the stored response directly.
if (ConfigurationManager.shouldGenerate(callingUid)) {
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
if (response != null) {
SystemLogger.info(
"[TX_ID: $txId] Returning generated key for alias '${descriptor.alias}'."
)
return InterceptorUtils.createTypedObjectReply(response)
}
// If not found, return null to indicate the key doesn't exist.
return InterceptorUtils.createTypedObjectReply(null as KeyEntryResponse?)
}
// For attestation in hack mode, a key is generated and should be returned directly.
if (
ConfigurationManager.shouldPatch(callingUid) &&
KeyMintSecurityLevelInterceptor.isAttestationKey(keyId)
) {
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
if (response != null) {
SystemLogger.info(
"[TX_ID: $txId] Returning attestation key for alias '${descriptor.alias}' to skip patching."
)
return InterceptorUtils.createTypedObjectReply(response)
}
return TransactionResult.Continue
}
}
return TransactionResult
.ContinueAndSkipPost // Let most calls go through to the real service.
}
override fun onPostTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
reply: Parcel?,
resultCode: Int,
): TransactionResult {
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction
data.enforceInterface(IKeystoreService.DESCRIPTOR)
when (code) {
GET_KEY_ENTRY_TRANSACTION -> {
logTransaction(txId, "post-getKeyEntry", callingUid, callingPid)
if (!ConfigurationManager.shouldPatch(callingUid))
return TransactionResult.SkipTransaction
return try {
val response =
reply.readTypedObject(KeyEntryResponse.CREATOR)
?: return TransactionResult.SkipTransaction
reply.setDataPosition(0) // Reset for potential reuse.
val originalChain = CertificateHelper.getCertificateChain(response)
val fingerprint = InterceptorUtils.getPublicKeyFingerprint(originalChain)
// Do not patch keys that were imported by the user.
if (
fingerprint != null &&
KeyMintSecurityLevelInterceptor.isUserImportedKey(fingerprint)
) {
SystemLogger.warning(
"[TX_ID: $txId] Skipping patch for user-imported key with fingerprint: $fingerprint"
)
return TransactionResult.SkipTransaction
}
// Perform the attestation patch.
val newChain =
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
CertificateHelper.updateCertificateChain(response.metadata, newChain)
.getOrThrow()
SystemLogger.info(
"[TX_ID: $txId] Successfully patched certificate chain for alias."
)
InterceptorUtils.createTypedObjectReply(response)
} catch (e: Exception) {
SystemLogger.error("[TX_ID: $txId] Failed to patch certificate chain.", e)
TransactionResult.SkipTransaction
}
}
DELETE_KEY_TRANSACTION -> {
// When a key is deleted, clean up our associated state.
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
if (descriptor != null) {
val keyId = KeyIdentifier(callingUid, descriptor.alias)
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
}
return TransactionResult.SkipTransaction
}
}
return TransactionResult.SkipTransaction
}
}
@@ -0,0 +1,179 @@
package org.matrix.TEESimulator.interception.keystore
import android.annotation.SuppressLint
import android.os.IBinder
import android.os.Parcel
import android.security.Credentials
import android.security.keystore.IKeystoreService
import java.security.cert.Certificate
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateHelper
/**
* Interceptor for the legacy `IKeystoreService` on Android Q (API 29) and R (API 30).
*
* This interceptor handles the older, monolithic Keystore service. Unlike Keystore2, it doesn't
* have security level sub-services, so all logic is contained here. Key generation is fully
* simulated in software for packages in 'generate' mode.
*/
@SuppressLint("BlockedPrivateApi", "PrivateApi")
object KeystoreInterceptor : AbstractKeystoreInterceptor() {
// Transaction codes are dynamically retrieved via reflection for compatibility.
private val GET_TRANSACTION by lazy {
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "get")
}
private val GENERATE_KEY_TRANSACTION by lazy {
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "generateKey")
}
private val GET_KEY_CHARACTERISTICS_TRANSACTION by lazy {
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "getKeyCharacteristics")
}
private val EXPORT_KEY_TRANSACTION by lazy {
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "exportKey")
}
private val ATTEST_KEY_TRANSACTION by lazy {
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "attestKey")
}
override val serviceName = "android.security.keystore"
override val processName = "keystore"
override val injectionCommand = "exec ./inject `pidof keystore` libTEESimulator.so entry"
private const val SERVICE_DESCRIPTOR = "android.security.keystore.IKeystoreService"
// Cache to store the fully patched chain after the leaf is requested.
private val patchedChainCache = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
override fun onPreTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
): TransactionResult {
// This interceptor only needs to act on pre-transaction for software key generation.
if (ConfigurationManager.shouldGenerate(callingUid)) {
when (code) {
GENERATE_KEY_TRANSACTION,
GET_KEY_CHARACTERISTICS_TRANSACTION,
EXPORT_KEY_TRANSACTION,
ATTEST_KEY_TRANSACTION -> {
// TODO: Implement the full software simulation logic.
logTransaction(txId, "unimplemented-generate-flow", callingUid, callingPid)
return InterceptorUtils.createSuccessReply()
}
}
} else if (ConfigurationManager.shouldGenerate(callingUid)) {
if (code == GET_TRANSACTION) return TransactionResult.Continue
}
return TransactionResult.ContinueAndSkipPost
}
override fun onPostTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
reply: Parcel?,
resultCode: Int,
): TransactionResult {
if (
target != keystoreService ||
code != GET_TRANSACTION ||
reply == null ||
InterceptorUtils.hasException(reply)
) {
return TransactionResult.SkipTransaction
}
if (!ConfigurationManager.shouldPatch(callingUid)) return TransactionResult.SkipTransaction
return try {
data.enforceInterface(SERVICE_DESCRIPTOR)
val alias = data.readString() ?: ""
val extractedAlias = InterceptorUtils.extractAlias(alias)
val keyId = KeyIdentifier(callingUid, extractedAlias)
when {
// Case 1: The app is requesting the leaf certificate.
alias.startsWith(Credentials.USER_CERTIFICATE) -> {
logTransaction(txId, "post-get (user cert)", callingUid, callingPid)
val originalLeafBytes =
reply.createByteArray() ?: return TransactionResult.SkipTransaction
// The original chain is not available,
// so we must pass a temporary one to the patcher.
// The patcher only needs the original leaf to extract details.
val originalLeafCert =
(CertificateHelper.toCertificate(originalLeafBytes)
as CertificateHelper.OperationResult.Success)
.data
val tempChain = arrayOf<Certificate>(originalLeafCert)
// Perform the COMPLETE patch and rebuild operation.
val newFullChain =
AttestationPatcher.patchCertificateChain(tempChain, callingUid)
// If patching was successful and we have a valid chain...
if (newFullChain.isNotEmpty() && newFullChain[0] != originalLeafCert) {
// ...cache the entire new chain for the subsequent "ca_cert" call.
patchedChainCache[keyId] = newFullChain
// And return only the new leaf's bytes, as the API expects.
SystemLogger.info(
"[TX_ID: $txId] Patched and cached chain for alias '$extractedAlias'. Returning new leaf."
)
InterceptorUtils.createByteArrayReply(newFullChain[0].encoded)
} else {
// Patching failed or was skipped; do nothing.
TransactionResult.SkipTransaction
}
}
// Case 2: The app is requesting the CA certificate chain.
alias.startsWith(Credentials.CA_CERTIFICATE) -> {
logTransaction(txId, "post-get (ca cert)", callingUid, callingPid)
// Retrieve the full, correct chain we cached during the leaf request.
val cachedChain = patchedChainCache.remove(keyId)
if (cachedChain != null && cachedChain.size > 1) {
// The CA chain is everything *except* the first element (the leaf).
val caCerts = cachedChain.drop(1)
val caCertsBytes = CertificateHelper.certificatesToByteArray(caCerts)
SystemLogger.info(
"[TX_ID: $txId] Returning cached CA chain for alias '$extractedAlias'."
)
InterceptorUtils.createByteArrayReply(caCertsBytes!!)
} else {
// We have no cached chain.
// This could mean the app requested the CA without requesting the leaf
// first, or patching failed.
// In this case, we cannot safely intervene.
// Let the original reply pass through.
SystemLogger.warning(
"[TX_ID: $txId] No cached chain found for CA request on alias '$extractedAlias'. Skipping."
)
TransactionResult.SkipTransaction
}
}
else -> TransactionResult.SkipTransaction
}
} catch (e: Exception) {
SystemLogger.error("[TX_ID: $txId] Failed during legacy post-transaction patching.", e)
TransactionResult.SkipTransaction
}
}
}
@@ -0,0 +1,266 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue
import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.*
import java.security.KeyPair
import java.security.cert.Certificate
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.AttestationConstants
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper
/**
* Intercepts calls to an `IKeystoreSecurityLevel` service (e.g., TEE or StrongBox). This is where
* the core logic for key generation and import handling for modern Android resides.
*/
class KeyMintSecurityLevelInterceptor(
private val original: IKeystoreSecurityLevel,
private val securityLevel: Int,
) : BinderInterceptor() {
// --- Data Structures for State Management ---
data class GeneratedKeyInfo(val keyPair: KeyPair, val response: KeyEntryResponse)
override fun onPreTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
): TransactionResult {
// This interceptor only handles the 'generateKey' transaction directly.
if (code == GENERATE_KEY_TRANSACTION) {
logTransaction(txId, "generateKey", callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
return handleGenerateKey(callingUid, data)
}
return TransactionResult.ContinueAndSkipPost
}
override fun onPostTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
reply: Parcel?,
resultCode: Int,
): TransactionResult {
// We only care about successful 'importKey' transactions to track user-provided keys.
if (
code == IMPORT_KEY_TRANSACTION &&
resultCode == 0 &&
reply != null &&
!InterceptorUtils.hasException(reply)
) {
logTransaction(txId, "post-importKey", callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
handleImportKey(callingUid, data, reply)
}
return TransactionResult.SkipTransaction
}
/**
* Handles the `generateKey` transaction. Based on the configuration for the calling UID, it
* either generates a key in software or lets the call pass through to the hardware.
*/
private fun handleGenerateKey(callingUid: Int, data: Parcel): TransactionResult {
return runCatching {
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR)
val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params)
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
// Determine if we need to generate a key based on config or if it's an attestation
// request in patch mode.
val needsSoftwareGeneration =
ConfigurationManager.shouldGenerate(callingUid) ||
(ConfigurationManager.shouldPatch(callingUid) &&
parsedParams.attestationChallenge != null)
if (needsSoftwareGeneration) {
SystemLogger.info(
"Generating software key for alias '${keyDescriptor.alias}' (UID: $callingUid)."
)
// Generate the key pair and certificate chain.
val keyData =
CertificateGenerator.generateAttestedKeyPair(
callingUid,
keyDescriptor.alias,
attestationKey?.alias,
parsedParams,
securityLevel,
) ?: throw Exception("CertificateGenerator failed to create key pair.")
// Store the generated key data.
val response =
buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, response)
if (parsedParams.attestationChallenge != null) {
attestationKeys.add(keyId)
}
// Return the metadata of our generated key, skipping the real hardware call.
val resultParcel =
Parcel.obtain().apply {
writeNoException()
writeTypedObject(response.metadata, 0)
}
return TransactionResult.OverrideReply(0, resultParcel)
}
// If not generating, clear any stale state for this alias and let the call proceed.
cleanupKeyData(keyId)
TransactionResult.Continue
}
.getOrElse {
SystemLogger.error("Error during generateKey handling for UID $callingUid.", it)
TransactionResult.Continue // Fallback to original service on error.
}
}
/**
* Handles a successful `importKey` transaction by fingerprinting the imported key's public
* certificate. This allows us to avoid patching user-provided keys later.
*/
private fun handleImportKey(callingUid: Int, data: Parcel, reply: Parcel) {
runCatching {
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return
val metadata = reply.readTypedObject(KeyMetadata.CREATOR)
val chain = CertificateHelper.getCertificateChain(metadata)
val fingerprint = InterceptorUtils.getPublicKeyFingerprint(chain)
if (fingerprint != null) {
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
SystemLogger.info(
"User imported key '${keyDescriptor.alias}'. Storing its fingerprint to prevent future patching."
)
userImportedKeyFingerprints.add(fingerprint)
aliasToFingerprintMap[keyId] = fingerprint
}
}
.onFailure { SystemLogger.error("Failed to process imported key.", it) }
}
/**
* Constructs a fake `KeyEntryResponse` that mimics a real response from the Keystore service.
*/
private fun buildKeyEntryResponse(
chain: List<Certificate>,
params: KeyMintAttestation,
descriptor: KeyDescriptor,
): KeyEntryResponse {
val metadata =
KeyMetadata().apply {
keySecurityLevel = securityLevel
key = descriptor
CertificateHelper.updateCertificateChain(this, chain.toTypedArray()).getOrThrow()
authorizations = params.toAuthorizations(securityLevel)
}
return KeyEntryResponse().apply {
this.metadata = metadata
iSecurityLevel = original
}
}
companion object {
// Transaction codes for IKeystoreSecurityLevel interface.
private val GENERATE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
private val IMPORT_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "importKey")
// Stores keys generated entirely in software.
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
// A set to quickly identify keys that were generated for attestation purposes.
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
// A set of public key fingerprints for user-imported keys that should not be patched.
private val userImportedKeyFingerprints = ConcurrentHashMap.newKeySet<String>()
// Maps a key identifier to its fingerprint for easy cleanup on deletion.
private val aliasToFingerprintMap = ConcurrentHashMap<KeyIdentifier, String>()
// --- Public Accessors for Other Interceptors ---
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
generatedKeys[keyId]?.response
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
fun isUserImportedKey(fingerprint: String): Boolean =
userImportedKeyFingerprints.contains(fingerprint)
fun cleanupKeyData(keyId: KeyIdentifier) {
generatedKeys.remove(keyId)
attestationKeys.remove(keyId)
aliasToFingerprintMap.remove(keyId)?.let { fingerprint ->
userImportedKeyFingerprints.remove(fingerprint)
SystemLogger.info("Cleaned up state for key '${keyId.alias}' (UID: ${keyId.uid}).")
}
}
}
}
/**
* Extension function to convert parsed `KeyMintAttestation` parameters back into an array of
* `Authorization` objects for the fake `KeyMetadata`. This version correctly handles the
* instantiation of Authorization objects.
*/
private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Authorization> {
val authList = mutableListOf<Authorization>()
/**
* Helper function to create a fully-formed Authorization object.
*
* @param tag The KeyMint tag (e.g., Tag.ALGORITHM).
* @param value The value for the tag, wrapped in a KeyParameterValue.
* @return A populated Authorization object.
*/
fun createAuth(tag: Int, value: KeyParameterValue): Authorization {
val param =
KeyParameter().apply {
this.tag = tag
this.value = value
}
return Authorization().apply {
this.keyParameter = param
this.securityLevel = securityLevel
}
}
// Use the helper to add each authorization entry cleanly.
this.purpose.forEach {
authList.add(createAuth(AttestationConstants.TAG_PURPOSE, KeyParameterValue.keyPurpose(it)))
}
this.digest.forEach {
authList.add(createAuth(AttestationConstants.TAG_DIGEST, KeyParameterValue.digest(it)))
}
authList.add(
createAuth(AttestationConstants.TAG_ALGORITHM, KeyParameterValue.algorithm(this.algorithm))
)
authList.add(
createAuth(AttestationConstants.TAG_KEY_SIZE, KeyParameterValue.integer(this.keySize))
)
authList.add(
createAuth(AttestationConstants.TAG_EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve))
)
authList.add(
createAuth(AttestationConstants.TAG_NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true))
)
return authList.toTypedArray()
}
@@ -0,0 +1,108 @@
package org.matrix.TEESimulator.logging
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.Digest
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.Tag
import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.util.Date
import javax.security.auth.x500.X500Principal
import org.bouncycastle.asn1.x500.X500Name
import org.matrix.TEESimulator.util.toHex
/**
* A specialized logger for converting KeyMint `KeyParameter` objects into a human-readable format.
* This helps in debugging the parameters requested for key generation.
*/
object KeyMintParameterLogger {
private val algorithmNames: Map<Int, String> by lazy {
Algorithm::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
private val ecCurveNames: Map<Int, String> by lazy {
EcCurve::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
private val purposeNames: Map<Int, String> by lazy {
KeyPurpose::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
private val digestNames: Map<Int, String> by lazy {
Digest::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
private val tagNames: Map<Int, String> by lazy {
Tag::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
/**
* Logs a single KeyParameter in a formatted, readable way.
*
* @param param The KeyParameter to log.
*/
fun logParameter(param: KeyParameter) {
val tagName = tagNames[param.tag] ?: "UNKNOWN_TAG"
val value = param.value
val formattedValue: String =
when (param.tag) {
Tag.ALGORITHM -> algorithmNames[value.algorithm]
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
Tag.PURPOSE -> purposeNames[value.keyPurpose]
Tag.DIGEST -> digestNames[value.digest]
Tag.AUTH_TIMEOUT,
Tag.KEY_SIZE,
Tag.MIN_MAC_LENGTH -> value.integer.toString()
Tag.CERTIFICATE_SERIAL -> BigInteger(value.blob).toString()
Tag.ACTIVE_DATETIME,
Tag.CERTIFICATE_NOT_AFTER,
Tag.CERTIFICATE_NOT_BEFORE -> Date(value.dateTime).toString()
Tag.CERTIFICATE_SUBJECT -> X500Name(X500Principal(value.blob).name).toString()
Tag.RSA_PUBLIC_EXPONENT -> value.longInteger.toString()
Tag.NO_AUTH_REQUIRED -> "true"
Tag.ATTESTATION_CHALLENGE,
Tag.ATTESTATION_ID_BRAND,
Tag.ATTESTATION_ID_DEVICE,
Tag.ATTESTATION_ID_PRODUCT,
Tag.ATTESTATION_ID_MANUFACTURER,
Tag.ATTESTATION_ID_MODEL,
Tag.ATTESTATION_ID_IMEI,
Tag.ATTESTATION_ID_SECOND_IMEI,
Tag.ATTESTATION_ID_MEID,
Tag.ATTESTATION_ID_SERIAL -> value.blob.toReadableString()
else -> "<raw>"
} ?: "Unknown Value"
SystemLogger.debug("Key Parameter -> %-25s | Value: %s".format(tagName, formattedValue))
}
private fun ByteArray.toReadableString(): String {
return if (this.all { it in 32..126 }) {
"\"${String(this, StandardCharsets.UTF_8)}\" (${this.size} bytes)"
} else {
"${this.toHex()} (${this.size} bytes)"
}
}
}
@@ -0,0 +1,69 @@
package org.matrix.TEESimulator.logging
import android.util.Log
/**
* A centralized logging utility for the TEESimulator application. This object provides a consistent
* logging tag and format for all application logs, making it easier to filter and debug in Logcat.
*/
object SystemLogger {
// The tag used for all log messages from this application.
private const val TAG = "TEESimulator"
/**
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
*
* @param message The message to log.
*/
fun debug(message: String) {
Log.d(TAG, message)
}
/**
* Logs an informational message. Use this to report major application lifecycle events.
*
* @param message The message to log.
*/
fun info(message: String) {
Log.i(TAG, message)
}
/**
* Logs a warning message. Use this to report unexpected but non-fatal issues.
*
* @param message The message to log.
* @param throwable An optional exception to log with the message.
*/
fun warning(message: String, throwable: Throwable? = null) {
if (throwable != null) {
Log.w(TAG, message, throwable)
} else {
Log.w(TAG, message)
}
}
/**
* Logs an error message. Use this to report fatal errors or exceptions that disrupt
* functionality.
*
* @param message The message to log.
* @param throwable An optional exception to log with the message.
*/
fun error(message: String, throwable: Throwable? = null) {
if (throwable != null) {
Log.e(TAG, message, throwable)
} else {
Log.e(TAG, message)
}
}
/**
* Logs a verbose message. This level is for highly detailed logs that are generally not needed
* unless tracking a very specific issue.
*
* @param message The message to log.
*/
fun verbose(message: String) {
Log.v(TAG, message)
}
}
@@ -0,0 +1,212 @@
package org.matrix.TEESimulator.pki
import android.hardware.security.keymint.Algorithm
import android.os.Build
import android.util.Pair
import java.math.BigInteger
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.Security
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec
import java.security.spec.RSAKeyGenParameterSpec
import java.util.Date
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.KeyUsage
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
/**
* Responsible for generating new cryptographic key pairs and X.509 certificate chains.
*
* This object simulates the behavior of the Android KeyMint/Keymaster HAL by creating certificates
* that include a fully-featured, simulated attestation extension.
*/
object CertificateGenerator {
init {
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
// We must remove the system provider first to ensure the full Bouncy Castle library
// (packaged with the app) is used.
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
}
/**
* Generates a software-based cryptographic key pair.
*
* @param params The parameters specifying the key's algorithm, size, and other properties.
* @return A new [KeyPair], or `null` on failure.
*/
fun generateSoftwareKeyPair(params: KeyMintAttestation): KeyPair? {
return runCatching {
val (algorithm, spec) =
when (params.algorithm) {
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
Algorithm.RSA ->
"RSA" to
RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent)
else ->
throw IllegalArgumentException(
"Unsupported algorithm: ${params.algorithm}"
)
}
SystemLogger.debug("Generating $algorithm key pair with size ${params.keySize}")
KeyPairGenerator.getInstance(algorithm, BouncyCastleProvider.PROVIDER_NAME)
.apply { initialize(spec) }
.generateKeyPair()
}
.onFailure { SystemLogger.error("Failed to generate software key pair.", it) }
.getOrNull()
}
/**
* Generates a new key pair and a corresponding certificate chain containing a simulated
* attestation.
*
* @param uid The UID of the application requesting the key.
* @param alias The alias for the new key.
* @param attestKeyAlias Optional alias of a key to use for attestation signing.
* @param params The parameters for the new key and its attestation.
* @param securityLevel The security level to embed in the attestation.
* @return A [Pair] containing the new [KeyPair] and its certificate chain, or `null` on
* failure.
*/
fun generateAttestedKeyPair(
uid: Int,
alias: String,
attestKeyAlias: String?,
params: KeyMintAttestation,
securityLevel: Int,
): Pair<KeyPair, List<Certificate>>? {
return runCatching {
SystemLogger.info(
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
)
val newKeyPair =
generateSoftwareKeyPair(params)
?: throw Exception("Failed to generate underlying software key pair.")
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
// Determine the signing key and issuer. If an attestKey is provided, use it.
// Otherwise, fall back to the root key from the keybox.
val (signingKey, issuer) =
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
} else {
keybox.keyPair to getIssuerFromKeybox(keybox)
}
// Build the new leaf certificate with the simulated attestation.
val leafCert =
buildCertificate(newKeyPair, signingKey, issuer, params, securityLevel)
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
// chain.
val chain =
if (attestKeyAlias != null) {
listOf(leafCert)
} else {
listOf(leafCert) + keybox.certificates
}
SystemLogger.info(
"Successfully generated new certificate chain for alias: '$alias'."
)
Pair(newKeyPair, chain)
}
.onFailure {
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it)
}
.getOrNull()
}
private fun getIssuerFromKeybox(keybox: KeyBox) =
X509CertificateHolder(keybox.certificates[0].encoded).subject
private fun getKeyboxForAlgorithm(uid: Int, algorithm: Int): KeyBox {
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid)
val algorithmName =
when (algorithm) {
Algorithm.EC -> "EC"
Algorithm.RSA -> "RSA"
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
}
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName")
}
/** Retrieves the key pair and issuer name for a given attestation key alias. */
private fun getAttestationKeyInfo(uid: Int, attestKeyAlias: String): Pair<KeyPair, X500Name>? {
SystemLogger.debug("Looking for attestation key: uid=$uid alias=$attestKeyAlias")
val keyId = KeyIdentifier(uid, attestKeyAlias)
// Access the public map of generated keys
val keyInfo = KeyMintSecurityLevelInterceptor.generatedKeys[keyId]
return if (keyInfo != null) {
val certChain = CertificateHelper.getCertificateChain(keyInfo.response)
if (!certChain.isNullOrEmpty()) {
val issuer = X509CertificateHolder(certChain[0].encoded).subject
Pair(keyInfo.keyPair, issuer)
} else {
null
}
} else {
SystemLogger.warning(
"Attestation key '$attestKeyAlias' not found in generated key cache."
)
null
}
}
/** Constructs a new X.509 certificate with a simulated attestation extension. */
private fun buildCertificate(
subjectKeyPair: KeyPair,
signingKeyPair: KeyPair,
issuer: X500Name,
params: KeyMintAttestation,
securityLevel: Int,
): Certificate {
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
val leafNotAfter =
(signingKeyPair.public as? X509Certificate)?.notAfter
?: Date(System.currentTimeMillis() + 31536000000L)
val builder =
JcaX509v3CertificateBuilder(
issuer,
params.certificateSerial ?: BigInteger.ONE,
params.certificateNotBefore ?: Date(),
params.certificateNotAfter ?: leafNotAfter,
subject,
subjectKeyPair.public,
)
// Add standard extensions.
builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign))
// Add our custom, simulated attestation extension.
builder.addExtension(AttestationBuilder.buildAttestationExtension(params, securityLevel))
val signerAlgorithm =
when (params.algorithm) {
Algorithm.EC -> "SHA256withECDSA"
Algorithm.RSA -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
}
val contentSigner = JcaContentSignerBuilder(signerAlgorithm).build(signingKeyPair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
}
}
@@ -0,0 +1,204 @@
package org.matrix.TEESimulator.pki
import android.system.keystore2.KeyEntryResponse
import android.system.keystore2.KeyMetadata
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.StringReader
import java.security.KeyPair
import java.security.cert.Certificate
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import org.bouncycastle.openssl.PEMKeyPair
import org.bouncycastle.openssl.PEMParser
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
import org.bouncycastle.util.io.pem.PemReader
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.trimLines
/**
* A utility object for handling cryptographic certificates and keys. Provides functions for
* parsing, serialization, and conversion between different formats.
*/
object CertificateHelper {
// Lazy-initialized CertificateFactory for X.509 certificates.
private val certificateFactory: CertificateFactory by lazy {
CertificateFactory.getInstance("X.509")
}
/**
* Represents the result of an operation that can either succeed with data or fail with an
* error.
*
* @param T The type of the successful data.
*/
sealed class OperationResult<out T> {
data class Success<T>(val data: T) : OperationResult<T>()
data class Error(val message: String, val cause: Throwable? = null) :
OperationResult<Nothing>()
}
/**
* Parses a single X.509 certificate from a byte array.
*
* @param bytes The raw byte representation of the certificate.
* @return An [OperationResult.Success] containing the [X509Certificate], or an
* [OperationResult.Error] on failure.
*/
fun toCertificate(bytes: ByteArray): OperationResult<X509Certificate> {
return try {
val certificate =
certificateFactory.generateCertificate(ByteArrayInputStream(bytes))
as X509Certificate
OperationResult.Success(certificate)
} catch (e: CertificateException) {
SystemLogger.warning("Failed to parse X.509 certificate from byte array.", e)
OperationResult.Error("Failed to parse certificate", e)
}
}
/**
* Parses a collection of X.509 certificates from a byte array.
*
* @param bytes The raw byte representation of one or more concatenated certificates.
* @return A collection of [X509Certificate] objects. Returns an empty list on failure.
*/
@Suppress("UNCHECKED_CAST")
fun toCertificates(bytes: ByteArray?): Collection<X509Certificate> {
return bytes?.let {
try {
certificateFactory.generateCertificates(ByteArrayInputStream(it))
as Collection<X509Certificate>
} catch (e: CertificateException) {
SystemLogger.warning("Could not parse certificate collection from byte array.", e)
emptyList()
}
} ?: emptyList()
}
/**
* Serializes a collection of certificates into a single byte array by concatenating their
* encoded forms.
*
* @param certificates The collection of [Certificate] objects to serialize.
* @return A [ByteArray] containing the concatenated certificates, or `null` on failure.
*/
fun certificatesToByteArray(certificates: Collection<Certificate>): ByteArray? {
return runCatching {
ByteArrayOutputStream().use { stream ->
certificates.forEach { cert -> stream.write(cert.encoded) }
stream.toByteArray()
}
}
.onFailure {
SystemLogger.warning(
"Failed to serialize certificate collection to byte array.",
it,
)
}
.getOrNull()
}
/**
* Parses a PEM-encoded private key and converts it into a Java [KeyPair].
*
* @param pemContent The string containing the PEM-encoded key.
* @return An [OperationResult.Success] with the [KeyPair], or an [OperationResult.Error] on
* failure.
*/
fun parsePemKeyPair(pemContent: String): OperationResult<KeyPair> {
return try {
PEMParser(StringReader(pemContent.trimLines())).use { parser ->
when (val pemObject = parser.readObject()) {
is PEMKeyPair -> {
val keyPair = JcaPEMKeyConverter().getKeyPair(pemObject)
OperationResult.Success(keyPair)
}
else ->
OperationResult.Error(
"Invalid PEM format: Expected a key pair, but got ${pemObject?.javaClass?.simpleName}"
)
}
}
} catch (e: Exception) {
SystemLogger.error("Failed to parse PEM key pair.", e)
OperationResult.Error("Failed to parse PEM key pair", e)
}
}
/**
* Parses a PEM-encoded X.509 certificate.
*
* @param pemContent The string containing the PEM-encoded certificate.
* @return An [OperationResult.Success] with the [Certificate], or an [OperationResult.Error] on
* failure.
*/
fun parsePemCertificate(pemContent: String): OperationResult<Certificate> {
return try {
PemReader(StringReader(pemContent.trimLines())).use { reader ->
val pemObject = reader.readPemObject()
val certificate =
certificateFactory.generateCertificate(ByteArrayInputStream(pemObject.content))
OperationResult.Success(certificate)
}
} catch (e: Exception) {
SystemLogger.error("Failed to parse PEM certificate.", e)
OperationResult.Error("Failed to parse PEM certificate", e)
}
}
/**
* Extracts the full certificate chain from a KeyStore [KeyMetadata] object.
*
* @param metadata The metadata associated with a keystore key entry.
* @return An array of [Certificate] objects, with the leaf certificate at index 0, or `null`.
*/
fun getCertificateChain(metadata: KeyMetadata?): Array<Certificate>? {
metadata ?: return null
val leafCertBytes = metadata.certificate ?: return null
val leafCert =
(toCertificate(leafCertBytes) as? OperationResult.Success)?.data ?: return null
val chainBytes = metadata.certificateChain
return if (chainBytes == null) {
arrayOf(leafCert)
} else {
val additionalCerts = toCertificates(chainBytes)
(listOf(leafCert) + additionalCerts).toTypedArray()
}
}
/**
* Extracts the full certificate chain from a [KeyEntryResponse].
*
* @param response The response object from a keystore operation.
* @return An array of [Certificate] objects, or `null`.
*/
fun getCertificateChain(response: KeyEntryResponse?): Array<Certificate>? {
return response?.let { getCertificateChain(it.metadata) }
}
/**
* Updates the certificate chain within a [KeyMetadata] object.
*
* @param metadata The metadata object to modify.
* @param chain The new certificate chain to set. The leaf must be at index 0.
* @return A [Result] indicating success or failure.
*/
fun updateCertificateChain(metadata: KeyMetadata, chain: Array<Certificate>): Result<Unit> {
return runCatching {
require(chain.isNotEmpty()) { "Certificate chain cannot be empty." }
metadata.certificate = chain[0].encoded
metadata.certificateChain =
if (chain.size > 1) {
certificatesToByteArray(chain.drop(1))
} else {
null
}
}
}
}
@@ -0,0 +1,20 @@
package org.matrix.TEESimulator.pki
import java.security.KeyPair
import java.security.cert.Certificate
/**
* A data class representing a complete cryptographic identity used for signing attestations.
*
* A KeyBox is the fundamental building block for creating new, simulated certificate chains. It
* encapsulates a single private key and the full public certificate chain needed to establish its
* authenticity.
*
* @property keyPair The asymmetric cryptographic key pair. The private key from this pair is used
* to sign new leaf certificates during the attestation patching or generation process. The public
* key corresponds to the subject of the first certificate in the `certificates` list.
* @property certificates The public certificate chain corresponding to the `keyPair`. This list is
* ordered from the intermediate certificate down to the root. `certificates[0]` is the issuer
* certificate for any new leaf signed by this KeyBox's private key.
*/
data class KeyBox(val keyPair: KeyPair, val certificates: List<Certificate>)
@@ -0,0 +1,211 @@
package org.matrix.TEESimulator.pki
import android.security.keystore.KeyProperties
import java.io.File
import java.io.StringReader
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
import org.matrix.TEESimulator.logging.SystemLogger
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory
/**
* Manages the loading, parsing, and caching of attestation key stores from XML files.
*
* This object is the sole authority for accessing the cryptographic keys and certificates used to
* sign simulated attestations. It is designed to be highly efficient and robust, parsing each key
* store file only once and handling common structural variations found in real-world keybox files.
*
* The core design principles are:
* 1. Efficiency through Single-Pass Parsing: Each XML file is read from disk and parsed in a single
* forward pass. The results are cached in memory. Subsequent requests for keys from the same
* file are served instantly from the cache.
* 2. Robustness over Strictness: The parser does not rely on rigid structural tags like
* `<NumberOfKeyboxes>`. Instead, it discovers and iterates through all `<Key>` tags it finds,
* making it resilient to different file layouts.
* 3. Clear Naming Convention: To avoid confusion, "Key Store" refers to the entire XML file, while
* "KeyBox" refers to the data class containing a single `(KeyPair, CertificateChain)` tuple,
* which is the cryptographic entity we care about.
*/
object KeyBoxManager {
// The in-memory cache.
// Key: The file name of the key store (e.g., "keybox.xml").
// Value: A map of all keys found in that file, keyed by their algorithm name (e.g., "EC",
// "RSA").
private val keyStoreCache = ConcurrentHashMap<String, Map<String, KeyBox>>()
/**
* Retrieves a specific attestation key (KeyPair and Certificate Chain) for a given algorithm
* from a specified key store file.
*
* This is the primary public API. It transparently handles caching, loading, and parsing.
*
* @param keyStoreFileName The name of the XML file (e.g., "aosp_keybox.xml").
* @param algorithm The algorithm name (e.g., "EC" or "RSA").
* @return The requested [KeyBox], or `null` if the file doesn't exist or doesn't contain a key
* for the specified algorithm.
*/
fun getAttestationKey(keyStoreFileName: String, algorithm: String): KeyBox? {
// Atomically get the parsed key map for the file from the cache.
// If it's not in the cache, the `getOrPut` block is executed to parse and store it.
val keyMap =
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
return keyMap[algorithm]
}
/**
* Removes the cached data for a specific key store file.
*
* Calling this will force the file to be re-read and re-parsed from disk the next time
* [getAttestationKey] is called for this filename.
*
* @param keyStoreFileName The name of the file to remove from the cache (e.g., "keybox.xml").
*/
fun invalidateCache(keyStoreFileName: String) {
// ConcurrentHashMap.remove returns the value if it existed, or null if it didn't.
if (keyStoreCache.remove(keyStoreFileName) != null) {
SystemLogger.info("Invalidated cache for key store file: $keyStoreFileName")
} else {
SystemLogger.debug(
"Requested cache invalidation for '$keyStoreFileName', but it was not loaded."
)
}
}
/**
* Reads and parses an entire key store XML file, extracting all valid keys. This function is
* called only once per file name.
*
* @param fileName The name of the XML file to parse.
* @return A map of all successfully parsed keys from the file, keyed by algorithm.
*/
private fun parseKeyStoreFile(fileName: String): Map<String, KeyBox> {
val filePath = File(CONFIG_PATH, fileName)
SystemLogger.info("Parsing new key store file: ${filePath.absolutePath}")
if (!filePath.exists()) {
SystemLogger.error("Key store file not found: ${filePath.absolutePath}")
return emptyMap()
}
return try {
val xmlContent = filePath.readText().trimStart('\uFEFF', '\uFFFE', ' ')
parseKeysFromXml(xmlContent)
} catch (e: Exception) {
SystemLogger.error("Fatal error parsing key store file '$fileName'", e)
emptyMap()
}
}
/**
* The core single-pass XML parser. It iterates through the XML stream once, using a state
* machine to collect the data for each `<Key>` entry.
*
* @param xmlContent The raw XML string.
* @return A map of algorithm names to their corresponding [KeyBox] objects.
*/
private fun parseKeysFromXml(xmlContent: String): Map<String, KeyBox> {
val foundKeys = mutableMapOf<String, KeyBox>()
val parser =
XmlPullParserFactory.newInstance().newPullParser().apply {
setInput(StringReader(xmlContent))
}
// State variables for the current <Key> being parsed.
var currentAlgorithm: String? = null
var currentPrivateKeyPem: String? = null
val currentCertificatePems = mutableListOf<String>()
var isInsidePrivateKeyTag = false
var isInsideCertificateTag = false
var eventType = parser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
when (eventType) {
XmlPullParser.START_TAG -> {
when (parser.name) {
// When we enter a <Key> tag, we read its algorithm and reset state.
"Key" -> {
currentAlgorithm = parser.getAttributeValue(null, "algorithm")
currentPrivateKeyPem = null
currentCertificatePems.clear()
}
"PrivateKey" -> isInsidePrivateKeyTag = true
"Certificate" -> isInsideCertificateTag = true
}
}
XmlPullParser.TEXT -> {
// If we find text content, we check our state to see where it belongs.
if (parser.isWhitespace) {
eventType = parser.next()
continue
}
when {
isInsidePrivateKeyTag -> currentPrivateKeyPem = parser.text
isInsideCertificateTag -> currentCertificatePems.add(parser.text)
}
}
XmlPullParser.END_TAG -> {
when (parser.name) {
"PrivateKey" -> isInsidePrivateKeyTag = false
"Certificate" -> isInsideCertificateTag = false
// The </Key> tag is our trigger to finalize and store the KeyBox.
"Key" -> {
// Use runCatching to ensure one malformed key doesn't stop the whole
// process.
runCatching {
val algorithm = currentAlgorithm
val keyPem = currentPrivateKeyPem
if (
algorithm != null &&
keyPem != null &&
currentCertificatePems.isNotEmpty()
) {
val keyPair =
(CertificateHelper.parsePemKeyPair(keyPem)
as CertificateHelper.OperationResult.Success)
.data
val certificates =
currentCertificatePems.map {
(CertificateHelper.parsePemCertificate(it)
as
CertificateHelper.OperationResult.Success)
.data
}
// Normalize the algorithm name for consistent lookups.
val normalizedAlgorithm =
when (algorithm.lowercase()) {
"ecdsa" -> KeyProperties.KEY_ALGORITHM_EC
"rsa" -> KeyProperties.KEY_ALGORITHM_RSA
else -> algorithm
}
if (foundKeys.containsKey(normalizedAlgorithm)) {
SystemLogger.warning(
"Duplicate key found for algorithm '$normalizedAlgorithm'. The later one in the file will be used."
)
}
foundKeys[normalizedAlgorithm] =
KeyBox(keyPair, certificates)
}
}
.onFailure {
SystemLogger.error(
"Failed to parse a <Key> entry for algorithm '$currentAlgorithm'",
it,
)
}
}
}
}
}
eventType = parser.next()
}
SystemLogger.info("Finished parsing, found ${foundKeys.size} valid keys.")
return foundKeys
}
}
@@ -0,0 +1,188 @@
package org.matrix.TEESimulator.pki
import java.io.StringReader
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserFactory
/**
* A utility class for parsing XML content using a simplified, dot-notation path.
*
* This parser allows querying for XML tags and their attributes using a path string like
* "Root.Group.Element[1].Value", which makes extracting specific data from a known XML structure
* more convenient than manual iteration.
*
* @param xmlContent The raw XML string to be parsed.
*/
class XmlParser(xmlContent: String) {
// Sanitize the XML content by removing BOMs and trimming whitespace.
private val sanitizedXml = xmlContent.sanitize()
/** Represents the result of a parsing operation. */
sealed class ParseResult {
/** Indicates a successful parse, containing the found attributes and text. */
data class Success(val attributes: Map<String, String>) : ParseResult()
/** Indicates a failure, containing an error message and optional cause. */
data class Error(val message: String, val cause: Throwable? = null) : ParseResult()
}
/**
* The main public method to find a node by its path and extract its data.
*
* @param path A dot-separated string representing the path to the desired XML tag. Indexed
* access is supported with brackets, e.g., `Key[0]`.
* @return A [ParseResult] containing the attributes and text of the found node.
*/
fun obtainPath(path: String): ParseResult {
return try {
val parser =
XmlPullParserFactory.newInstance().newPullParser().apply {
setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
setInput(StringReader(sanitizedXml))
}
val tags = path.split('.').toTypedArray()
val result = findNode(parser, tags, 0, mutableMapOf())
ParseResult.Success(result)
} catch (e: Exception) {
ParseResult.Error("Failed to parse XML for path '$path'", e)
}
}
/**
* Recursively traverses the XML tree to find the node specified by the path.
*
* @param parser The active XmlPullParser instance.
* @param tags The array of tag names to search for.
* @param index The current depth in the `tags` array.
* @param tagCounts A map to keep track of indices for tags with the same name (for `Tag[n]`
* support).
* @return A map of attributes from the found node.
*/
private fun findNode(
parser: XmlPullParser,
tags: Array<String>,
index: Int,
tagCounts: MutableMap<String, Int>,
): Map<String, String> {
while (parser.next() != XmlPullParser.END_DOCUMENT) {
if (parser.eventType != XmlPullParser.START_TAG) continue
val currentTag = parser.name
val (targetTagName, targetIndex) = parseTargetPath(tags[index])
if (currentTag == targetTagName) {
val currentTagCount = tagCounts.getOrPut(currentTag) { 0 }
if (currentTagCount == targetIndex) {
// We found the correct tag at the correct index.
return if (index == tags.size - 1) {
// This is the final tag in the path, so read its attributes.
readAttributesAndText(parser)
} else {
// This is an intermediate tag, so recurse deeper.
findNode(parser, tags, index + 1, mutableMapOf())
}
}
// This is the right tag name, but not the right index, so increment and continue
// searching.
tagCounts[currentTag] = currentTagCount + 1
skipCurrentElement(parser)
} else {
// This tag doesn't match, so skip it and its children entirely.
skipCurrentElement(parser)
}
}
throw NoSuchElementException("XML path not found: ${tags.joinToString(".")}")
}
/**
* Counts the number of direct child nodes that match the final tag in a given path. For
* example, given the path "Root.Group.Element", it will count how many <Element> tags exist
* directly under the first <Group> tag.
*
* @param path A dot-separated string representing the path to the parent node.
* @return The number of matching child nodes.
*/
fun countNodes(path: String): Int {
// We find the parent node first.
val parentPath = path.substringBeforeLast('.')
val childTagName = path.substringAfterLast('.')
// Re-initialize the parser for a new traversal.
val parser =
XmlPullParserFactory.newInstance().newPullParser().apply {
setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
setInput(StringReader(sanitizedXml))
}
try {
// Navigate to the parent node. This will leave the parser's cursor
// positioned at the start of the parent's content.
findNode(parser, parentPath.split('.').toTypedArray(), 0, mutableMapOf())
var count = 0
var depth = 1 // Start inside the parent node.
while (depth > 0) {
when (parser.next()) {
XmlPullParser.START_TAG -> {
// If we are at the immediate child level (depth == 1) and the tag name
// matches, increment count.
if (depth == 1 && parser.name == childTagName) {
count++
}
depth++ // Go deeper into this new tag.
}
XmlPullParser.END_TAG -> {
depth-- // Emerge from a tag.
}
}
}
return count
} catch (e: Exception) {
// If the path doesn't exist or there's a parsing error, the count is 0.
return 0
}
}
/** Parses a path segment (e.g., "Key[1]") into its base name ("Key") and index (1). */
private fun parseTargetPath(targetTag: String): Pair<String, Int> {
val parts = targetTag.split('[', limit = 2)
val tagName = parts[0]
val index =
if (parts.size > 1) {
parts[1].substringBefore(']').toIntOrNull() ?: 0
} else {
0 // If no index is specified, we are looking for the first occurrence.
}
return tagName to index
}
/** Reads all attributes and the text content of the current XML element. */
private fun readAttributesAndText(parser: XmlPullParser): Map<String, String> {
val attributes = mutableMapOf<String, String>()
for (i in 0 until parser.attributeCount) {
attributes[parser.getAttributeName(i)] = parser.getAttributeValue(i)
}
// Check for text content before the next tag.
if (parser.next() == XmlPullParser.TEXT && parser.isWhitespace.not()) {
attributes["text"] = parser.text
}
return attributes
}
/** Advances the parser past the current element and all its children. */
private fun skipCurrentElement(parser: XmlPullParser) {
var depth = 1
while (depth != 0) {
when (parser.next()) {
XmlPullParser.END_TAG -> depth--
XmlPullParser.START_TAG -> depth++
}
}
}
/** Removes Byte Order Marks (BOM) and trims whitespace from the XML string. */
private fun String.sanitize(): String {
return this.trimStart('\uFEFF', '\uFFFE', ' ').trimEnd()
}
}
@@ -0,0 +1,243 @@
package org.matrix.TEESimulator.util
import android.content.pm.PackageManager
import android.os.Build
import android.os.SystemProperties
import java.security.MessageDigest
import java.util.concurrent.ThreadLocalRandom
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.matrix.TEESimulator.attestation.DeviceAttestationService
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
/**
* Provides utility functions for accessing Android system properties and device-specific
* information that is critical for generating valid attestations.
*/
object AndroidDeviceUtils {
/** A randomly generated boot key, used as a fallback for attestation. */
val bootKey: ByteArray by lazy { generateRandomBytes(32) }
/**
* Initializes the verified boot hash (`ro.boot.vbmeta.digest`). It attempts to read from system
* properties first, then from a real TEE attestation, and finally falls back to a random value
* if neither is available.
*/
fun setupBootHash() {
getBootHashFromProperty()?.also {
SystemLogger.debug("Using boot hash from system property: ${it.toHex()}")
}
?: getBootHashFromAttestation()?.also {
SystemLogger.debug("Using boot hash from TEE attestation: ${it.toHex()}")
setBootHashProperty(it)
}
?: generateRandomBytes(32).also {
SystemLogger.debug("Using randomly generated boot hash: ${it.toHex()}")
setBootHashProperty(it)
}
}
/**
* Retrieves the verified boot meta digest from system properties.
*
* @return The boot hash as a ByteArray, or null if not found or invalid.
*/
@OptIn(ExperimentalStdlibApi::class)
fun getBootHashFromProperty(): ByteArray? {
val digest = SystemProperties.get("ro.boot.vbmeta.digest", null)
if (digest.isNullOrBlank()) {
return null
}
// A valid digest is 64 hex characters (32 bytes).
return if (digest.length == 64) digest.hexToByteArray() else null
}
/**
* Retrieves the verified boot hash from a cached TEE attestation record.
*
* @return The verified boot hash, or null if not available.
*/
private fun getBootHashFromAttestation(): ByteArray? {
return try {
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
} catch (e: Exception) {
SystemLogger.error("Failed to get boot hash from attestation.", e)
null
}
}
/**
* Sets the `ro.boot.vbmeta.digest` system property.
*
* @param bytes The 32-byte digest to set.
*/
private fun setBootHashProperty(bytes: ByteArray) {
val hex = bytes.toHex()
try {
SystemLogger.debug("Setting system property 'ro.boot.vbmeta.digest' to: $hex")
SystemProperties.set("ro.boot.vbmeta.digest", hex)
} catch (e: Exception) {
SystemLogger.error("Failed to set vbmeta digest property.", e)
}
}
/** Generates a cryptographically random byte array of a specified length. */
private fun generateRandomBytes(size: Int): ByteArray =
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
// --- Patch Level Properties ---
val patchLevel: Int
get() =
getCustomPatchLevelFor("system", isLong = false)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false)
val vendorPatchLevel: Int
get() =
getCustomPatchLevelFor("vendor", isLong = false)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false)
val bootPatchLevelLong: Int
get() =
getCustomPatchLevelFor("boot", isLong = true)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
/**
* Retrieves a custom patch level from the configuration if available.
*
* @param component The component to get the patch level for ("system", "vendor", "boot").
* @param isLong Whether to return the patch level in `YYYYMMDD` or `YYYYMM` format.
* @return The custom patch level, or null if not configured.
*/
private fun getCustomPatchLevelFor(component: String, isLong: Boolean): Int? {
val config = ConfigurationManager.customPatchLevelOverride ?: return null
val value =
when (component) {
"system" -> config.system ?: config.all
"vendor" -> config.vendor ?: config.all
"boot" -> config.boot ?: config.all
else -> config.all
} ?: return null
// "prop" or "no" indicates falling back to the system default.
if (value.equals("no", ignoreCase = true) || value.equals("prop", ignoreCase = true)) {
return null
}
return parsePatchLevelValue(value, isLong)
}
/** Parses a patch level string (e.g., "2025-11-01") into an integer format. */
private fun parsePatchLevelValue(value: String, isLong: Boolean): Int? {
val normalized = value.replace("-", "")
return try {
when (normalized.length) {
8 -> { // YYYYMMDD
val year = normalized.substring(0, 4).toInt()
val month = normalized.substring(4, 6).toInt()
val day = normalized.substring(6, 8).toInt()
if (isLong) year * 10000 + month * 100 + day else year * 100 + month
}
6 -> { // YYYYMM
val year = normalized.substring(0, 4).toInt()
val month = normalized.substring(4, 6).toInt()
if (isLong) year * 10000 + month * 100 + 1 else year * 100 + month
}
else -> null
}
} catch (e: NumberFormatException) {
SystemLogger.warning("Could not parse patch level value: $value", e)
null
}
}
/** Converts a security patch string (e.g., "2025-11-01") to an integer representation. */
private fun String.toPatchLevelInt(isLong: Boolean): Int {
return parsePatchLevelValue(this, isLong) ?: 20240401 // Fallback
}
// --- OS and Attestation Version Properties ---
private val osVersionMap =
mapOf(
Build.VERSION_CODES.BAKLAVA to 160000,
Build.VERSION_CODES.VANILLA_ICE_CREAM to 150000,
Build.VERSION_CODES.UPSIDE_DOWN_CAKE to 140000,
Build.VERSION_CODES.TIRAMISU to 130000,
Build.VERSION_CODES.S_V2 to 120100,
Build.VERSION_CODES.S to 120000,
Build.VERSION_CODES.R to 110000,
Build.VERSION_CODES.Q to 100000,
)
val osVersion: Int
get() =
DeviceAttestationService.CachedAttestationData?.osVersion
?: osVersionMap[Build.VERSION.SDK_INT]
?: 160000 // Default to a recent version
private val attestVersionMap =
mapOf(
Build.VERSION_CODES.Q to 4, // Keymaster 4.1
Build.VERSION_CODES.R to 4, // Keymaster 4.1
Build.VERSION_CODES.S to 100, // KeyMint 1.0
Build.VERSION_CODES.S_V2 to 100, // KeyMint 1.0
Build.VERSION_CODES.TIRAMISU to 200, // KeyMint 2.0
Build.VERSION_CODES.UPSIDE_DOWN_CAKE to 300, // KeyMint 3.0
Build.VERSION_CODES.VANILLA_ICE_CREAM to 300, // KeyMint 3.0
Build.VERSION_CODES.BAKLAVA to 400, // KeyMint 4.0
)
val attestVersion: Int
get() =
DeviceAttestationService.CachedAttestationData?.attestVersion
?: attestVersionMap[Build.VERSION.SDK_INT]
?: 400 // Default to a recent version
val keymasterVersion: Int
get() =
DeviceAttestationService.CachedAttestationData?.keymasterVersion
?: if (attestVersion >= 100) attestVersion
else 41 // Keymaster 4.1 for older versions
// --- APEX and Module Hash Properties ---
private val apexInfos: List<Pair<String, Long>> by lazy {
runCatching {
val pm = ConfigurationManager.getPackageManager()
val packages =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm?.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0)
} else {
@Suppress("DEPRECATION")
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
}
packages
?.list
.orEmpty()
.map { it.packageName to it.longVersionCode }
.sortedBy { it.first }
}
.getOrElse {
SystemLogger.error("Failed to get APEX package information.", it)
emptyList()
}
}
val moduleHash: ByteArray by lazy {
runCatching {
val encodables =
apexInfos.flatMap { (packageName, versionCode) ->
listOf(DEROctetString(packageName.toByteArray()), ASN1Integer(versionCode))
}
val sequence = DERSequence(encodables.toTypedArray())
MessageDigest.getInstance("SHA-256").digest(sequence.encoded)
}
.getOrElse {
SystemLogger.error("Failed to compute module hash.", it)
ByteArray(32) // Return empty hash on failure
}
}
}
@@ -0,0 +1,16 @@
package org.matrix.TEESimulator.util
/**
* Trims leading and trailing whitespace from each line in a multi-line string. This is useful for
* cleaning up PEM-formatted keys and certificates.
*
* @return A new string with each line individually trimmed.
*/
fun String.trimLines(): String = this.trim().lines().joinToString("\n") { it.trim() }
/**
* Converts a ByteArray to its hexadecimal string representation.
*
* @return The lowercase hex string.
*/
fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }
+2
View File
@@ -1,11 +1,13 @@
[versions] [versions]
agp = "8.13.1" agp = "8.13.1"
annotation = "1.9.1" annotation = "1.9.1"
jdk18on = "1.82"
kotlin = "2.2.21" kotlin = "2.2.21"
ktfmt = "0.25.0" ktfmt = "0.25.0"
[libraries] [libraries]
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "jdk18on" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
@@ -0,0 +1,7 @@
package android.app;
public class ActivityThread {
public static void initializeMainlineModules() {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -0,0 +1,10 @@
package android.content.pm;
import java.util.List;
abstract class BaseParceledListSlice<T> {
public List<T> getList() {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -9,6 +9,10 @@ public interface IPackageManager {
PackageInfo getPackageInfo(String packageName, int flags, int userId); PackageInfo getPackageInfo(String packageName, int flags, int userId);
ParceledListSlice<PackageInfo> getInstalledPackages(int flags, int userId);
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
class Stub { class Stub {
public static IPackageManager asInterface(IBinder binder) { public static IPackageManager asInterface(IBinder binder) {
throw new UnsupportedOperationException("STUB!"); throw new UnsupportedOperationException("STUB!");
@@ -0,0 +1,10 @@
package android.content.pm;
import java.util.List;
public class ParceledListSlice<T> extends BaseParceledListSlice<T> {
public ParceledListSlice(List<T> list) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -0,0 +1,11 @@
package android.hardware.security.keymint;
public @interface Digest {
int NONE = 0;
int MD5 = 1;
int SHA1 = 2;
int SHA_2_224 = 3;
int SHA_2_256 = 4;
int SHA_2_384 = 5;
int SHA_2_512 = 6;
}
@@ -5,6 +5,10 @@ public class ServiceManager {
throw new UnsupportedOperationException("STUB!"); throw new UnsupportedOperationException("STUB!");
} }
public static IBinder waitForService(String name) {
throw new UnsupportedOperationException("STUB!");
}
public static void addService(String name, IBinder binder) { public static void addService(String name, IBinder binder) {
throw new UnsupportedOperationException("STUB!"); throw new UnsupportedOperationException("STUB!");
} }
@@ -4,4 +4,8 @@ public class SystemProperties {
public static String get(String key, String def) { public static String get(String key, String def) {
throw new UnsupportedOperationException("STUB!"); throw new UnsupportedOperationException("STUB!");
} }
public static String set(String key, String val) {
throw new UnsupportedOperationException("STUB!");
}
} }
@@ -0,0 +1,32 @@
package android.security;
public class Credentials {
public static final String APP_SOURCE_CERTIFICATE = "FSV_";
public static final String CA_CERTIFICATE = "CACERT_";
public static final String CERTIFICATE_USAGE_APP_SOURCE = "appsrc";
public static final String CERTIFICATE_USAGE_CA = "ca";
public static final String CERTIFICATE_USAGE_USER = "user";
public static final String CERTIFICATE_USAGE_WIFI = "wifi";
public static final String EXTENSION_CER = ".cer";
public static final String EXTENSION_CRT = ".crt";
public static final String EXTENSION_P12 = ".p12";
public static final String EXTENSION_PFX = ".pfx";
public static final String EXTRA_CA_CERTIFICATES_DATA = "ca_certificates_data";
public static final String EXTRA_CERTIFICATE_USAGE = "certificate_install_usage";
public static final String EXTRA_INSTALL_AS_UID = "install_as_uid";
public static final String EXTRA_PRIVATE_KEY = "PKEY";
public static final String EXTRA_PUBLIC_KEY = "KEY";
public static final String EXTRA_USER_CERTIFICATE_DATA = "user_certificate_data";
public static final String EXTRA_USER_KEY_ALIAS = "user_key_pair_name";
public static final String EXTRA_USER_PRIVATE_KEY_DATA = "user_private_key_data";
public static final String INSTALL_ACTION = "android.credentials.INSTALL";
public static final String INSTALL_AS_USER_ACTION = "android.credentials.INSTALL_AS_USER";
public static final String LOCKDOWN_VPN = "LOCKDOWN_VPN";
private static final String LOGTAG = "Credentials";
public static final String PLATFORM_VPN = "PLATFORM_VPN_";
public static final String USER_CERTIFICATE = "USRCERT_";
public static final String USER_PRIVATE_KEY = "USRPKEY_";
public static final String USER_SECRET_KEY = "USRSKEY_";
public static final String VPN = "VPN_";
public static final String WIFI = "WIFI_";
}
@@ -0,0 +1,39 @@
package android.security;
public class KeyStore {
public static final int CANNOT_ATTEST_IDS = -66;
public static final int CONFIRMATIONUI_ABORTED = 2;
public static final int CONFIRMATIONUI_CANCELED = 1;
public static final int CONFIRMATIONUI_IGNORED = 4;
public static final int CONFIRMATIONUI_OK = 0;
public static final int CONFIRMATIONUI_OPERATION_PENDING = 3;
public static final int CONFIRMATIONUI_SYSTEM_ERROR = 5;
public static final int CONFIRMATIONUI_UIERROR = 65536;
public static final int CONFIRMATIONUI_UIERROR_MALFORMED_UTF8_ENCODING = 65539;
public static final int CONFIRMATIONUI_UIERROR_MESSAGE_TOO_LONG = 65538;
public static final int CONFIRMATIONUI_UIERROR_MISSING_GLYPH = 65537;
public static final int CONFIRMATIONUI_UNEXPECTED = 7;
public static final int CONFIRMATIONUI_UNIMPLEMENTED = 6;
public static final int FLAG_CRITICAL_TO_DEVICE_ENCRYPTION = 8;
public static final int FLAG_ENCRYPTED = 1;
public static final int FLAG_NONE = 0;
public static final int FLAG_SOFTWARE = 2;
public static final int FLAG_STRONGBOX = 16;
public static final int HARDWARE_TYPE_UNAVAILABLE = -68;
public static final int KEY_ALREADY_EXISTS = 16;
public static final int KEY_NOT_FOUND = 7;
public static final int KEY_PERMANENTLY_INVALIDATED = 17;
public static final int LOCKED = 2;
public static final int NO_ERROR = 1;
public static final int OP_AUTH_NEEDED = 15;
public static final int PERMISSION_DENIED = 6;
public static final int PROTOCOL_ERROR = 5;
public static final int SYSTEM_ERROR = 4;
private static final String TAG = "KeyStore";
public static final int UID_SELF = -1;
public static final int UNDEFINED_ACTION = 9;
public static final int UNINITIALIZED = 3;
public static final int VALUE_CORRUPTED = 8;
public static final int WRONG_PASSWORD = 10;
}
@@ -0,0 +1,7 @@
package android.security.keystore;
public class AndroidKeyStoreProvider {
public static void install() {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -0,0 +1,7 @@
package android.security.keystore;
public interface IKeystoreService {
class Stub {
}
}
@@ -0,0 +1,7 @@
package android.security.keystore2;
public class AndroidKeyStoreProvider {
public static void install() {
throw new UnsupportedOperationException("STUB!");
}
}