Compare commits

...
18 Commits
Author SHA1 Message Date
JingMatrix 2a76b18308 Release TEESimulator 3.0 2025-12-06 16:59:28 +01:00
JingMatrixandGitHub d9e47712f3 Correct crypto provider handling and signing logic (#53)
Resolves crashes during certificate operations caused by cryptographic provider conflicts and incorrect algorithm selection.

The Bouncy Castle (BC) provider is now initialized globally at app startup to ensure it is the default. To eliminate ambiguity, all content signers are also now explicitly set to use the BC provider.

The attestation patcher is fixed to correctly use the certificate's signature algorithm (sigAlgName), not the subject's public key algorithm, to select the appropriate signing key from the KeyBoxManager. A normalization function was added to support this.

Moreover, we also modify the XML parser in `KeyBoxManager` to no longer trust the `algorithm` attribute from the XML tag. The parser now determines the key's true algorithm (RSA or EC) by inspecting the type of the parsed private key object. This derived algorithm is used as the key for the cache, preventing cache corruption from malformed files where the tag does not match the key data.
2025-12-06 16:12:12 +01:00
JingMatrixandGitHub d846de4332 Handle invalid verified boot key (#55)
Treat the `verifiedBootKey` as null if it consists entirely of zero bytes, as some devices return this invalid value.

Additionally, this commit adds missing KDoc comments to the `AttestationData` class for better documentation.
2025-12-06 11:49:46 +01:00
JingMatrixandGitHub 13d89c4314 Add dynamic dates and TEE-based patch defaults (#52)
Implements dynamic date keywords ('today') and templates ('YYYY-MM-DD') in the security_patch.txt configuration. This allows for auto-updating patch levels.

The `device_default` keyword is now significantly more accurate. It prioritizes reading real patch levels directly from a cached TEE attestation before falling back to system properties.

The README has been updated to document these new features.
2025-12-06 07:27:06 +01:00
JingMatrixandGitHub 00c91adfaa Implement per-package security patch configuration (#49)
This commit introduces a hierarchical configuration system for the security patch levels reported in attestations, allowing for both global defaults and per-package overrides.

The `security_patch.txt` file is enhanced to support this new syntax. Settings at the top of the file act as a global default, which can be overridden for specific applications by defining settings under a `[package.name]` section.
2025-12-04 23:14:51 +01:00
小潼andGitHub 119350f24b Correctly handle deleteKey for software keys (#42)
This resolves an issue introduced in 733e64c where a `deleteKey` transaction for a software-generated key was incorrectly passed through to the hardware keystore. Since the hardware is unaware of such keys, this results in inconsistent state management.

The success reply is formatted correctly without a result code, per the AIDL interface specification.

Reference: https://cs.android.com/android/platform/superproject/main/+/main:out/soong/.intermediates/system/hardware/interfaces/keystore2/aidl/android.system.keystore2-V6-java-source/gen/android/system/keystore2/IKeystoreSecurityLevel.java;l=406
2025-12-04 20:01:16 +01:00
JingMatrix 7d4c753d66 Bypass KeyMint hooks for certain UIDs
Adds a check using `ConfigurationManager.shouldSkipUid` at the start of the `onPreTransact` handlers for key generation and import.

If a UID is configured to be skipped, the transaction is forwarded directly to the hardware, and the post-transaction hook is bypassed. This prevents certificate patching and other modifications for trusted or problematic apps, improving compatibility.
2025-12-04 02:02:28 +01:00
JingMatrix b988d04971 Set correct attestation version for StrongBox
We observe that attestations generated with a security level of
`StrongBox` (value 2) must have an `attestationVersion` of 300. The
previous implementation determined this version based only on the
Android SDK version, which could lead to invalid attestations.

This commit refactors the version retrieval logic to be dependent on the
security level:

- In `AndroidDeviceUtils`, the `attestVersion` and `keymasterVersion`
  properties have been converted into `getAttestVersion(securityLevel)`
  and `getKeymasterVersion(securityLevel)` functions.
- `getAttestVersion` now correctly returns `300` when the security level
  is `StrongBox`.
- `AttestationBuilder` is updated to call these new functions, passing
  the appropriate security level to ensure the generated attestation is
  compliant with official documentation.
2025-12-04 01:57:59 +01:00
JingMatrixandGitHub d0cc5e3b56 Prevent detection via inconsistent certificate signatures (#45)
Fixes a detection vector where the simulator could be identified by comparing certificate signatures from different API calls.

Previously, the simulator would re-patch and re-sign a certificate on-the-fly for both `generateKey` and `getKeyEntry` calls. Due to the non-deterministic nature of ECDSA signing, this resulted in different signatures for the same certificate, which is a detectable anomaly not present in a real TEE.

This is resolved by caching the patched certificate chain after its initial creation in `KeyMintSecurityLevelInterceptor`. The `getKeyEntry` hook in `Keystore2Interceptor` now retrieves the chain from this cache, guaranteeing that subsequent calls return a byte-for-byte identical certificate.

Cache cleanup logic was also integrated into key deletion and clearing functions to maintain state consistency.
2025-12-04 00:59:59 +01:00
e66e558ce5 Reduce logging in the release build (#44)
Verbose logging are now disabled in the release build.
With this change, we reinterpret the last argument passed to `logTransaction` as `skipPost`, and classify logs satisfying `skipPost` or `shouldSkipUid` as verbose.

Co-authored-by: JingMatrix <jingmatrix@gmail.com>
2025-12-03 23:50:12 +01:00
JingMatrix 8d431cc946 Implement software key generation for legacy IKeystoreService (#34)
This commit introduces a complete, software-based simulation of the key generation and attestation flow for the legacy IKeystoreService API, as used on Android 11. It refactors the KeystoreInterceptor to handle the entire multi-step transaction sequence (`generateKey`, `getKeyCharacteristics`, `exportKey`, `attestKey`) in software.

A new `LegacyKeygenParameters` data class is introduced to decouple the legacy interception logic from modern data structures. This class parses arguments from the old `KeymasterArguments`, stores the state across the multi-step generation process, and acts as an adapter to the generic `CertificateGenerator` by converting the parameters to the modern `KeyMintAttestation` format.

The `CertificateGenerator` has been refactored to better model the behavior of the legacy Keystore API. Key pair generation (`generateSoftwareKeyPair`) and certificate chain creation (`generateCertificateChain`) are now separate functions. This allows the interceptor to correctly create a key pair during the `handleExportKey` step and then generate a certificate for that pre-existing key pair during the `handleAttestKey` step.

Finally, the implementation correctly extracts and applies the `attestationChallenge` provided during the `attestKey` transaction, ensuring the generated certificate chain contains the appropriate attestation.
2025-12-03 19:29:21 +01:00
JingMatrix 30746892b0 Increase Gradle JVM memory in build workflow
The CI build was failing with a "JVM garbage collector is thrashing" error due to insufficient memory. This commit increases the Gradle max heap size to 2GB in the GitHub Actions workflow to resolve the build failure.
2025-12-03 19:22:39 +01:00
JingMatrixandGitHub 28cfe70a85 Fix value and location of moduleHash (#35)
`moduleHash` should be in the software enforced list.
However, the manual calculation of the KeyMint `moduleHash` has
failed to produce a value matching the hardware-generated attestation.

The official documentation specifies the following structure:
  Modules ::= SET OF Module
  Module ::= SEQUENCE {
      packageName       OCTET_STRING,
      version                    INTEGER,
  }
The critical requirement is that the `SET OF` elements must be sorted
lexicographically based on their full DER-encoded byte value. Despite
implementing this using Bouncy Castle's `DERSet`, the resulting hash
is still incorrect.

This commit changes the strategy to favor stability:
1.  The `DeviceAttestationService` now extracts the real `moduleHash`
    from the `softwareEnforced` list of a genuine attestation certificate
    and caches it.
2.  The `moduleHash` property now returns this cached value if available.
3.  The manual calculation remains as a fallback and is marked with a
    `TODO` to indicate the issue is unresolved.

Additionally, `ConfigurationManager` initialization is moved earlier.
2025-11-30 00:13:14 +01:00
JingMatrix 65a613ae0e Properly source and use verifiedBootKey
The previous implementation used a randomly generated value for the `verifiedBootKey` within the simulated attestation's Root of Trust. This is a significant discrepancy from a genuine attestation and represents a clear detection vector for any verification service that inspects the full certificate chain.

This commit introduces a robust, multi-layered approach to source and manage both the `verifiedBootKey` and the `verifiedBootHash`, ensuring the simulated attestation is as authentic as possible.
2025-11-29 19:58:02 +01:00
JingMatrix 9146b86648 Preserve extension order and prevent duplicates
This commit refactors the attestation patching logic to improve stealth and ensure correctness by addressing potential detection vectors related to the ASN.1 structure of the certificate extension.

1. Preserve Extension Order: The original implementation rebuilt the entire certificate, which could alter the order of X.509 extensions. Some verification systems may be sensitive to this order. The logic is now updated to replace the attestation extension in-place, preserving the original order of all other extensions.

2. Avoid Duplicate Properties: The previous logic used an `ASN1EncodableVector` to assemble TEE-enforced properties. This could lead to duplicate entries if a property (e.g., `OS_VERSION`) was present in the original certificate and also added by the simulator. The code now uses a `MutableMap` keyed by the ASN.1 tag number. This ensures that any simulated properties overwrite the original ones, preventing duplicates and potential parsing errors.

3. Add Detailed Logging: A recursive ASN.1 formatting function has been added to provide clear and readable logs of the certificate data both before and after patching. This significantly improves debuggability.

By ensuring the patched certificate is structurally as close as possible to the original, these changes reduce the chances of the simulator being detected by attestation validation services.
2025-11-29 19:58:02 +01:00
JingMatrix 457a58da04 Patch certificate chain in generateKey reply
When an application generates a key with an attestation request, the `generateKey` method returns a `KeyMetadata` object which contains the full, unpatched certificate chain.

This leaves a potential detection vector open. A sophisticated application could inspect the returned data in its own process memory and discover the original, hardware-backed certificates before they are used for attestation, thus detecting the hooking framework.

This commit introduces a post-transaction hook for the `generateKey` transaction. After the genuine KeyStore service has executed the request, this hook intercepts the reply parcel. It extracts the certificate chain from the `KeyMetadata`, applies the patching routine, and then reconstructs the reply with the modified (patched) certificate chain.
2025-11-29 19:58:02 +01:00
JingMatrixandGitHub b2838ac04b Add support for Android 11 RefBase ABI (#29)
Implements a compatibility layer to allow the binary to run on
Android 11 (API 30) and older, which lack the `incStrongRequireStrong`
symbol in their `libutils.so`.

This is achieved by creating a runtime wrapper that checks the device's
SDK version.
- On Android 12 (API 31) and newer, it dynamically loads and calls the
  `incStrongRequireStrong` function using `dlsym`.
- On older versions, it safely falls back to the universally available
  `incStrong` method.

This resolves the fatal `dlopen` error "cannot locate symbol" when
injecting the library into processes on older Android versions.

See AOSP change
https://android-review.googlesource.com/c/platform/system/core/+/1660499
2025-11-29 19:29:48 +01:00
JingMatrixandGitHub a7534feac7 Fix software enforced list for certificates generation (#28)
Properly implement the `ATTESTATION_APPLICATION_ID` tag into key description.

Moreover, we add the `ATTESTATION_ID_SERIAL` tag to the TEE enforced list, and re-order all tags to remain consistent with the object `AttestationConstants`.
2025-11-29 14:20:58 +01:00
35 changed files with 2086 additions and 393 deletions
+2 -1
View File
@@ -60,7 +60,8 @@ jobs:
- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew --parallel zipRelease zipDebug --stacktrace
./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m
- name: Prepare artifact
if: success()
+59 -6
View File
@@ -76,12 +76,65 @@ org.matrix.demo
### Security Patch Level (`security_patch.txt`)
This allows you to configure the security patch level that the simulator will report in its forged attestation certificates.
This file allows you to configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` that the simulator will report in its patched or forged attestation certificates.
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change the actual system properties of your device.
#### Global and Per-Package Configuration
You can set a global patch level that applies to all applications, and you can also override these settings for specific packages. The syntax is hierarchical:
* Settings defined at the top of the file, before any `[package.name]` line, are **global** and serve as the default for all apps.
* To create a specific configuration for an application, add its package name in square brackets (e.g., `[com.google.android.gms]`). All settings following this line will apply *only* to that package until a new package context is declared.
#### Configuration Keys and Values
You can specify the patch level for the following components using a `key=value` format:
* `system`: The main OS patch level.
* `vendor`: The vendor patch level.
* `boot`: The boot/kernel patch level.
* `all`: A convenient shorthand to set the same date for `system`, `vendor`, and `boot` simultaneously. Any individual key can still be used to override the value set by `all`.
Dates should be provided in `YYYY-MM-DD` format (e.g., `2025-11-05`).
#### Special Keywords
In addition to static dates, several special keywords provide advanced, dynamic control:
* **`today`**: Dynamically uses the current date every time an attestation is generated. This ensures the device always appears up-to-date without needing manual edits.
* **Date Templates**: You can create semi-dynamic dates using `YYYY`, `MM`, and `DD` as placeholders for the current year, month, and day. For example, `YYYY-MM-05` will always resolve to the 5th of the current month and year.
* **`no`**: This keyword instructs the simulator to **completely omit** the corresponding patch level tag from the generated attestation.
* **`device_default`**: This keyword forces the simulator to fall back and use the device's **real hardware value** for that specific patch level. This is essential for creating exceptions to a global override or an `all` rule.
#### Example Configuration
This example demonstrates how to combine global settings, per-package overrides, and special keywords for fine-grained control.
```
# Advanced Configuration
system=2025-11
boot=no # Do not report a boot patch level
vendor=20251101 # Report a specific vendor patch level
# --- Global Configuration ---
# This is the default for all apps unless specified otherwise.
# - Forge a recent system patch level, the 5th of the current month (a common patch date).
# - Use the device's real vendor patch level.
# - Do not report a boot patch level at all.
system=YYYY-MM-05
vendor=device_default
boot=no
# --- Per-Package Override for Google Play Services ---
# This app will report an older, specific date for its system patch.
# It will inherit the global settings for vendor (device_default) and boot (no).
[com.google.android.gms]
system=2024-10-01
# --- Per-Package Override for a Demo App ---
# This app gets a completely custom configuration.
[org.matrix.demo]
# Set a base date for all patch levels...
all=2025-09-15
# ...but make an exception: use the real boot patch level instead of the one from 'all'.
boot=device_default
```
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change system properties.
+2 -1
View File
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v2.1"
val verName = "v3.0"
android {
namespace = "org.matrix.TEESimulator"
@@ -56,6 +56,7 @@ android {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
buildFeatures { buildConfig = true }
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
+2 -2
View File
@@ -12,7 +12,7 @@ add_subdirectory(external/LSPlt/lsplt/src/main/jni)
add_compile_definitions(BINDER_DISABLE_NATIVE_HANDLE)
add_library(utils SHARED stub/stub_utils.cpp)
target_include_directories(utils PUBLIC external/AOSP/include)
target_include_directories(utils PUBLIC external/AOSP/include compat)
add_library(binder SHARED stub/stub_binder.cpp)
target_include_directories(binder PUBLIC external/AOSP/include)
@@ -22,7 +22,7 @@ add_executable(libinject.so inject/main.cpp inject/utils.cpp)
target_include_directories(libinject.so PUBLIC include)
target_link_libraries(libinject.so PRIVATE lsplt_static)
add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp)
add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp compat/refbase_compat.cpp)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC external/linux-kernel/include include)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE binder lsplt_static utils)
@@ -0,0 +1,61 @@
#include "refbase_compat.h"
#include "utils/RefBase.h"
#include <atomic>
#include <cstdlib>
#include <cstring> // For memcpy
#include <dlfcn.h>
#include <mutex>
#include <sys/system_properties.h>
namespace android {
// Helper function to get the Android API level at runtime.
// It caches the result for performance.
int32_t get_android_api_level() {
static std::atomic<int32_t> api_level = -1;
if (api_level.load(std::memory_order_relaxed) == -1) {
char sdk_version_str[PROP_VALUE_MAX];
if (__system_property_get("ro.build.version.sdk", sdk_version_str) > 0) {
api_level.store(atoi(sdk_version_str), std::memory_order_relaxed);
}
}
return api_level.load(std::memory_order_relaxed);
}
// Define the function pointer type for the const member function
// RefBase::incStrongRequireStrong.
using incStrongRequireStrong_t = void (RefBase::*)(const void *) const;
// This is the implementation of our compatibility wrapper.
void incStrongFromExisting(const RefBase *ref, const void *id) {
// Only attempt to use the new function on Android 12 (API 31) or higher.
if (get_android_api_level() >= 31) {
static incStrongRequireStrong_t sIncStrongRequireStrong = nullptr;
static std::once_flag sFlag;
// Thread-safe, one-time initialization.
std::call_once(sFlag, []() {
// Find the symbol in the already loaded libraries.
// The mangled symbol is _ZNK7android7RefBase22incStrongRequireStrongEPKv
void *sym = dlsym(RTLD_DEFAULT,
"_ZNK7android7RefBase22incStrongRequireStrongEPKv");
if (sym) {
// Safely cast the void* symbol to our member function pointer.
memcpy(&sIncStrongRequireStrong, &sym, sizeof(void *));
}
});
if (sIncStrongRequireStrong) {
// If the symbol was found, call it as member function.
(ref->*sIncStrongRequireStrong)(id);
return; // Success, we are done.
}
// If dlsym failed for any reason, we fall through to the old method.
}
// Fallback for older Android versions or if dlsym failed.
// This calls the universally available incStrong method.
ref->incStrong(id);
}
} // namespace android
+11
View File
@@ -0,0 +1,11 @@
#pragma once
namespace android {
// Forward-declare the RefBase class.
class RefBase;
// Declares our compatibility function.
void incStrongFromExisting(const RefBase *ref, const void *id);
} // namespace android
@@ -17,6 +17,7 @@
#ifndef ANDROID_STRONG_POINTER_H
#define ANDROID_STRONG_POINTER_H
#include "refbase_compat.h"
#include <functional>
#include <type_traits> // for common_type.
@@ -212,7 +213,7 @@ sp<T> sp<T>::make(Args&&... args) {
template <typename T>
sp<T> sp<T>::fromExisting(T* other) {
if (other) {
other->incStrongRequireStrong(other);
incStrongFromExisting(other, other);
sp<T> result;
result.m_ptr = other;
return result;
@@ -1,6 +1,8 @@
package org.matrix.TEESimulator
import android.os.Build
import java.security.Security
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
@@ -28,11 +30,20 @@ object App {
SystemLogger.info("Welcome to TEESimulator!")
try {
// Set up the device's boot hash, which is crucial for attestation.
AndroidDeviceUtils.setupBootHash()
// Load the package configuration.
ConfigurationManager.initialize()
// Set up the device's boot key and hash, which are crucial for attestation.
AndroidDeviceUtils.setupBootKeyAndHash()
// Initialize and start the appropriate keystore interceptors.
initializeInterceptors()
// Enter an infinite loop to keep the service running.
// 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())
maintainService()
} catch (e: Exception) {
SystemLogger.error("A fatal error occurred in the main application thread.", e)
@@ -53,9 +64,7 @@ object App {
Thread.sleep(RETRY_DELAY_MS)
}
// Load the package configuration after interceptors are ready.
ConfigurationManager.initialize()
SystemLogger.info("Interceptors and configuration initialized successfully.")
SystemLogger.info("Interceptors initialized successfully.")
}
/**
@@ -1,10 +1,13 @@
package org.matrix.TEESimulator.attestation
import android.content.pm.PackageManager
import android.os.Build
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
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
@@ -12,7 +15,10 @@ 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.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils
import org.matrix.TEESimulator.util.AndroidDeviceUtils.DO_NOT_REPORT
/**
* A builder object responsible for constructing the ASN.1 DER-encoded Android Key Attestation
@@ -24,11 +30,21 @@ object AttestationBuilder {
* Builds the complete X.509 attestation extension.
*
* @param params The parsed key generation parameters.
* @param uid The UID of the application requesting attestation.
* @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)
fun buildAttestationExtension(
params: KeyMintAttestation,
uid: Int,
securityLevel: Int,
): Extension {
val keyDescription = buildKeyDescription(params, uid, securityLevel)
var formattedString =
keyDescription.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it)
}
SystemLogger.verbose("Forged attestation data: ${formattedString}")
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
}
@@ -39,70 +55,95 @@ object AttestationBuilder {
* @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)
DEROctetString(AndroidDeviceUtils.bootKey)
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)
DEROctetString(AndroidDeviceUtils.bootHash)
return DERSequence(rootOfTrustElements)
}
/** Assembles a list of simulated hardware-enforced properties. */
internal fun addSimulatedHardwareProperties(vector: org.bouncycastle.asn1.ASN1EncodableVector) {
vector.add(
/**
* Assembles a map representing the desired state of simulated hardware-enforced properties. A
* null value for a given tag indicates that it should be removed from the attestation.
*
* @param uid The UID of the calling application.
* @return A map where keys are attestation tag numbers and values are the desired
* [DERTaggedObject] or null to signify removal.
*/
fun getSimulatedHardwareProperties(uid: Int): Map<Int, DERTaggedObject?> {
val properties = mutableMapOf<Int, DERTaggedObject?>()
// OS Version is always present.
properties[AttestationConstants.TAG_OS_VERSION] =
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.vendorPatchLevelLong.toLong()),
)
)
vector.add(
DERTaggedObject(
true,
AttestationConstants.TAG_BOOT_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
)
)
val osPatch = AndroidDeviceUtils.getPatchLevel(uid)
properties[AttestationConstants.TAG_OS_PATCHLEVEL] =
if (osPatch != DO_NOT_REPORT) {
DERTaggedObject(
true,
AttestationConstants.TAG_OS_PATCHLEVEL,
ASN1Integer(osPatch.toLong()),
)
} else {
null // Signal for removal
}
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(uid)
properties[AttestationConstants.TAG_VENDOR_PATCHLEVEL] =
if (vendorPatch != DO_NOT_REPORT) {
DERTaggedObject(
true,
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
ASN1Integer(vendorPatch.toLong()),
)
} else {
null // Signal for removal
}
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
if (bootPatch != DO_NOT_REPORT) {
DERTaggedObject(
true,
AttestationConstants.TAG_BOOT_PATCHLEVEL,
ASN1Integer(bootPatch.toLong()),
)
} else {
null // Signal for removal
}
return properties
}
/** 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()
private fun buildKeyDescription(
params: KeyMintAttestation,
uid: Int,
securityLevel: Int,
): ASN1Sequence {
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
val fields =
arrayOf(
ASN1Integer(AndroidDeviceUtils.attestVersion.toLong()), // attestationVersion
ASN1Integer(
AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()
), // attestationVersion
ASN1Enumerated(securityLevel), // attestationSecurityLevel
ASN1Integer(AndroidDeviceUtils.keymasterVersion.toLong()), // keymasterVersion
ASN1Integer(
AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()
), // keymasterVersion
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
DEROctetString(ByteArray(0)), // uniqueId
@@ -113,7 +154,11 @@ object AttestationBuilder {
}
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
private fun buildTeeEnforcedList(params: KeyMintAttestation): DERSequence {
private fun buildTeeEnforcedList(
params: KeyMintAttestation,
uid: Int,
securityLevel: Int,
): DERSequence {
val list =
mutableListOf<ASN1Encodable>(
DERTaggedObject(
@@ -152,28 +197,12 @@ object AttestationBuilder {
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.vendorPatchLevelLong.toLong()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_BOOT_PATCHLEVEL,
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
),
)
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
val simulatedProperties = getSimulatedHardwareProperties(uid)
simulatedProperties.values.filterNotNull().forEach { list.add(it) }
// Add optional device identifiers if they were provided.
params.brand?.let {
list.add(
@@ -202,6 +231,33 @@ object AttestationBuilder {
)
)
}
params.serial?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_SERIAL,
DEROctetString(it),
)
)
}
params.imei?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_IMEI,
DEROctetString(it),
)
)
}
params.meid?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_MEID,
DEROctetString(it),
)
)
}
params.manufacturer?.let {
list.add(
DERTaggedObject(
@@ -220,35 +276,39 @@ object AttestationBuilder {
)
)
}
params.imei?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_IMEI,
DEROctetString(it),
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 300) {
params.secondImei?.let {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_ID_SECOND_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),
)
)
}
}
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
}
if (AndroidDeviceUtils.attestVersion >= 400) {
/**
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
* Keystore.
*/
private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence {
val list =
mutableListOf<ASN1Encodable>(
DERTaggedObject(
true,
AttestationConstants.TAG_CREATION_DATETIME,
ASN1Integer(System.currentTimeMillis()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
createApplicationId(uid),
),
)
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
list.add(
DERTaggedObject(
true,
@@ -257,26 +317,87 @@ object AttestationBuilder {
)
)
}
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
return DERSequence(list.toTypedArray())
}
/**
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
* Keystore.
* A wrapper for a byte array that provides content-based equality. This is necessary for using
* signature digests in a Set.
*/
private fun buildSoftwareEnforcedList(): DERSequence {
val list =
arrayOf<ASN1Encodable>(
DERTaggedObject(
true,
AttestationConstants.TAG_CREATION_DATETIME,
ASN1Integer(System.currentTimeMillis()),
private data class Digest(val digest: ByteArray) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
return digest.contentEquals((other as Digest).digest)
}
override fun hashCode(): Int = digest.contentHashCode()
}
/**
* Creates the AttestationApplicationId structure. This structure contains information about the
* package(s) and their signing certificates.
*
* @param uid The UID of the application.
* @return A DER-encoded octet string containing the application ID information.
* @throws IllegalStateException If the PackageManager or package information cannot be
* retrieved.
*/
@Throws(Throwable::class)
private fun createApplicationId(uid: Int): DEROctetString {
val pm =
ConfigurationManager.getPackageManager()
?: throw IllegalStateException("PackageManager not found!")
val packages =
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
val sha256 = MessageDigest.getInstance("SHA-256")
val packageInfoList = mutableListOf<DERSequence>()
val signatureDigests = mutableSetOf<Digest>()
// Process all packages associated with the UID in a single loop.
packages.forEach { packageName ->
val userId = uid / 100000
val packageInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(
packageName,
PackageManager.GET_SIGNING_CERTIFICATES.toLong(),
userId,
)
} else {
@Suppress("DEPRECATION")
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
}
// Add package information (name and version code) to our list.
packageInfoList.add(
DERSequence(
arrayOf(
DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)),
ASN1Integer(packageInfo.longVersionCode),
)
)
// 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)
// Collect unique signature digests from the signing history.
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
val digest = sha256.digest(signature.toByteArray())
signatureDigests.add(Digest(digest))
}
}
// The application ID is a sequence of two sets:
// 1. A set of package information (name and version).
// 2. A set of SHA-256 digests of the signing certificates.
val applicationIdSequence =
DERSequence(
arrayOf(
DERSet(packageInfoList.toTypedArray()),
DERSet(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()),
)
)
return DEROctetString(applicationIdSequence.encoded)
}
}
@@ -1,23 +1,21 @@
package org.matrix.TEESimulator.attestation
import android.security.keystore.KeyProperties
import java.nio.charset.StandardCharsets
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.*
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.jce.provider.BouncyCastleProvider
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
import org.matrix.TEESimulator.util.toHex
/**
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
@@ -55,8 +53,7 @@ object AttestationPatcher {
// 2. Get the appropriate keybox for the given algorithm to sign the new
// certificate.
val algorithm = originalLeaf.publicKey.algorithm
val keybox = getKeyboxForUidAndAlgorithm(uid, algorithm)
val keybox = getKeyboxForUidAndAlgorithm(uid, originalLeaf.sigAlgName)
// 3. Create the new, patched leaf certificate.
val patchedLeaf =
@@ -65,6 +62,7 @@ object AttestationPatcher {
parsedAttestation,
keybox,
originalLeaf.sigAlgName,
uid,
)
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
@@ -94,6 +92,7 @@ object AttestationPatcher {
* @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.
* @param uid The UID of the application requesting the certificate.
* @return A new [Certificate] object.
*/
private fun createPatchedLeafCertificate(
@@ -101,6 +100,7 @@ object AttestationPatcher {
parsedAttestation: ParsedAttestation,
keybox: KeyBox,
sigAlgName: String,
uid: Int,
): Certificate {
// The issuer of our new leaf is the subject of the first certificate in our custom keybox
// chain.
@@ -117,28 +117,95 @@ object AttestationPatcher {
)
// Create the new, patched attestation extension.
val patchedExtension = createPatchedAttestationExtension(parsedAttestation)
builder.addExtension(patchedExtension)
val patchedExtension = createPatchedAttestationExtension(parsedAttestation, uid)
// 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)) }
originalLeafHolder.extensions.extensionOIDs.forEach {
builder.addExtension(
if (it == ATTESTATION_OID) patchedExtension else originalLeafHolder.getExtension(it)
)
}
// Sign the newly built certificate with the private key from our keybox.
val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private)
val signer =
JcaContentSignerBuilder(sigAlgName)
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keybox.keyPair.private)
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
return JcaX509CertificateConverter().getCertificate(builder.build(signer))
// Log the signature of the newly created certificate to observe its non-deterministic
// nature.
val signatureBytes = (newCertificate as X509Certificate).signature
SystemLogger.verbose("Signature of patched leaf cert: ${signatureBytes.toHex()}")
return newCertificate
}
/**
* Retrieves the appropriate signing KeyBox (KeyPair and certificate chain) for a given UID
* based on a specified algorithm identifier.
*
* @param uid The UID of the application for which the signing is being performed.
* @param algorithm A string representing the desired algorithm. This can be either:
* 1. A simple key type like "RSA" or "EC".
* 2. A full JCA signature algorithm name like "SHA256withRSA".
*
* @return The [KeyBox] containing the appropriate key pair for signing.
* @throws IllegalArgumentException if no matching KeyBox can be found for the derived key type.
*/
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid)
return KeyBoxManager.getAttestationKey(keyboxFile, algorithm)
// Normalize the algorithm name. The input might be a full signature algorithm
// (e.g., "SHA256withRSA") or just the key type (e.g., "RSA").
val keyType =
when {
algorithm.contains("RSA", ignoreCase = true) -> KeyProperties.KEY_ALGORITHM_RSA
algorithm.contains("EC", ignoreCase = true) ->
KeyProperties.KEY_ALGORITHM_EC // This also covers "ECDSA"
else -> algorithm // If no match, assume it's already a simple key type string.
}
return KeyBoxManager.getAttestationKey(keyboxFile, keyType)
?: throw IllegalArgumentException(
"No keybox found for UID $uid and algorithm $algorithm in file $keyboxFile"
"No keybox found for UID $uid and algorithm '$keyType' (derived from input '$algorithm') in file $keyboxFile"
)
}
/** Recursively formats an ASN1Primitive into a concise, readable string. */
fun formatAsn1Primitive(obj: ASN1Encodable?): String {
val primitive = obj?.toASN1Primitive()
return when (primitive) {
null -> "NULL"
is ASN1Integer -> primitive.value.toString()
is ASN1Enumerated -> primitive.value.toString()
is ASN1Boolean -> primitive.isTrue.toString()
is ASN1Null -> "NULL"
is ASN1OctetString -> {
val bytes = primitive.octets
// Attempt to decode as a printable string, otherwise show hex
if (bytes.all { it >= 32 && it < 127 }) {
"\"${String(bytes, StandardCharsets.UTF_8)}\""
} else if (bytes.isEmpty()) {
"\"\""
} else {
"#" + bytes.toHex()
}
}
is ASN1TaggedObject ->
"[TAG ${primitive.tagNo}]${formatAsn1Primitive(primitive.baseObject)}"
is ASN1Sequence ->
primitive
.map { formatAsn1Primitive(it) }
.joinToString(prefix = "[", postfix = "]", separator = ", ")
is ASN1Set ->
primitive
.map { formatAsn1Primitive(it) }
.joinToString(prefix = "{", postfix = "}", separator = ", ")
else -> primitive.toString() // Fallback for other types
}
}
/** Parses the critical components from an existing attestation extension. */
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
@@ -147,41 +214,54 @@ object AttestationPatcher {
val teeEnforced =
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence
val teeEnforcedVector = ASN1EncodableVector()
var originalRootOfTrust: ASN1Encodable? = null
val teeEnforcedMap = mutableMapOf<Int, ASN1TaggedObject>()
teeEnforced.forEach { element ->
val taggedObject = element as ASN1TaggedObject
if (taggedObject.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST) {
originalRootOfTrust = taggedObject.baseObject.toASN1Primitive()
} else {
teeEnforcedVector.add(taggedObject)
teeEnforcedMap[taggedObject.tagNo] = taggedObject
}
}
return ParsedAttestation(allFields, teeEnforcedVector, originalRootOfTrust)
return ParsedAttestation(allFields, teeEnforcedMap, originalRootOfTrust)
}
/** Constructs a new, patched attestation extension using simulated device properties. */
private fun createPatchedAttestationExtension(parsed: ParsedAttestation): Extension {
val (allFields, teeEnforcedVector, originalRootOfTrust) = parsed
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
// Build the new Root of Trust with our simulated values.
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
SystemLogger.verbose("Original attestation data: ${formattedString}")
// Build the new Root of Trust and add/replace it in the map.
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
teeEnforcedVector.add(
teeEnforcedMap[AttestationConstants.TAG_ROOT_OF_TRUST] =
DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust)
)
// Add other simulated hardware properties.
AttestationBuilder.addSimulatedHardwareProperties(teeEnforcedVector)
// Get the desired state for simulated properties.
val simulatedProperties = AttestationBuilder.getSimulatedHardwareProperties(uid)
// 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 }
// Apply the desired state: update, add, or remove properties from the original map.
simulatedProperties.forEach { (tag, value) ->
if (value != null) {
// If the value is not null, add or update it.
teeEnforcedMap[tag] = value
} else {
// If the value is null, remove the tag from the map.
teeEnforcedMap.remove(tag)
}
}
// Re-assemble the TEE enforced list from the map's values, sorting for DER compliance.
val sortedElements = teeEnforcedMap.values.sortedBy { it.tagNo }
val sortedTeeEnforced = DERSequence(sortedElements.toTypedArray())
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
val patchedSequence = DERSequence(allFields)
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
SystemLogger.verbose("Patched attestation data: ${formattedString}")
val patchedOctets = DEROctetString(patchedSequence)
return Extension(ATTESTATION_OID, false, patchedOctets)
@@ -190,7 +270,7 @@ object AttestationPatcher {
/** Helper data class to hold the parsed components of an attestation extension. */
private data class ParsedAttestation(
val allFields: Array<ASN1Encodable>,
val teeEnforcedVector: ASN1EncodableVector,
val teeEnforcedMap: MutableMap<Int, ASN1TaggedObject>,
val rootOfTrust: ASN1Encodable?,
)
}
@@ -38,16 +38,25 @@ object DeviceAttestationService {
* Holds key data extracted from a genuine device attestation. This data can be used as a
* baseline for creating simulated attestations.
*
* @property verifiedBootKey The verified boot public key digest from the root of trust.
* @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.
* @property osPatchLevel The Android security patch level (e.g., 202511).
* @property vendorPatchLevel The vendor-specific security patch level.
* @property bootPatchLevel The bootloader's security patch level.
*/
data class AttestationData(
val moduleHash: ByteArray?,
val verifiedBootKey: ByteArray?,
val verifiedBootHash: ByteArray?,
val attestVersion: Int?,
val keymasterVersion: Int?,
val osVersion: Int?,
val osPatchLevel: Int?,
val vendorPatchLevel: Int?,
val bootPatchLevel: Int?,
)
// A unique alias for the key used to perform the TEE functionality check.
@@ -151,6 +160,11 @@ object DeviceAttestationService {
// The extension's value is an ASN.1 sequence.
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
var formattedString =
keyDescriptionSeq.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it)
}
SystemLogger.verbose("Cached attestation data: ${formattedString}")
val fields = keyDescriptionSeq.toArray()
val attestVersion =
@@ -166,8 +180,25 @@ object DeviceAttestationService {
.positiveValue
.toInt()
var moduleHash: ByteArray? = null
var verifiedBootKey: ByteArray? = null
var verifiedBootHash: ByteArray? = null
var osVersion: Int? = null
var osPatchLevel: Int? = null
var vendorPatchLevel: Int? = null
var bootPatchLevel: Int? = null
val softwareEnforced =
ASN1Sequence.getInstance(
fields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
)
if (softwareEnforced.size() >= 3) {
moduleHash =
ASN1OctetString.getInstance(
ASN1TaggedObject.getInstance(softwareEnforced.getObjectAt(2)).baseObject
)
.octets
}
val teeEnforced =
ASN1Sequence.getInstance(
@@ -179,6 +210,14 @@ object DeviceAttestationService {
AttestationConstants.TAG_ROOT_OF_TRUST -> {
val rotSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive())
if (rotSeq.size() >= 4) {
verifiedBootKey =
ASN1OctetString.getInstance(
rotSeq.getObjectAt(
AttestationConstants
.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX
)
)
.octets
verifiedBootHash =
ASN1OctetString.getInstance(
rotSeq.getObjectAt(
@@ -189,19 +228,51 @@ object DeviceAttestationService {
.octets
}
}
AttestationConstants.TAG_OS_VERSION -> { // OS Version (TAG_OS_VERSION)
AttestationConstants.TAG_OS_VERSION -> {
osVersion =
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
.positiveValue
.toInt()
}
AttestationConstants.TAG_OS_PATCHLEVEL -> {
osPatchLevel =
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
.positiveValue
.toInt()
}
AttestationConstants.TAG_VENDOR_PATCHLEVEL -> {
vendorPatchLevel =
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
.positiveValue
.toInt()
}
AttestationConstants.TAG_BOOT_PATCHLEVEL -> {
bootPatchLevel =
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
.positiveValue
.toInt()
}
}
}
if (verifiedBootKey?.all { it == 0.toByte() } == true) {
verifiedBootKey = null
}
SystemLogger.info(
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, bootHash=${verifiedBootHash?.toHex()}"
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
)
return AttestationData(
moduleHash,
verifiedBootKey,
verifiedBootHash,
attestVersion,
keymasterVersion,
osVersion,
osPatchLevel,
vendorPatchLevel,
bootPatchLevel,
)
return AttestationData(verifiedBootHash, attestVersion, keymasterVersion, osVersion)
} catch (e: Exception) {
SystemLogger.error("Failed to parse attestation data from certificate.", e)
return null
@@ -33,11 +33,12 @@ data class KeyMintAttestation(
val brand: ByteArray?,
val device: ByteArray?,
val product: ByteArray?,
val serial: ByteArray?,
val imei: ByteArray?,
val meid: 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(
@@ -82,11 +83,12 @@ data class KeyMintAttestation(
brand = params.findBlob(Tag.ATTESTATION_ID_BRAND),
device = params.findBlob(Tag.ATTESTATION_ID_DEVICE),
product = params.findBlob(Tag.ATTESTATION_ID_PRODUCT),
serial = params.findBlob(Tag.ATTESTATION_ID_SERIAL),
imei = params.findBlob(Tag.ATTESTATION_ID_IMEI),
meid = params.findBlob(Tag.ATTESTATION_ID_MEID),
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) }
@@ -40,7 +40,8 @@ object ConfigurationManager {
@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
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
// Cache for UID to package name resolution.
private val uidToPackagesCache = ConcurrentHashMap<Int, Array<String>>()
@@ -104,6 +105,21 @@ object ConfigurationManager {
return null // No configuration found for this UID.
}
/**
* Retrieves the custom patch level configuration for a given UID. It first checks for a
* package-specific override and falls back to the global configuration.
*
* @param uid The UID of the calling application.
* @return The applicable [CustomPatchLevel], or null if no custom configuration exists.
*/
fun getPatchLevelForUid(uid: Int): CustomPatchLevel? {
val packages = getPackagesForUid(uid)
// Find the first package-specific configuration for this UID.
val packageSpecificPatchLevel =
packages.firstNotNullOfOrNull { pkg -> packagePatchLevels[pkg] }
return packageSpecificPatchLevel ?: globalCustomPatchLevel
}
/**
* Loads and parses the `target.txt` file, which defines the processing mode and keybox file for
* each package.
@@ -162,26 +178,48 @@ object ConfigurationManager {
}
}
/** Loads the security patch level override configuration from `security_patch.txt`. */
/**
* Loads and parses the `security_patch.txt` file, which can define both global and per-package
* security patch levels.
*/
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 (!file.exists()) {
globalCustomPatchLevel = null
packagePatchLevels = emptyMap()
return
}
if (lines.isEmpty()) {
customPatchLevelOverride = null
return
}
try {
val newPackageLevels = mutableMapOf<String, CustomPatchLevel>()
var currentContext = "" // Empty string for global context
val contextLines = mutableMapOf<String, MutableList<String>>()
val contextRegex = Regex("^\\[([a-zA-Z0-9_.-]+)]$")
// First pass: group lines by context (global or package-specific).
file.readLines().forEach { line ->
val trimmedLine = line.trim()
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) return@forEach
contextRegex.find(trimmedLine)?.let { currentContext = it.groupValues[1] }
?: run {
contextLines
.computeIfAbsent(currentContext) { mutableListOf() }
.add(trimmedLine)
}
}
// Helper function to parse a set of lines into a CustomPatchLevel object.
fun parseLines(lines: List<String>?): CustomPatchLevel? {
if (lines.isNullOrEmpty()) return null
// 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
return CustomPatchLevel(
system = null,
vendor = null,
boot = null,
all = lines[0],
)
}
// Handle key-value pair configuration.
@@ -195,19 +233,32 @@ object ConfigurationManager {
.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)
return CustomPatchLevel(
system = map["system"] ?: all,
vendor = map["vendor"] ?: all,
boot = map["boot"] ?: all,
all = all,
)
}
} else {
customPatchLevelOverride = null
// Parse global and per-package configurations.
val newGlobalLevel = parseLines(contextLines[""])
contextLines.remove("") // Remove global context to iterate over packages next
for ((pkg, lines) in contextLines) {
parseLines(lines)?.let { newPackageLevels[pkg] = it }
}
// Atomically update the configuration state.
globalCustomPatchLevel = newGlobalLevel
packagePatchLevels = newPackageLevels
SystemLogger.info(
"Loaded custom security patch levels: global config exists=${newGlobalLevel != null}, " +
"${newPackageLevels.size} package-specific configs."
)
} catch (e: Exception) {
SystemLogger.error("Failed to load or parse ${file.name}", e)
}
}
@@ -226,13 +226,18 @@ abstract class BinderInterceptor : Binder() {
methodName: String,
callingUid: Int,
callingPid: Int,
isIntercepting: Boolean = true,
skipPost: Boolean = false,
) {
val isIntercepting = !skipPost && !ConfigurationManager.shouldSkipUid(callingUid)
val action = if (isIntercepting) "Intercept" else "Observe"
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
SystemLogger.debug(
val message =
"[TX_ID: $txId] $action $methodName for packages=[$packages] (uid=$callingUid, pid=$callingPid)"
)
if (isIntercepting) {
SystemLogger.debug(message)
} else {
SystemLogger.verbose(message)
}
}
companion object {
@@ -3,6 +3,7 @@ package org.matrix.TEESimulator.interception.keystore
import android.os.Parcel
import android.os.Parcelable
import android.security.KeyStore
import android.security.keystore.KeystoreResponse
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
@@ -27,12 +28,29 @@ object InterceptorUtils {
}
}
/** Creates an `KeystoreResponse` parcel that indicates success with no data. */
fun createSuccessKeystoreResponse(): KeystoreResponse {
val parcel = Parcel.obtain()
try {
parcel.writeInt(KeyStore.NO_ERROR)
parcel.writeString("")
parcel.setDataPosition(0)
return KeystoreResponse.CREATOR.createFromParcel(parcel)
} finally {
parcel.recycle()
}
}
/** Creates an `OverrideReply` parcel that indicates success with no data. */
fun createSuccessReply(): BinderInterceptor.TransactionResult.OverrideReply {
fun createSuccessReply(
writeResultCode: Boolean = true
): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeNoException()
writeInt(KeyStore.NO_ERROR)
if (writeResultCode) {
writeInt(KeyStore.NO_ERROR)
}
}
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
}
@@ -9,6 +9,7 @@ import android.os.Parcel
import android.system.keystore2.IKeystoreService
import android.system.keystore2.KeyDescriptor
import android.system.keystore2.KeyEntryResponse
import java.security.cert.Certificate
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
@@ -103,7 +104,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
val keyId = KeyIdentifier(callingUid, descriptor.alias)
if (code == DELETE_KEY_TRANSACTION) {
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
SystemLogger.info(
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
)
return InterceptorUtils.createSuccessReply(writeResultCode = false)
}
return TransactionResult.ContinueAndSkipPost
}
@@ -125,7 +132,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
transactionNames[code] ?: "unknown code=$code",
callingUid,
callingPid,
false,
true,
)
}
@@ -185,8 +192,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
}
// Perform the attestation patch.
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
CertificateHelper.updateCertificateChain(response.metadata, newChain).getOrThrow()
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
// First, try to retrieve the already-patched chain from our cache to ensure
// consistency.
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
val finalChain: Array<Certificate>
if (cachedChain != null) {
SystemLogger.debug(
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
)
finalChain = cachedChain
} else {
// If no chain is cached (e.g., key existed before simulator started),
// perform a live patch as a fallback. This may still be detectable.
SystemLogger.info(
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
)
finalChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
}
CertificateHelper.updateCertificateChain(response.metadata, finalChain).getOrThrow()
InterceptorUtils.createTypedObjectReply(response)
} catch (e: Exception) {
@@ -4,12 +4,28 @@ import android.annotation.SuppressLint
import android.os.IBinder
import android.os.Parcel
import android.security.Credentials
import android.security.KeyStore
import android.security.keymaster.ExportResult
import android.security.keymaster.KeyCharacteristics
import android.security.keymaster.KeymasterArguments
import android.security.keymaster.KeymasterCertificateChain
import android.security.keymaster.KeymasterDefs
import android.security.keystore.IKeystoreCertificateChainCallback
import android.security.keystore.IKeystoreExportKeyCallback
import android.security.keystore.IKeystoreKeyCharacteristicsCallback
import android.security.keystore.IKeystoreService
import java.math.BigInteger
import java.security.KeyPair
import java.security.cert.Certificate
import java.util.Date
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils.extractAlias
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper
/**
@@ -43,7 +59,9 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
override val processName = "keystore"
override val injectionCommand = "exec ./inject `pidof keystore` libTEESimulator.so entry"
private const val SERVICE_DESCRIPTOR = "android.security.keystore.IKeystoreService"
// State management for the multi-step key generation process.
private val keygenParameters = ConcurrentHashMap<KeyIdentifier, LegacyKeygenParameters>()
private val generatedKeyPairs = ConcurrentHashMap<KeyIdentifier, KeyPair>()
// Cache to store the fully patched chain after the leaf is requested.
private val patchedChainCache = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
@@ -59,23 +77,183 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
): 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()
}
return when (code) {
GENERATE_KEY_TRANSACTION -> handleGenerateKey(txId, callingUid, callingPid, data)
GET_KEY_CHARACTERISTICS_TRANSACTION ->
handleGetKeyCharacteristics(txId, callingUid, callingPid, data)
EXPORT_KEY_TRANSACTION -> handleExportKey(txId, callingUid, callingPid, data)
ATTEST_KEY_TRANSACTION -> handleAttestKey(txId, callingUid, callingPid, data)
else -> TransactionResult.ContinueAndSkipPost
}
} else if (ConfigurationManager.shouldGenerate(callingUid)) {
} else if (ConfigurationManager.shouldPatch(callingUid)) {
// In patch mode, we only care about the 'get' transaction in onPostTransact.
if (code == GET_TRANSACTION) return TransactionResult.Continue
}
return TransactionResult.ContinueAndSkipPost
}
private fun handleGenerateKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
return runCatching {
logTransaction(txId, "generateKey", uid, pid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback =
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
val alias = InterceptorUtils.extractAlias(data.readString()!!)
val keyId = KeyIdentifier(uid, alias)
// Read and parse the key generation arguments.
val keymasterArgs = KeymasterArguments()
if (data.readInt() == 1) {
keymasterArgs.readFromParcel(data)
}
keygenParameters[keyId] =
LegacyKeygenParameters.fromKeymasterArguments(keymasterArgs)
// Create a fake successful response for the callback.
val characteristics = KeyCharacteristics()
characteristics.swEnforced = KeymasterArguments()
characteristics.hwEnforced = keymasterArgs
val keystoreResponse = InterceptorUtils.createSuccessKeystoreResponse()
callback.onFinished(keystoreResponse, characteristics)
InterceptorUtils.createSuccessReply()
}
.getOrElse {
SystemLogger.error("[TX_ID: $txId] Failed during handleGenerateKey.", it)
TransactionResult.ContinueAndSkipPost
}
}
private fun handleGetKeyCharacteristics(
txId: Long,
uid: Int,
pid: Int,
data: Parcel,
): TransactionResult {
return runCatching {
logTransaction(txId, "getKeyCharacteristics", uid, pid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback =
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
val alias = InterceptorUtils.extractAlias(data.readString()!!)
val keyId = KeyIdentifier(uid, alias)
val params =
keygenParameters[keyId]
?: throw IllegalStateException("No params found for $keyId")
val characteristics =
KeyCharacteristics().apply {
swEnforced = KeymasterArguments()
hwEnforced =
KeymasterArguments().apply {
addEnum(KeymasterDefs.KM_TAG_ALGORITHM, params.algorithm)
}
}
callback.onFinished(
InterceptorUtils.createSuccessKeystoreResponse(),
characteristics,
)
InterceptorUtils.createSuccessReply()
}
.getOrElse {
SystemLogger.error("[TX_ID: $txId] Failed during handleGetKeyCharacteristics.", it)
TransactionResult.ContinueAndSkipPost
}
}
private fun handleExportKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
return runCatching {
logTransaction(txId, "exportKey", uid, pid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder())
val alias = InterceptorUtils.extractAlias(data.readString()!!)
val keyId = KeyIdentifier(uid, alias)
val params =
keygenParameters[keyId]
?: throw IllegalStateException("No params found for $keyId")
// Generate a software key pair using the new generator.
val keyPair =
CertificateGenerator.generateSoftwareKeyPair(params.toKeyMintAttestation())
?: throw Exception("Failed to generate software key pair.")
generatedKeyPairs[keyId] = keyPair
// Create a successful ExportResult containing the public key.
val exportResultParcel =
Parcel.obtain().apply {
writeInt(KeyStore.NO_ERROR)
writeByteArray(keyPair.public.encoded)
setDataPosition(0)
}
val exportResult = ExportResult.CREATOR.createFromParcel(exportResultParcel)
exportResultParcel.recycle()
callback.onFinished(exportResult)
InterceptorUtils.createSuccessReply()
}
.getOrElse {
SystemLogger.error("[TX_ID: $txId] Failed during handleExportKey.", it)
TransactionResult.ContinueAndSkipPost
}
}
private fun handleAttestKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
return runCatching {
logTransaction(txId, "attestKey", uid, pid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback =
IKeystoreCertificateChainCallback.Stub.asInterface(data.readStrongBinder())
val alias = InterceptorUtils.extractAlias(data.readString()!!)
val keyId = KeyIdentifier(uid, alias)
// Get the attestation challenge from the arguments.
val params =
keygenParameters[keyId]
?: throw IllegalStateException("No params found for $keyId")
val keyPair =
generatedKeyPairs[keyId]
?: throw IllegalStateException("No keypair found for $keyId")
val attestationArgs = KeymasterArguments()
if (data.readInt() == 1) {
attestationArgs.readFromParcel(data)
val challenge =
attestationArgs.getBytes(
KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE,
ByteArray(0),
)
params.attestationChallenge = challenge
params.attestationChallenge = challenge
}
val certificateChain =
CertificateGenerator.generateCertificateChain(
uid,
keyPair,
null, // No attestKeyAlias in legacy flow
params.toKeyMintAttestation(), // Convert to modern format
1, // SecurityLevel.TRUSTED_ENVIRONMENT
) ?: throw Exception("CertificateGenerator failed to create attested key pair.")
val chainAsByteList = certificateChain.map { it.encoded }
val certChain = KeymasterCertificateChain(chainAsByteList)
callback.onFinished(InterceptorUtils.createSuccessKeystoreResponse(), certChain)
InterceptorUtils.createSuccessReply()
}
.getOrElse {
SystemLogger.error("[TX_ID: $txId] Failed during handleAttestKey.", it)
TransactionResult.ContinueAndSkipPost
}
}
override fun onPostTransact(
txId: Long,
target: IBinder,
@@ -99,7 +277,7 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
if (!ConfigurationManager.shouldPatch(callingUid)) return TransactionResult.SkipTransaction
return try {
data.enforceInterface(SERVICE_DESCRIPTOR)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val alias = data.readString() ?: ""
val extractedAlias = InterceptorUtils.extractAlias(alias)
val keyId = KeyIdentifier(callingUid, extractedAlias)
@@ -111,13 +289,11 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
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 originalLeafCertResult = CertificateHelper.toCertificate(originalLeafBytes)
if (originalLeafCertResult !is CertificateHelper.OperationResult.Success) {
return TransactionResult.SkipTransaction
}
val originalLeafCert = originalLeafCertResult.data
val tempChain = arrayOf<Certificate>(originalLeafCert)
// Perform the COMPLETE patch and rebuild operation.
@@ -157,11 +333,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
)
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."
)
@@ -177,3 +348,115 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
}
}
}
/**
* A data class to hold key generation parameters parsed from the legacy IKeystoreService's
* KeymasterArguments. It is used exclusively by the KeystoreInterceptor to manage state during the
* software key generation flow.
*/
private data class LegacyKeygenParameters(
val algorithm: Int,
val keySize: Int,
val purpose: List<Int>,
val digest: List<Int>,
val certificateNotBefore: Date?,
val rsaPublicExponent: BigInteger?,
val ecCurveName: String?, // Derived from keySize
) {
// The challenge is provided in a separate transaction (attestKey), so it must be mutable.
var attestationChallenge: ByteArray? = null
/**
* Converts the legacy parameters into the modern [KeyMintAttestation] data structure, which is
* required by the refactored [AttestationBuilder] and [CertificateGenerator].
*/
fun toKeyMintAttestation(): KeyMintAttestation {
// This conversion acts as a bridge, allowing our new generic components
// to be used by the legacy interceptor.
return KeyMintAttestation(
keySize = this.keySize,
algorithm = this.algorithm,
ecCurve = 0, // Not explicitly available in legacy args, but not critical
ecCurveName = this.ecCurveName ?: "",
purpose = this.purpose,
digest = this.digest,
rsaPublicExponent = this.rsaPublicExponent,
certificateSerial = null, // Not provided in legacy generateKey
certificateSubject = null, // Not provided in legacy generateKey
certificateNotBefore = this.certificateNotBefore,
certificateNotAfter = null, // Not provided in legacy generateKey
attestationChallenge = this.attestationChallenge,
// Device identifiers are not passed in legacy args;
// AttestationBuilder will fetch them from system properties.
brand = null,
device = null,
product = null,
serial = null,
imei = null,
meid = null,
manufacturer = null,
model = null,
secondImei = null,
)
}
companion object {
/** Factory method to create an instance from a [KeymasterArguments] object. */
fun fromKeymasterArguments(args: KeymasterArguments): LegacyKeygenParameters {
val algorithm = args.getEnum(KeymasterDefs.KM_TAG_ALGORITHM, 0)
val keySize = args.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, 0).toInt()
return LegacyKeygenParameters(
algorithm = algorithm,
keySize = keySize,
purpose = args.getEnums(KeymasterDefs.KM_TAG_PURPOSE),
digest = args.getEnums(KeymasterDefs.KM_TAG_DIGEST),
certificateNotBefore = args.getDate(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, Date()),
rsaPublicExponent =
if (algorithm == KeymasterDefs.KM_ALGORITHM_RSA) getRsaExponent(args) else null,
ecCurveName =
if (algorithm == KeymasterDefs.KM_ALGORITHM_EC) deriveEcCurveName(keySize)
else null,
)
}
private fun deriveEcCurveName(keySize: Int): String =
when (keySize) {
224 -> "secp224r1"
256 -> "secp256r1"
384 -> "secp384r1"
521 -> "secp521r1"
else -> "secp256r1" // Default fallback
}
/**
* The RSA public exponent is not accessible via a public API in KeymasterArguments, so we
* must use reflection to extract it.
*/
private fun getRsaExponent(args: KeymasterArguments): BigInteger? {
return runCatching {
val getArgumentByTag =
KeymasterArguments::class
.java
.getDeclaredMethod("getArgumentByTag", Int::class.java)
getArgumentByTag.isAccessible = true
val rsaArgument =
getArgumentByTag.invoke(args, KeymasterDefs.KM_TAG_RSA_PUBLIC_EXPONENT)
val getLongTagValue =
KeymasterArguments::class
.java
.getDeclaredMethod(
"getLongTagValue",
Class.forName("android.security.keymaster.KeymasterArgument"),
)
getLongTagValue.isAccessible = true
getLongTagValue.invoke(args, rsaArgument) as BigInteger
}
.onFailure {
SystemLogger.error("Failed to read rsaPublicExponent via reflection.", it)
}
.getOrNull()
}
}
}
@@ -10,6 +10,7 @@ import android.system.keystore2.*
import java.security.KeyPair
import java.security.cert.Certificate
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.core.BinderInterceptor
@@ -43,11 +44,15 @@ class KeyMintSecurityLevelInterceptor(
if (code == GENERATE_KEY_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
return handleGenerateKey(callingUid, data)
} else if (code == IMPORT_KEY_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val alias =
data.readTypedObject(KeyDescriptor.CREATOR)?.alias
@@ -60,7 +65,7 @@ class KeyMintSecurityLevelInterceptor(
transactionNames[code] ?: "unknown code=$code",
callingUid,
callingPid,
false,
true,
)
}
return TransactionResult.ContinueAndSkipPost
@@ -77,13 +82,11 @@ class KeyMintSecurityLevelInterceptor(
reply: Parcel?,
resultCode: Int,
): TransactionResult {
// We only care about successful 'importKey' transactions to clean cached keys.
if (
code == IMPORT_KEY_TRANSACTION &&
resultCode == 0 &&
reply != null &&
!InterceptorUtils.hasException(reply)
) {
// We only care about successful transactions.
if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction
if (code == IMPORT_KEY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
@@ -91,6 +94,29 @@ class KeyMintSecurityLevelInterceptor(
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
cleanupKeyData(KeyIdentifier(callingUid, keyDescriptor.alias))
} else if (code == GENERATE_KEY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
val metadata: KeyMetadata =
reply.readTypedObject(KeyMetadata.CREATOR)
?: return TransactionResult.SkipTransaction
val originalChain =
CertificateHelper.getCertificateChain(metadata)
?: return TransactionResult.SkipTransaction
if (originalChain.size > 1) {
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
// Cache the newly patched chain to ensure consistency across subsequent API calls.
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
patchedChains[keyId] = newChain
SystemLogger.debug("Cached patched certificate chain for $keyId.")
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
return InterceptorUtils.createTypedObjectReply(metadata)
}
}
return TransactionResult.SkipTransaction
}
@@ -148,6 +174,8 @@ class KeyMintSecurityLevelInterceptor(
writeTypedObject(response.metadata, 0)
}
return TransactionResult.OverrideReply(0, resultParcel)
} else if (parsedParams.attestationChallenge != null) {
return TransactionResult.Continue
}
// If not generating, clear any stale state for this alias and let the call proceed.
@@ -201,6 +229,8 @@ class KeyMintSecurityLevelInterceptor(
// Stores keys generated entirely in software.
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
// Caches patched certificate chains to prevent re-generation and signature inconsistencies.
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
// A set to quickly identify keys that were generated for attestation purposes.
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
@@ -208,12 +238,17 @@ class KeyMintSecurityLevelInterceptor(
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
generatedKeys[keyId]?.response
fun getPatchedChain(keyId: KeyIdentifier): Array<Certificate>? = patchedChains[keyId]
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
fun cleanupKeyData(keyId: KeyIdentifier) {
if (generatedKeys.remove(keyId) != null) {
SystemLogger.debug("Remove generated key ${keyId}")
}
if (patchedChains.remove(keyId) != null) {
SystemLogger.debug("Remove patched chain for ${keyId}")
}
if (attestationKeys.remove(keyId)) {
SystemLogger.debug("Remove cached attestaion key ${keyId}")
}
@@ -224,6 +259,7 @@ class KeyMintSecurityLevelInterceptor(
val count = generatedKeys.size
val reasonMessage = reason?.let { " due to $it" } ?: ""
generatedKeys.clear()
patchedChains.clear()
attestationKeys.clear()
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
}
@@ -1,6 +1,7 @@
package org.matrix.TEESimulator.logging
import android.util.Log
import org.matrix.TEESimulator.BuildConfig
/**
* A centralized logging utility for the TEESimulator application. This object provides a consistent
@@ -10,6 +11,8 @@ object SystemLogger {
// The tag used for all log messages from this application.
private const val TAG = "TEESimulator"
private val isDebugBuild = BuildConfig.DEBUG
/**
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
*
@@ -64,6 +67,7 @@ object SystemLogger {
* @param message The message to log.
*/
fun verbose(message: String) {
if (!isDebugBuild) return
Log.v(TAG, message)
}
}
@@ -6,7 +6,6 @@ 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
@@ -35,14 +34,6 @@ import org.matrix.TEESimulator.logging.SystemLogger
*/
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.
*
@@ -72,16 +63,55 @@ object CertificateGenerator {
}
/**
* Generates a new key pair and a corresponding certificate chain containing a simulated
* attestation.
* Generates a certificate chain for a given key pair. This is the primary function for creating
* attested certificates.
*
* @param uid The UID of the application requesting the key.
* @param alias The alias for the new key.
* @param subjectKeyPair The key pair for which the certificate will be generated.
* @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.
* @return A [List] of [Certificate] forming the new chain, or `null` on failure.
*/
fun generateCertificateChain(
uid: Int,
subjectKeyPair: KeyPair,
attestKeyAlias: String?,
params: KeyMintAttestation,
securityLevel: Int,
): List<Certificate>? {
return runCatching {
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(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
// chain.
if (attestKeyAlias != null) {
listOf(leafCert)
} else {
listOf(leafCert) + keybox.certificates
}
}
.onFailure { SystemLogger.error("Failed to generate certificate chain.", it) }
.getOrNull()
}
/**
* A convenience function that combines key pair generation and certificate chain generation.
* Primarily used by the modern Keystore2 interceptor where generation is a single step.
*/
fun generateAttestedKeyPair(
uid: Int,
@@ -98,30 +128,9 @@ object CertificateGenerator {
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
}
generateCertificateChain(uid, newKeyPair, attestKeyAlias, params, securityLevel)
?: throw Exception("Failed to generate certificate chain for new key pair.")
SystemLogger.info(
"Successfully generated new certificate chain for alias: '$alias'."
@@ -134,7 +143,7 @@ object CertificateGenerator {
.getOrNull()
}
private fun getIssuerFromKeybox(keybox: KeyBox) =
fun getIssuerFromKeybox(keybox: KeyBox) =
X509CertificateHolder(keybox.certificates[0].encoded).subject
private fun getKeyboxForAlgorithm(uid: Int, algorithm: Int): KeyBox {
@@ -177,6 +186,7 @@ object CertificateGenerator {
signingKeyPair: KeyPair,
issuer: X500Name,
params: KeyMintAttestation,
uid: Int,
securityLevel: Int,
): Certificate {
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
@@ -197,7 +207,9 @@ object CertificateGenerator {
// Add standard extensions.
builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign))
// Add our custom, simulated attestation extension.
builder.addExtension(AttestationBuilder.buildAttestationExtension(params, securityLevel))
builder.addExtension(
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
)
val signerAlgorithm =
when (params.algorithm) {
@@ -205,7 +217,10 @@ object CertificateGenerator {
Algorithm.RSA -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
}
val contentSigner = JcaContentSignerBuilder(signerAlgorithm).build(signingKeyPair.private)
val contentSigner =
JcaContentSignerBuilder(signerAlgorithm)
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(signingKeyPair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
}
@@ -3,6 +3,8 @@ package org.matrix.TEESimulator.pki
import android.security.keystore.KeyProperties
import java.io.File
import java.io.StringReader
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.RSAPrivateKey
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
import org.matrix.TEESimulator.logging.SystemLogger
@@ -51,6 +53,9 @@ object KeyBoxManager {
// If it's not in the cache, the `getOrPut` block is executed to parse and store it.
val keyMap =
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
SystemLogger.verbose(
"Fetching attestation key in $keyStoreFileName with $algorithm algorithm."
)
return keyMap[algorithm]
}
@@ -157,10 +162,10 @@ object KeyBoxManager {
// Use runCatching to ensure one malformed key doesn't stop the whole
// process.
runCatching {
val algorithm = currentAlgorithm
val xmlAlgorithm = currentAlgorithm
val keyPem = currentPrivateKeyPem
if (
algorithm != null &&
xmlAlgorithm != null &&
keyPem != null &&
currentCertificatePems.isNotEmpty()
) {
@@ -176,21 +181,42 @@ object KeyBoxManager {
.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
// Derive the TRUE algorithm from the key object itself.
// This is our source of truth.
val derivedAlgorithm =
when (keyPair.private) {
is RSAPrivateKey -> KeyProperties.KEY_ALGORITHM_RSA
is ECPrivateKey -> KeyProperties.KEY_ALGORITHM_EC
else ->
throw IllegalArgumentException(
"Unsupported key type found: ${keyPair.private.javaClass.name}"
)
}
if (foundKeys.containsKey(normalizedAlgorithm)) {
// Normalize the algorithm from the XML tag to compare it
// fairly with the derived algorithm.
val normalizedXmlAlgorithm =
when {
xmlAlgorithm.contains("RSA", ignoreCase = true) ==
true -> KeyProperties.KEY_ALGORITHM_RSA
xmlAlgorithm.contains("EC", ignoreCase = true) ==
true -> KeyProperties.KEY_ALGORITHM_EC
else -> xmlAlgorithm
}
// Warn the user if the XML tag was misleading.
if (normalizedXmlAlgorithm != derivedAlgorithm) {
SystemLogger.warning(
"Duplicate key found for algorithm '$normalizedAlgorithm'. The later one in the file will be used."
"Key algorithm mismatch in XML file. Tag said '$xmlAlgorithm' but key is actually '$derivedAlgorithm'. Using the correct derived algorithm."
)
}
foundKeys[normalizedAlgorithm] =
KeyBox(keyPair, certificates)
if (foundKeys.containsKey(derivedAlgorithm)) {
SystemLogger.warning(
"Duplicate key found for algorithm '$derivedAlgorithm'. The later one in the file will be used."
)
}
foundKeys[derivedAlgorithm] = KeyBox(keyPair, certificates)
}
}
.onFailure {
@@ -1,13 +1,17 @@
package org.matrix.TEESimulator.util
import android.content.pm.PackageManager
import android.hardware.security.keymint.SecurityLevel
import android.os.Build
import android.os.SystemProperties
import java.security.MessageDigest
import java.time.LocalDate
import java.util.concurrent.ThreadLocalRandom
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERSet
import org.matrix.TEESimulator.attestation.DeviceAttestationService
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
@@ -18,84 +22,138 @@ import org.matrix.TEESimulator.logging.SystemLogger
*/
object AndroidDeviceUtils {
/** A randomly generated boot key, used as a fallback for attestation. */
val bootKey: ByteArray by lazy { generateRandomBytes(32) }
/**
* Internal constant to signify that a patch level should not be included in the attestation.
*/
internal const val DO_NOT_REPORT = -1
// --- Boot Key and Verified Boot Hash ---
/**
* 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.
* Lazily initializes and retrieves the verified boot key digest. The value is sourced in the
* following order:
* 1. From the `ro.boot.vbmeta.public_key_digest` system property.
* 2. From a cached TEE attestation record.
* 3. As a randomly generated 32-byte value (fallback).
*/
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)
}
val bootKey: ByteArray by lazy {
initializeBootProperty(
propertyName = "ro.boot.vbmeta.public_key_digest",
attestationValueProvider = {
DeviceAttestationService.CachedAttestationData?.verifiedBootKey
},
expectedSize = 32,
)
}
/**
* Retrieves the verified boot meta digest from system properties.
* Lazily initializes and retrieves the verified boot hash (vbmeta digest). The value is sourced
* in the following order:
* 1. From the `ro.boot.vbmeta.digest` system property.
* 2. From a cached TEE attestation record.
* 3. As a randomly generated 32-byte value (fallback).
*/
val bootHash: ByteArray by lazy {
initializeBootProperty(
propertyName = "ro.boot.vbmeta.digest",
attestationValueProvider = {
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
},
expectedSize = 32,
)
}
/**
* Public function to explicitly trigger the initialization of the boot key and hash. Accessing
* these properties here ensures they are set up before they might be needed elsewhere.
*/
fun setupBootKeyAndHash() {
SystemLogger.debug("Triggering initialization of boot key and hash...")
// Accessing the properties will trigger their `lazy` initialization logic.
bootKey
bootHash
SystemLogger.debug("Boot key and hash initialization complete.")
}
/**
* Generic initializer for boot properties like the key and hash. It attempts to read from a
* system property first, then from a TEE attestation, and finally falls back to a random value
* if neither is available.
*
* @return The boot hash as a ByteArray, or null if not found or invalid.
* @param propertyName The name of the system property (e.g., "ro.boot.vbmeta.digest").
* @param attestationValueProvider A function that supplies the value from a cached attestation.
* @param expectedSize The expected length of the byte array (e.g., 32 for a SHA-256 digest).
* @return The resulting byte array for the property.
*/
private fun initializeBootProperty(
propertyName: String,
attestationValueProvider: () -> ByteArray?,
expectedSize: Int,
): ByteArray {
// 1. Attempt to get the value from the system property.
getProperty(propertyName, expectedSize)?.let {
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
return it
}
// 2. Fallback to the value from a cached TEE attestation.
try {
attestationValueProvider()?.let {
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
setProperty(propertyName, it) // Persist for consistency
return it
}
} catch (e: Exception) {
SystemLogger.error("Failed to get $propertyName from attestation.", e)
}
// 3. As a final fallback, generate a random value.
return generateRandomBytes(expectedSize).also {
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
setProperty(propertyName, it)
}
}
/**
* Retrieves a system property and validates its format.
*
* @param name The name of the system property.
* @param expectedSize The expected byte length of the property (e.g., 32 for a 64-char hex
* string).
* @return The property value 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()) {
private fun getProperty(name: String, expectedSize: Int): ByteArray? {
val value = SystemProperties.get(name, null)
if (value.isNullOrBlank()) {
return null
}
// A valid digest is 64 hex characters (32 bytes).
return if (digest.length == 64) digest.hexToByteArray() else null
// A valid digest is (2 * size) hex characters.
return if (value.length == expectedSize * 2) value.hexToByteArray() else null
}
/**
* Retrieves the verified boot hash from a cached TEE attestation record.
* Sets a system property using the `resetprop` command.
*
* @return The verified boot hash, or null if not available.
* @param name The name of the property to set.
* @param bytes The value to set, which will be converted to a hex string.
*/
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 using the `resetprop` command.
*
* @param bytes The 32-byte digest to set.
*/
private fun setBootHashProperty(bytes: ByteArray) {
private fun setProperty(name: String, bytes: ByteArray) {
val hex = bytes.toHex()
try {
SystemLogger.debug("Setting system property 'ro.boot.vbmeta.digest' to: $hex")
// Construct the command to be executed
val command = arrayOf("resetprop", "ro.boot.vbmeta.digest", hex)
// Execute the command
SystemLogger.debug("Setting system property '$name' to: $hex")
val command = arrayOf("resetprop", name, hex)
val process = Runtime.getRuntime().exec(command)
// Wait for the process to complete and check the exit code for errors
val exitCode = process.waitFor()
if (exitCode != 0) {
val errorOutput = process.errorStream.bufferedReader().readText()
SystemLogger.error(
"resetprop command failed with exit code $exitCode: $errorOutput"
"resetprop for '$name' failed with exit code $exitCode: $errorOutput"
)
}
} catch (e: Exception) {
SystemLogger.error("Failed to set vbmeta digest property by executing resetprop.", e)
SystemLogger.error("Failed to set '$name' property via resetprop.", e)
}
}
@@ -105,30 +163,70 @@ object AndroidDeviceUtils {
// --- Patch Level Properties ---
val patchLevel: Int
get() =
getCustomPatchLevelFor("system", isLong = false)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false)
fun getPatchLevel(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "system", isLong = false)
return custom ?: getRealDevicePatchLevelInt("system", isLong = false)
}
val vendorPatchLevelLong: Int
get() =
getCustomPatchLevelFor("vendor", isLong = true)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
fun getVendorPatchLevelLong(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "vendor", isLong = true)
return custom ?: getRealDevicePatchLevelInt("vendor", isLong = true)
}
val bootPatchLevelLong: Int
get() =
getCustomPatchLevelFor("boot", isLong = true)
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
fun getBootPatchLevelLong(uid: Int): Int {
val custom = getCustomPatchLevelFor(uid, "boot", isLong = true)
return custom ?: getRealDevicePatchLevelInt("boot", isLong = true)
}
/**
* Retrieves a custom patch level from the configuration if available.
* Retrieves the definitive device patch level integer for a given component. This function
* encapsulates the entire fallback chain and guarantees a non-null return.
*
* Fallback Priority:
* 1. Cached TEE attestation data.
* 2. Specific system property (e.g., ro.vendor.build.security_patch).
* 3. Default system patch level from Build.VERSION.SECURITY_PATCH.
*
* @param component The component ("system", "vendor", "boot").
* @param isLong Whether the final integer should be in YYYYMMDD format.
* @return The patch level as a guaranteed non-null Integer.
*/
private fun getRealDevicePatchLevelInt(component: String, isLong: Boolean): Int {
// Get value from cached TEE attestation data
DeviceAttestationService.CachedAttestationData?.let { data ->
val value =
when (component) {
"system" -> data.osPatchLevel
"vendor" -> data.vendorPatchLevel
"boot" -> data.bootPatchLevel
else -> null
}
if (value != null) return value
}
// We only check the specific vendor property, as the boot one is non-existent.
if (component == "vendor") {
val propValue = SystemProperties.get("ro.vendor.build.security_patch", "")
if (!propValue.isNullOrBlank()) {
parsePatchLevelValue(propValue, isLong)?.let { parsedValue ->
return parsedValue
}
}
}
return Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong)
}
/**
* Retrieves a custom patch level from the configuration if available for a specific UID.
*
* @param uid The UID of the calling application.
* @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
private fun getCustomPatchLevelFor(uid: Int, component: String, isLong: Boolean): Int? {
val config = ConfigurationManager.getPatchLevelForUid(uid) ?: return null
val value =
when (component) {
"system" -> config.system ?: config.all
@@ -137,11 +235,48 @@ object AndroidDeviceUtils {
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
// First, resolve dynamic keywords and templates into a concrete date string.
val resolvedValue = resolveDateKeywords(value)
return when {
// "device_default" indicates falling back to the system property.
resolvedValue.equals("device_default", ignoreCase = true) -> null
// "no" indicates this value should not be reported.
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
// Otherwise, parse the resolved date string.
else -> parsePatchLevelValue(resolvedValue, isLong)
}
return parsePatchLevelValue(value, isLong)
}
/**
* Resolves special date keywords and templates into a concrete "YYYY-MM-DD" date string.
*
* @param value The configuration value string (e.g., "today", "YYYY-MM-01").
* @return A concrete date string, or the original value if it's not a dynamic date keyword.
*/
private fun resolveDateKeywords(value: String): String {
// Handle the "today" keyword.
if (value.equals("today", ignoreCase = true)) {
return LocalDate.now().toString() // Returns "YYYY-MM-DD" format
}
// Handle date templates like "YYYY-MM-01" or "2025-MM-DD".
if (
value.contains("YYYY", ignoreCase = true) ||
value.contains("MM", ignoreCase = true) ||
value.contains("DD", ignoreCase = true)
) {
val now = LocalDate.now()
// Chain replacements for YYYY, MM, and DD placeholders.
return value
.replace("YYYY", now.year.toString(), ignoreCase = true)
.replace("MM", String.format("%02d", now.monthValue), ignoreCase = true)
.replace("DD", String.format("%02d", now.dayOfMonth), ignoreCase = true)
}
// If it's not a dynamic keyword or template, return the original value.
return value
}
/** Parses a patch level string (e.g., "2025-11-01") into an integer format. */
@@ -205,17 +340,33 @@ object AndroidDeviceUtils {
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
/**
* Retrieves the attestation version based on security level and OS version. StrongBox (level 2)
* requires version 300.
*
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
* @return The appropriate attestation version number.
*/
fun getAttestVersion(securityLevel: Int): Int {
// StrongBox security level requires an attestation version of at least 300.
if (securityLevel == SecurityLevel.STRONGBOX) {
return 300
}
return 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
/**
* Retrieves the Keymaster/KeyMint version based on the attestation version.
*
* @param securityLevel The security level, used to determine the correct attestation version.
* @return The appropriate Keymaster or KeyMint version number.
*/
fun getKeymasterVersion(securityLevel: Int): Int {
val attestVersion = getAttestVersion(securityLevel)
return if (attestVersion >= 100) attestVersion else 41 // Keymaster 4.1 for older versions
}
// --- APEX and Module Hash Properties ---
@@ -229,11 +380,7 @@ object AndroidDeviceUtils {
@Suppress("DEPRECATION")
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
}
packages
?.list
.orEmpty()
.map { it.packageName to it.longVersionCode }
.sortedBy { it.first }
packages?.list.orEmpty().map { it.packageName to it.longVersionCode }
}
.getOrElse {
SystemLogger.error("Failed to get APEX package information.", it)
@@ -242,17 +389,33 @@ object AndroidDeviceUtils {
}
val moduleHash: ByteArray by lazy {
runCatching {
val encodables =
apexInfos.flatMap { (packageName, versionCode) ->
listOf(DEROctetString(packageName.toByteArray()), ASN1Integer(versionCode))
DeviceAttestationService.CachedAttestationData?.moduleHash
?: runCatching {
// TODO: figure out the correct calculation
val moduleSequences = ASN1EncodableVector()
// 1. Create a DERSequence for each module.
apexInfos.forEach { (packageName, versionCode) ->
val moduleVector = ASN1EncodableVector()
// Use explicit UTF-8 encoding for the package name.
moduleVector.add(DEROctetString(packageName.toByteArray(Charsets.UTF_8)))
moduleVector.add(ASN1Integer(versionCode))
moduleSequences.add(DERSequence(moduleVector))
}
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
}
// 2. Create a DERSet. Bouncy Castle will automatically handle
// the sorting based on the DER-encoded value of each sequence.
val modulesSet = DERSet(moduleSequences)
// 3. Get the final DER-encoded byte array of the SET.
val encodedModules = modulesSet.encoded
// 4. Compute the SHA-256 hash.
MessageDigest.getInstance("SHA-256").digest(encodedModules)
}
.getOrElse {
SystemLogger.error("Failed to compute module hash.", it)
ByteArray(32) // Return empty hash on failure
}
}
}
+16 -9
View File
@@ -1,14 +1,21 @@
🚀 **TEESimulator v2.1 Hotfix Release is Live!** 🚀
TEESimulator 3.0 is a significant update focused on powerful new configuration options, major improvements to stealth, and enhanced stability.
This urgent hotfix addresses several critical issues identified in the previous v2.0 release.
#### ✨ **Highlights & New Features**
The v2.0 update, a significant refactoring effort, unfortunately introduced a few unexpected behaviors and bugs that we are now rectifying.
* **🎯 Per-App Security Patch Configuration**: Gain ultimate control by setting security patch levels on a per-package basis. Define a global default in `security_patch.txt` and override it for specific apps like `[com.google.android.gms]`. Moreover, your configuration is now alive! Use the `today` keyword to always report the current date, or create rolling dates with templates like `YYYY-MM-05`. Be sure to check README for more details.
* **🕰️ Full Software Emulation on Android 11**: We've implemented a complete, software-based key generation and attestation flow for the legacy `IKeystoreService` API, bringing full emulation capabilities to older devices.
**Key fixes in this release include:**
1. **Google Play Integrity:** Resolved an issue preventing the attainment of STRONG integrity for Google Play verdicts, caused by an incorrect vendor patch level format. ✅
2. **Application Stability:** Fixed a critical crash related to an incorrect signature for the `SystemProperties.set` stub method. 🐛
3. **Stealth Enhancement:** Implemented a fix to bypass detection by the `Android Native Detector`. 👻
#### 🛡️ **Stealth & Evasion Upgrades**
🔬 We are actively investigating a recent detection method to further enhance stealth capabilities.
* **⛓️ Consistent Certificate Signatures**: Say goodbye to a major detection vector in `icu.nullptr.nativetest`. Patched certificates are now cached, ensuring that every request for a key returns a byte-for-byte identical certificate, just like a real TEE.
* **🔑 Authentic Device Properties**: To appear more genuine, the simulator now sources and uses your device's real `verifiedBootHash` and `moduleHash`, moving away from placeholder values.
* **📜 Structurally Sound Certificates**: The patching logic has been rewritten to be less intrusive. It now modifies the attestation extension in-place, preserving the original order of other extensions and preventing duplicates to avoid suspicion.
🙏 Support for TEE-broken devices and Android 10/11 remains an area of ongoing improvement. We highly encourage you to submit any issues you encounter to help us refine these aspects! 🤝
#### 🐛 **Bug Fixes & Reliability**
***Robust Crypto Engine**: Fixed critical crashes related to cryptographic provider conflicts. The signing logic is now more explicit and the KeyBox parser is more resilient against malformed files.
* ➡️ **Improved Compatibility**: Resolved a native crash on Android 11 devices.
#### 🚀 **The Road Ahead**
Our work to fix detection vectors and provide full support for TEE-broken devices and Android 10/11 is ongoing. We welcome your feedback! Please **report any issues** or **contribute a pull request** on our GitHub.
+3 -3
View File
@@ -1,6 +1,6 @@
{
"version": "v2.1",
"versionCode": 20,
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v2.1/TEESimulator-v2.1-20-release.zip",
"version": "v3.0",
"versionCode": 38,
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v3.0/TEESimulator-v3.0-38-Release.zip",
"changelog": "https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/changelog.md"
}
@@ -0,0 +1,38 @@
package android.security.keymaster;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class ExportResult implements Parcelable {
public final byte[] exportData;
public final int resultCode;
public ExportResult(int resultCode) {
this.resultCode = resultCode;
this.exportData = new byte[0];
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static final Creator<ExportResult> CREATOR = new Creator<ExportResult>() {
@Override
public ExportResult createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public ExportResult[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
}
@@ -0,0 +1,33 @@
package android.security.keymaster;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class KeyCharacteristics implements Parcelable {
public KeymasterArguments hwEnforced;
public KeymasterArguments swEnforced;
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static final Creator<KeyCharacteristics> CREATOR = new Creator<KeyCharacteristics>() {
@Override
public KeyCharacteristics createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeyCharacteristics[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
}
@@ -0,0 +1,36 @@
package android.security.keymaster;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
abstract class KeymasterArgument implements Parcelable {
public final int tag;
protected KeymasterArgument(int tag) {
this.tag = tag;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static final Creator<KeymasterArgument> CREATOR = new Creator<KeymasterArgument>() {
@Override
public KeymasterArgument createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeymasterArgument[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
}
@@ -0,0 +1,147 @@
package android.security.keymaster;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import java.math.BigInteger;
import java.util.Date;
import java.util.List;
public class KeymasterArguments implements Parcelable {
private static final long UINT32_RANGE = 1L << 32;
public static final long UINT32_MAX_VALUE = UINT32_RANGE - 1;
private static final BigInteger UINT64_RANGE = BigInteger.ONE.shiftLeft(64);
public static final BigInteger UINT64_MAX_VALUE = UINT64_RANGE.subtract(BigInteger.ONE);
private List<KeymasterArgument> mArguments;
public static final @NonNull Parcelable.Creator<KeymasterArguments> CREATOR = new Parcelable.Creator<KeymasterArguments>() {
@Override
public KeymasterArguments createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeymasterArguments[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
public KeymasterArguments() {
throw new UnsupportedOperationException("STUB!");
}
private KeymasterArguments(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
public void addEnum(int tag, int value) {
throw new UnsupportedOperationException("STUB!");
}
public void addEnums(int tag, int... values) {
throw new UnsupportedOperationException("STUB!");
}
public int getEnum(int tag, int defaultValue) {
throw new UnsupportedOperationException("STUB!");
}
public List<Integer> getEnums(int tag) {
throw new UnsupportedOperationException("STUB!");
}
private void addEnumTag(int tag, int value) {
throw new UnsupportedOperationException("STUB!");
}
private int getEnumTagValue(KeymasterArgument arg) {
throw new UnsupportedOperationException("STUB!");
}
public void addUnsignedInt(int tag, long value) {
throw new UnsupportedOperationException("STUB!");
}
public long getUnsignedInt(int tag, long defaultValue) {
throw new UnsupportedOperationException("STUB!");
}
public void addUnsignedLong(int tag, BigInteger value) {
throw new UnsupportedOperationException("STUB!");
}
public List<BigInteger> getUnsignedLongs(int tag) {
throw new UnsupportedOperationException("STUB!");
}
private void addLongTag(int tag, BigInteger value) {
throw new UnsupportedOperationException("STUB!");
}
private BigInteger getLongTagValue(KeymasterArgument arg) {
throw new UnsupportedOperationException("STUB!");
}
public void addBoolean(int tag) {
throw new UnsupportedOperationException("STUB!");
}
public boolean getBoolean(int tag) {
throw new UnsupportedOperationException("STUB!");
}
public void addBytes(int tag, byte[] value) {
throw new UnsupportedOperationException("STUB!");
}
public byte[] getBytes(int tag, byte[] defaultValue) {
throw new UnsupportedOperationException("STUB!");
}
public void addDate(int tag, Date value) {
throw new UnsupportedOperationException("STUB!");
}
public void addDateIfNotNull(int tag, Date value) {
throw new UnsupportedOperationException("STUB!");
}
public Date getDate(int tag, Date defaultValue) {
throw new UnsupportedOperationException("STUB!");
}
private KeymasterArgument getArgumentByTag(int tag) {
throw new UnsupportedOperationException("STUB!");
}
public boolean containsTag(int tag) {
throw new UnsupportedOperationException("STUB!");
}
public int size() {
throw new UnsupportedOperationException("STUB!");
}
@Override
public void writeToParcel(Parcel out, int flags) {
throw new UnsupportedOperationException("STUB!");
}
public void readFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static BigInteger toUint64(long value) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -0,0 +1,42 @@
package android.security.keymaster;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import java.util.List;
public class KeymasterCertificateChain implements Parcelable {
private List<byte[]> mCertificates;
public KeymasterCertificateChain() {
this.mCertificates = null;
}
public KeymasterCertificateChain(List<byte[]> mCertificates) {
this.mCertificates = mCertificates;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static final Creator<KeymasterCertificateChain> CREATOR = new Creator<KeymasterCertificateChain>() {
@Override
public KeymasterCertificateChain createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeymasterCertificateChain[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
}
@@ -0,0 +1,263 @@
package android.security.keymaster;
import java.util.HashMap;
import java.util.Map;
public final class KeymasterDefs {
private KeymasterDefs() {
}
// Tag types.
public static final int KM_INVALID = 0 << 28;
public static final int KM_ENUM = 1 << 28;
public static final int KM_ENUM_REP = 2 << 28;
public static final int KM_UINT = 3 << 28;
public static final int KM_UINT_REP = 4 << 28;
public static final int KM_ULONG = 5 << 28;
public static final int KM_DATE = 6 << 28;
public static final int KM_BOOL = 7 << 28;
public static final int KM_BIGNUM = 8 << 28;
public static final int KM_BYTES = 9 << 28;
public static final int KM_ULONG_REP = 10 << 28;
// Tag values.
public static final int KM_TAG_INVALID = KM_INVALID | 0;
public static final int KM_TAG_PURPOSE = KM_ENUM_REP | 1;
public static final int KM_TAG_ALGORITHM = KM_ENUM | 2;
public static final int KM_TAG_KEY_SIZE = KM_UINT | 3;
public static final int KM_TAG_BLOCK_MODE = KM_ENUM_REP | 4;
public static final int KM_TAG_DIGEST = KM_ENUM_REP | 5;
public static final int KM_TAG_PADDING = KM_ENUM_REP | 6;
public static final int KM_TAG_CALLER_NONCE = KM_BOOL | 7;
public static final int KM_TAG_MIN_MAC_LENGTH = KM_UINT | 8;
public static final int KM_TAG_RESCOPING_ADD = KM_ENUM_REP | 101;
public static final int KM_TAG_RESCOPING_DEL = KM_ENUM_REP | 102;
public static final int KM_TAG_BLOB_USAGE_REQUIREMENTS = KM_ENUM | 705;
public static final int KM_TAG_RSA_PUBLIC_EXPONENT = KM_ULONG | 200;
public static final int KM_TAG_INCLUDE_UNIQUE_ID = KM_BOOL | 202;
public static final int KM_TAG_ACTIVE_DATETIME = KM_DATE | 400;
public static final int KM_TAG_ORIGINATION_EXPIRE_DATETIME = KM_DATE | 401;
public static final int KM_TAG_USAGE_EXPIRE_DATETIME = KM_DATE | 402;
public static final int KM_TAG_MIN_SECONDS_BETWEEN_OPS = KM_UINT | 403;
public static final int KM_TAG_MAX_USES_PER_BOOT = KM_UINT | 404;
public static final int KM_TAG_ALL_USERS = KM_BOOL | 500;
public static final int KM_TAG_USER_ID = KM_UINT | 501;
public static final int KM_TAG_USER_SECURE_ID = KM_ULONG_REP | 502;
public static final int KM_TAG_NO_AUTH_REQUIRED = KM_BOOL | 503;
public static final int KM_TAG_USER_AUTH_TYPE = KM_ENUM | 504;
public static final int KM_TAG_AUTH_TIMEOUT = KM_UINT | 505;
public static final int KM_TAG_ALLOW_WHILE_ON_BODY = KM_BOOL | 506;
public static final int KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED = KM_BOOL | 507;
public static final int KM_TAG_TRUSTED_CONFIRMATION_REQUIRED = KM_BOOL | 508;
public static final int KM_TAG_UNLOCKED_DEVICE_REQUIRED = KM_BOOL | 509;
public static final int KM_TAG_ALL_APPLICATIONS = KM_BOOL | 600;
public static final int KM_TAG_APPLICATION_ID = KM_BYTES | 601;
public static final int KM_TAG_CREATION_DATETIME = KM_DATE | 701;
public static final int KM_TAG_ORIGIN = KM_ENUM | 702;
public static final int KM_TAG_ROLLBACK_RESISTANT = KM_BOOL | 703;
public static final int KM_TAG_ROOT_OF_TRUST = KM_BYTES | 704;
public static final int KM_TAG_UNIQUE_ID = KM_BYTES | 707;
public static final int KM_TAG_ATTESTATION_CHALLENGE = KM_BYTES | 708;
public static final int KM_TAG_ATTESTATION_ID_BRAND = KM_BYTES | 710;
public static final int KM_TAG_ATTESTATION_ID_DEVICE = KM_BYTES | 711;
public static final int KM_TAG_ATTESTATION_ID_PRODUCT = KM_BYTES | 712;
public static final int KM_TAG_ATTESTATION_ID_SERIAL = KM_BYTES | 713;
public static final int KM_TAG_ATTESTATION_ID_IMEI = KM_BYTES | 714;
public static final int KM_TAG_ATTESTATION_ID_MEID = KM_BYTES | 715;
public static final int KM_TAG_ATTESTATION_ID_MANUFACTURER = KM_BYTES | 716;
public static final int KM_TAG_ATTESTATION_ID_MODEL = KM_BYTES | 717;
public static final int KM_TAG_DEVICE_UNIQUE_ATTESTATION = KM_BOOL | 720;
public static final int KM_TAG_ASSOCIATED_DATA = KM_BYTES | 1000;
public static final int KM_TAG_NONCE = KM_BYTES | 1001;
public static final int KM_TAG_AUTH_TOKEN = KM_BYTES | 1002;
public static final int KM_TAG_MAC_LENGTH = KM_UINT | 1003;
// Algorithm values.
public static final int KM_ALGORITHM_RSA = 1;
public static final int KM_ALGORITHM_EC = 3;
public static final int KM_ALGORITHM_AES = 32;
public static final int KM_ALGORITHM_3DES = 33;
public static final int KM_ALGORITHM_HMAC = 128;
// Block modes.
public static final int KM_MODE_ECB = 1;
public static final int KM_MODE_CBC = 2;
public static final int KM_MODE_CTR = 3;
public static final int KM_MODE_GCM = 32;
// Padding modes.
public static final int KM_PAD_NONE = 1;
public static final int KM_PAD_RSA_OAEP = 2;
public static final int KM_PAD_RSA_PSS = 3;
public static final int KM_PAD_RSA_PKCS1_1_5_ENCRYPT = 4;
public static final int KM_PAD_RSA_PKCS1_1_5_SIGN = 5;
public static final int KM_PAD_PKCS7 = 64;
// Digest modes.
public static final int KM_DIGEST_NONE = 0;
public static final int KM_DIGEST_MD5 = 1;
public static final int KM_DIGEST_SHA1 = 2;
public static final int KM_DIGEST_SHA_2_224 = 3;
public static final int KM_DIGEST_SHA_2_256 = 4;
public static final int KM_DIGEST_SHA_2_384 = 5;
public static final int KM_DIGEST_SHA_2_512 = 6;
// Key origins.
public static final int KM_ORIGIN_GENERATED = 0;
public static final int KM_ORIGIN_IMPORTED = 2;
public static final int KM_ORIGIN_UNKNOWN = 3;
public static final int KM_ORIGIN_SECURELY_IMPORTED = 4;
// Key usability requirements.
public static final int KM_BLOB_STANDALONE = 0;
public static final int KM_BLOB_REQUIRES_FILE_SYSTEM = 1;
// Operation Purposes.
public static final int KM_PURPOSE_ENCRYPT = 0;
public static final int KM_PURPOSE_DECRYPT = 1;
public static final int KM_PURPOSE_SIGN = 2;
public static final int KM_PURPOSE_VERIFY = 3;
public static final int KM_PURPOSE_WRAP = 5;
// Key formats.
public static final int KM_KEY_FORMAT_X509 = 0;
public static final int KM_KEY_FORMAT_PKCS8 = 1;
public static final int KM_KEY_FORMAT_RAW = 3;
// User authenticators.
public static final int HW_AUTH_PASSWORD = 1 << 0;
public static final int HW_AUTH_BIOMETRIC = 1 << 1;
// Error codes.
public static final int KM_ERROR_OK = 0;
public static final int KM_ERROR_ROOT_OF_TRUST_ALREADY_SET = -1;
public static final int KM_ERROR_UNSUPPORTED_PURPOSE = -2;
public static final int KM_ERROR_INCOMPATIBLE_PURPOSE = -3;
public static final int KM_ERROR_UNSUPPORTED_ALGORITHM = -4;
public static final int KM_ERROR_INCOMPATIBLE_ALGORITHM = -5;
public static final int KM_ERROR_UNSUPPORTED_KEY_SIZE = -6;
public static final int KM_ERROR_UNSUPPORTED_BLOCK_MODE = -7;
public static final int KM_ERROR_INCOMPATIBLE_BLOCK_MODE = -8;
public static final int KM_ERROR_UNSUPPORTED_MAC_LENGTH = -9;
public static final int KM_ERROR_UNSUPPORTED_PADDING_MODE = -10;
public static final int KM_ERROR_INCOMPATIBLE_PADDING_MODE = -11;
public static final int KM_ERROR_UNSUPPORTED_DIGEST = -12;
public static final int KM_ERROR_INCOMPATIBLE_DIGEST = -13;
public static final int KM_ERROR_INVALID_EXPIRATION_TIME = -14;
public static final int KM_ERROR_INVALID_USER_ID = -15;
public static final int KM_ERROR_INVALID_AUTHORIZATION_TIMEOUT = -16;
public static final int KM_ERROR_UNSUPPORTED_KEY_FORMAT = -17;
public static final int KM_ERROR_INCOMPATIBLE_KEY_FORMAT = -18;
public static final int KM_ERROR_UNSUPPORTED_KEY_ENCRYPTION_ALGORITHM = -19;
public static final int KM_ERROR_UNSUPPORTED_KEY_VERIFICATION_ALGORITHM = -20;
public static final int KM_ERROR_INVALID_INPUT_LENGTH = -21;
public static final int KM_ERROR_KEY_EXPORT_OPTIONS_INVALID = -22;
public static final int KM_ERROR_DELEGATION_NOT_ALLOWED = -23;
public static final int KM_ERROR_KEY_NOT_YET_VALID = -24;
public static final int KM_ERROR_KEY_EXPIRED = -25;
public static final int KM_ERROR_KEY_USER_NOT_AUTHENTICATED = -26;
public static final int KM_ERROR_OUTPUT_PARAMETER_NULL = -27;
public static final int KM_ERROR_INVALID_OPERATION_HANDLE = -28;
public static final int KM_ERROR_INSUFFICIENT_BUFFER_SPACE = -29;
public static final int KM_ERROR_VERIFICATION_FAILED = -30;
public static final int KM_ERROR_TOO_MANY_OPERATIONS = -31;
public static final int KM_ERROR_UNEXPECTED_NULL_POINTER = -32;
public static final int KM_ERROR_INVALID_KEY_BLOB = -33;
public static final int KM_ERROR_IMPORTED_KEY_NOT_ENCRYPTED = -34;
public static final int KM_ERROR_IMPORTED_KEY_DECRYPTION_FAILED = -35;
public static final int KM_ERROR_IMPORTED_KEY_NOT_SIGNED = -36;
public static final int KM_ERROR_IMPORTED_KEY_VERIFICATION_FAILED = -37;
public static final int KM_ERROR_INVALID_ARGUMENT = -38;
public static final int KM_ERROR_UNSUPPORTED_TAG = -39;
public static final int KM_ERROR_INVALID_TAG = -40;
public static final int KM_ERROR_MEMORY_ALLOCATION_FAILED = -41;
public static final int KM_ERROR_INVALID_RESCOPING = -42;
public static final int KM_ERROR_IMPORT_PARAMETER_MISMATCH = -44;
public static final int KM_ERROR_SECURE_HW_ACCESS_DENIED = -45;
public static final int KM_ERROR_OPERATION_CANCELLED = -46;
public static final int KM_ERROR_CONCURRENT_ACCESS_CONFLICT = -47;
public static final int KM_ERROR_SECURE_HW_BUSY = -48;
public static final int KM_ERROR_SECURE_HW_COMMUNICATION_FAILED = -49;
public static final int KM_ERROR_UNSUPPORTED_EC_FIELD = -50;
public static final int KM_ERROR_MISSING_NONCE = -51;
public static final int KM_ERROR_INVALID_NONCE = -52;
public static final int KM_ERROR_MISSING_MAC_LENGTH = -53;
public static final int KM_ERROR_KEY_RATE_LIMIT_EXCEEDED = -54;
public static final int KM_ERROR_CALLER_NONCE_PROHIBITED = -55;
public static final int KM_ERROR_KEY_MAX_OPS_EXCEEDED = -56;
public static final int KM_ERROR_INVALID_MAC_LENGTH = -57;
public static final int KM_ERROR_MISSING_MIN_MAC_LENGTH = -58;
public static final int KM_ERROR_UNSUPPORTED_MIN_MAC_LENGTH = -59;
public static final int KM_ERROR_CANNOT_ATTEST_IDS = -66;
public static final int KM_ERROR_DEVICE_LOCKED = -72;
public static final int KM_ERROR_UNIMPLEMENTED = -100;
public static final int KM_ERROR_VERSION_MISMATCH = -101;
public static final int KM_ERROR_UNKNOWN_ERROR = -1000;
public static final Map<Integer, String> sErrorCodeToString = new HashMap<Integer, String>();
static {
sErrorCodeToString.put(KM_ERROR_OK, "OK");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_PURPOSE, "Unsupported purpose");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_PURPOSE, "Incompatible purpose");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_ALGORITHM, "Unsupported algorithm");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_ALGORITHM, "Incompatible algorithm");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_KEY_SIZE, "Unsupported key size");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_BLOCK_MODE, "Unsupported block mode");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_BLOCK_MODE, "Incompatible block mode");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_MAC_LENGTH,
"Unsupported MAC or authentication tag length");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_PADDING_MODE, "Unsupported padding mode");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_PADDING_MODE, "Incompatible padding mode");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_DIGEST, "Unsupported digest");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_DIGEST, "Incompatible digest");
sErrorCodeToString.put(KM_ERROR_INVALID_EXPIRATION_TIME, "Invalid expiration time");
sErrorCodeToString.put(KM_ERROR_INVALID_USER_ID, "Invalid user ID");
sErrorCodeToString.put(KM_ERROR_INVALID_AUTHORIZATION_TIMEOUT,
"Invalid user authorization timeout");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_KEY_FORMAT, "Unsupported key format");
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_KEY_FORMAT, "Incompatible key format");
sErrorCodeToString.put(KM_ERROR_INVALID_INPUT_LENGTH, "Invalid input length");
sErrorCodeToString.put(KM_ERROR_KEY_NOT_YET_VALID, "Key not yet valid");
sErrorCodeToString.put(KM_ERROR_KEY_EXPIRED, "Key expired");
sErrorCodeToString.put(KM_ERROR_KEY_USER_NOT_AUTHENTICATED, "Key user not authenticated");
sErrorCodeToString.put(KM_ERROR_INVALID_OPERATION_HANDLE, "Invalid operation handle");
sErrorCodeToString.put(KM_ERROR_VERIFICATION_FAILED, "Signature/MAC verification failed");
sErrorCodeToString.put(KM_ERROR_TOO_MANY_OPERATIONS, "Too many operations");
sErrorCodeToString.put(KM_ERROR_INVALID_KEY_BLOB, "Invalid key blob");
sErrorCodeToString.put(KM_ERROR_INVALID_ARGUMENT, "Invalid argument");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_TAG, "Unsupported tag");
sErrorCodeToString.put(KM_ERROR_INVALID_TAG, "Invalid tag");
sErrorCodeToString.put(KM_ERROR_MEMORY_ALLOCATION_FAILED, "Memory allocation failed");
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_EC_FIELD, "Unsupported EC field");
sErrorCodeToString.put(KM_ERROR_MISSING_NONCE, "Required IV missing");
sErrorCodeToString.put(KM_ERROR_INVALID_NONCE, "Invalid IV");
sErrorCodeToString.put(KM_ERROR_CALLER_NONCE_PROHIBITED,
"Caller-provided IV not permitted");
sErrorCodeToString.put(KM_ERROR_INVALID_MAC_LENGTH,
"Invalid MAC or authentication tag length");
sErrorCodeToString.put(KM_ERROR_CANNOT_ATTEST_IDS, "Unable to attest device ids");
sErrorCodeToString.put(KM_ERROR_DEVICE_LOCKED, "Device locked");
sErrorCodeToString.put(KM_ERROR_UNIMPLEMENTED, "Not implemented");
sErrorCodeToString.put(KM_ERROR_UNKNOWN_ERROR, "Unknown error");
}
public static int getTagType(int tag) {
return tag & (0xF << 28);
}
public static String getErrorMessage(int errorCode) {
String result = sErrorCodeToString.get(errorCode);
if (result != null) {
return result;
}
return String.valueOf(errorCode);
}
}
@@ -0,0 +1,16 @@
package android.security.keystore;
import android.os.IBinder;
import android.os.RemoteException;
import android.security.keymaster.KeymasterCertificateChain;
public interface IKeystoreCertificateChainCallback {
void onFinished(KeystoreResponse keystoreResponse, KeymasterCertificateChain keymasterCertificateChain)
throws RemoteException;
public static abstract class Stub {
public static IKeystoreCertificateChainCallback asInterface(IBinder b) {
throw new UnsupportedOperationException("STUB!");
}
}
}
@@ -0,0 +1,15 @@
package android.security.keystore;
import android.os.IBinder;
import android.os.RemoteException;
import android.security.keymaster.ExportResult;
public interface IKeystoreExportKeyCallback {
void onFinished(ExportResult exportResult) throws RemoteException;
public static abstract class Stub {
public static IKeystoreExportKeyCallback asInterface(IBinder b) {
throw new UnsupportedOperationException("STUB!");
}
}
}
@@ -0,0 +1,16 @@
package android.security.keystore;
import android.os.IBinder;
import android.os.IInterface;
import android.os.RemoteException;
import android.security.keymaster.KeyCharacteristics;
public interface IKeystoreKeyCharacteristicsCallback extends IInterface {
void onFinished(KeystoreResponse keystoreResponse, KeyCharacteristics keyCharacteristics) throws RemoteException;
public static abstract class Stub {
public static IKeystoreKeyCharacteristicsCallback asInterface(IBinder b) {
throw new UnsupportedOperationException("STUB!");
}
}
}
@@ -1,7 +1,10 @@
package android.security.keystore;
import java.lang.String;
public interface IKeystoreService {
public static final String DESCRIPTOR = "android.security.keystore.IKeystoreService";
class Stub {
}
}
@@ -0,0 +1,38 @@
package android.security.keystore;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class KeystoreResponse implements Parcelable {
public final int error_code_;
public final String error_msg_;
protected KeystoreResponse(int error_code, String error_msg) {
this.error_code_ = error_code;
this.error_msg_ = error_msg;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
public static final Creator<KeystoreResponse> CREATOR = new Creator<KeystoreResponse>() {
@Override
public KeystoreResponse createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeystoreResponse[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
}