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:
@@ -67,6 +67,7 @@ android {
|
||||
dependencies {
|
||||
compileOnly(project(":stub"))
|
||||
compileOnly(libs.annotation)
|
||||
implementation(libs.bcpkix)
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
|
||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
-keepclasseswithmembers class org.matrix.TEESimulator.MainKt {
|
||||
-keepclasseswithmembers class org.matrix.TEESimulator.App {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+141
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+186
@@ -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
|
||||
}
|
||||
}
|
||||
+179
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+266
@@ -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) }
|
||||
@@ -1,11 +1,13 @@
|
||||
[versions]
|
||||
agp = "8.13.1"
|
||||
annotation = "1.9.1"
|
||||
jdk18on = "1.82"
|
||||
kotlin = "2.2.21"
|
||||
ktfmt = "0.25.0"
|
||||
|
||||
[libraries]
|
||||
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
|
||||
bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "jdk18on" }
|
||||
|
||||
[plugins]
|
||||
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);
|
||||
|
||||
ParceledListSlice<PackageInfo> getInstalledPackages(int flags, int userId);
|
||||
|
||||
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
|
||||
|
||||
class Stub {
|
||||
public static IPackageManager asInterface(IBinder binder) {
|
||||
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!");
|
||||
}
|
||||
|
||||
public static IBinder waitForService(String name) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
public static void addService(String name, IBinder binder) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
@@ -4,4 +4,8 @@ public class SystemProperties {
|
||||
public static String get(String key, String def) {
|
||||
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!");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user