Compare commits

..
44 Commits
Author SHA1 Message Date
Enginex0 f50004d9a5 docs(release): write v3.2 changelog and point update.json to fork
Changelog covers all 22 commits since v3.1: rate limiter,
importKey hardening, key persistence, supervisor daemon,
security patch consistency, and lifecycle scripts.
update.json now targets Enginex0/TEESimulator releases.
2026-02-06 23:50:30 +01:00
Enginex0 8c10cf71ce chore(module): add co-author credit, verbose action output, point updates to fork 2026-02-06 23:31:03 +01:00
Enginex0 1ed3d9ad6e fix(install): extract action.sh and uninstall.sh during module install 2026-02-06 23:22:16 +01:00
Enginex0 4107127506 chore(build): bump version to v3.2 2026-02-06 23:17:20 +01:00
Enginex0 e36c4e351c feat(module): add action.sh and uninstall.sh lifecycle scripts
action.sh clears persistent key storage on user trigger.
uninstall.sh kills daemon processes and removes all module
artifacts while preserving target.txt and keybox config.
2026-02-06 22:34:02 +01:00
Enginex0andGKI Builder 1546c3bba0 Derive boot and vendor patch levels from system prop when system=prop
TrickyAddon fetches Pixel bulletin dates for boot/vendor but system=prop
resolves to the real device prop, creating a cross-component date mismatch
on non-Pixel devices. Force all three through the same prop resolution path.
2026-02-06 21:10:59 +01:00
Enginex0andGKI Builder 81a8ce0c60 Rate-limit per-UID hardware keygen and harden importKey eviction
Sliding window limits each UID to 2 hardware generateKey calls per
30s burst window with max 2 concurrent. Overflow falls back to
software cert generation.

importKey post-hook retains patched chains instead of full eviction,
preventing detectors from using generate-then-import to bypass
attestation patching. getKeyEntry serves retained chains for imported
keys that overwrote attested aliases.
2026-02-06 21:10:59 +01:00
Enginex0andGKI Builder 7c4df3e237 Cap interceptable binder payload size at 256KB
Prevents thread starvation from flood attacks targeting the
binder interceptor with oversized payloads.
2026-02-06 21:10:59 +01:00
Enginex0andGKI Builder 1ac08411be Revert "Add X.509 certificate extensions for RFC 5280 compliance"
This reverts commit a11a5e41a2.
2026-02-06 21:10:59 +01:00
Enginex0andGKI Builder a11a5e41a2 Add X.509 certificate extensions for RFC 5280 compliance
- BasicConstraints: CA=false (critical)
- SubjectKeyIdentifier via SHA-1 hash
- AuthorityKeyIdentifier linked to issuer cert
2026-02-06 00:47:32 +01:00
Enginex0andGKI Builder c367aa5efc Add file-level locking to prevent race conditions in key persistence
Per-key ReentrantLock prevents concurrent writes to same key file
2026-02-06 00:31:53 +01:00
Enginex0andGKI Builder 88781ff31d Delegate key re-persistence to GeneratedKeyPersistence layer
Remove duplicate rePersistKeyIfNeeded, use centralized implementation
2026-02-06 00:24:50 +01:00
Enginex0andGKI Builder 409fb5fcc3 Reject oversized aliases to prevent binder buffer exhaustion
MAX_ALIAS_LENGTH (256KB) with 4x safety margin for transaction overhead
2026-02-06 00:24:50 +01:00
Enginex0andGKI Builder 0cf8f70544 fix(pki): strip HTML comments from PEM blocks before parsing
Some upstream keybox sources inject HTML comments inside PEM
certificate blocks. BouncyCastle's PEMParser chokes on these
non-base64 lines, silently failing to load the keybox.

Filter lines starting with <!-- in trimLines() before the content
reaches the PEM parser.
2026-02-06 00:16:07 +01:00
Enginex0andGKI Builder 7ecea09ec6 fix(app): add global uncaught exception handler for clean restart
Individual thread crashes silently kill the thread without bringing
down the process. The half-dead process stays alive but broken, and
the service.sh restart loop never fires.

Install a default uncaught exception handler that logs the error and
calls exitProcess(0), triggering the restart loop for full recovery.
2026-02-06 00:16:07 +01:00
Enginex0andGKI Builder 887c5fc666 fix(config): prevent FileObserver NPE on config file deletion
When a config file is deleted, the event handler sets file=null but
then force-unwraps it with file!! in the when block, crashing the
FileObserver thread. All subsequent config change notifications are
silently lost.

Replace force-unwrap with safe call, log a warning on deletion.
2026-02-06 00:16:07 +01:00
Enginex0andGKI Builder ce0ca18d98 Preserve generated keys across keybox rotation
Only invalidate patched cert chains when keybox changes.
Generated key material is independent and survives rotation.
2026-02-06 00:07:46 +01:00
Enginex0andGKI Builder fa28e9fc71 Integrate key persistence with interceptors
Save keys on generation, restore on daemon startup, delete on cleanup.
Re-persist when cert chain updates via updateSubcomponents.
2026-02-06 00:07:46 +01:00
Enginex0andGKI Builder c3822197b1 Add generated key persistence layer
Persist GENERATE-mode keys to disk so they survive daemon restarts.
Binary format with version header, atomic write via tmp+rename.
2026-02-06 00:07:46 +01:00
Enginex0andGKI Builder f276806096 fix(native): block attestation leak when interceptor service is dead
When the Java interceptor process dies, callback->transact() returns
DEAD_OBJECT but the code fell through to the real keystore, exposing
genuine TEE state to requesting apps.

Add pingBinder() liveness check on pre-transact failure. If the
interceptor is confirmed dead, return DEAD_OBJECT to the caller
instead of forwarding to real hardware. Apps see a transient service
error rather than the actual device attestation state.
2026-02-05 23:57:40 +01:00
Enginex0 9aa4a33c5e Add fork-based supervisor daemon for instant restart 2026-02-05 23:57:24 +01:00
Enginex0andGitHub 593bcfef83 Set correct certificate KeyUsage based on KeyPurpose (#119)
The previous implementation hardcoded the X.509 KeyUsage extension to `keyCertSign` for all generated certificates. This was only correct for keys with the `ATTEST_KEY` purpose and violated the Android HAL specification for keys intended for other uses. For instance, a key created for signing (`KeyPurpose::SIGN`) requires the `digitalSignature` bit to be set, not `keyCertSign`.

This commit corrects the logic by dynamically constructing the `KeyUsage` bitmask from the key's specified purposes, adhering to the mapping defined in `KeyCreationResult.aidl`. This ensures that generated certificates now have the correct KeyUsage bits, accurately reflecting the key's intended function (e.g., signing, decryption, key wrapping) and making them compliant with the specification.
2026-02-04 09:07:21 +01:00
JingMatrixandGitHub 5e68cb5f4b Resolve reference leak and warnings in binder interception (#122)
This merge addresses a critical strong reference leak in the ioctl hook that occurred during binder transaction interception. The leak was caused by a double increment of the reference count—once manually and once by a smart pointer's constructor—with only a single corresponding decrement. The fix ensures a balanced increment and decrement, preventing the leak and subsequent crashes.

Additionally, this change:
-   Reverts a now-unnecessary compatibility layer for the Android 11 RefBase ABI.
-   Implements `getInterfaceDescriptor` in the `BinderStub` to silence framework warnings that appeared after the primary leak was fixed.
2026-02-04 09:03:51 +01:00
JingMatrix a1bb3bbfa3 Release TEESimulator 3.1 2026-01-31 22:49:29 +01:00
JingMatrix e13adb925d Correct misunderstanding of takeIf execution order
The previous code incorrectly assumed `takeIf` prevents the execution of the receiver statement. Since `takeIf` is an extension function, the receiver—`InterceptorUtils.getTransactCode`—was evaluated eagerly *before* the version check predicate could run.

This commit replaces the `takeIf` chain with a standard `if/else` block to ensure the reflection call is only executed when the API level supports it.

Additionally, repeated `IKeystoreService.Stub::class.java` references were refactored into a `stubBinderClass` property.
2026-01-31 12:51:09 +01:00
c3f8f087a6 Support key enumeration via listEntries interception (#84)
Previously, generated keys were functional but invisible to enumeration APIs like `KeyStore.aliases()`. Because these keys reside solely in the simulator's memory, the standard database query performed by the system Keystore does not return them.

This commit intercepts `listEntries` and `listEntriesBatched` to inject these generated keys into the results.

Key implementation details:
- ListEntriesHandler: Encapsulates the logic to merge hardware-backed keys with software-backed keys.
- Ordering: Uses a `TreeMap` to ensure merged results are lexicographically sorted, mimicking AOSP behavior.
- Binder Safety: Implements `estimateSafeAmountToReturn` to calculate the response size. The handler truncates the result list if it exceeds the binder transaction limit (~350KB) as done in AOSP.
- Pagination: Respects the `startPastAlias` parameter to support batched listing.

Co-authored-by: JingMatrix <jingmatrix@gmail.com>
2026-01-31 11:37:25 +01:00
JingMatrix 51f32b9db2 Move attestation challenge check to certificate generation
Relocate the `attestationChallenge` length validation from `generateSoftwareKeyPair` to `generateCertificateChain`.

The challenge is only utilized during the construction of the certificate chain (via `AttestationBuilder.buildKeyDescription`). Placing the check in the key pair generation stage caused the logic to miss the `attestKey` transaction hook in `KeystoreInterceptor`.

This fixes a bug introduced in ce740542f7 which missed the detection bypass for Android 10 and 11 devices.
2026-01-30 21:13:02 +01:00
JingMatrix d60ad8fe47 Handle swapped attestation lists on certain Android 11 devices (#108)
Observed an abnormal Keymaster attestation structure on certain Android 11 devices where the `softwareEnforced` and `teeEnforced` authorization lists were swapped in order. This is a deviation from the documented specification and the behavior seen on most devices.

This non-compliance caused parsing failures, as the code expected the `teeEnforced` list to be at a fixed index (7). On the affected devices, this index contained the `softwareEnforced` list, which critically lacks the `TAG_ROOT_OF_TRUST` needed for successful validation and patching.

This commit introduces a defensive normalization step to handle this device-specific anomaly gracefully:

1.  Before parsing, the code now inspects the ASN.1 sequence at the expected `softwareEnforced` index (6).
2.  It checks for the presence of the `TAG_ROOT_OF_TRUST`, which can only exist in the TEE-enforced list.
3.  If the tag is found, the code concludes the lists are swapped and corrects the `allFields` array in-place by swapping the elements at indices 6 and 7.

By normalizing the data structure at the beginning, the rest of the parsing and patching logic can proceed without modification, ensuring correct operation on both compliant and non-compliant devices.
2026-01-30 21:10:41 +01:00
JingMatrix 9a1fbe8c79 Correct alias parsing in KeystoreInterceptor (#106)
The `extractAlias` utility was failing to strip `USRCERT_` and `CACERT_` prefixes, causing a cache miss during certificate chain patching. The function is now updated to correctly handle these prefixes.

Moreover, more logs are added to help debugging in the future.
2026-01-29 19:23:57 +01:00
JingMatrixandGitHub 68b660dfe1 Add SELinux rules for libTEESimulator.so loading (#104)
Allow `keystore` to access the `file` class for `adb_data_file` and `shell_data_file` contexts.

The target contexts correspond to the following locations:
- `adb_data_file`: The library path `/data/adb/modules/tricky_store/libTEESimulator.so`, used for FD transfer.
- `shell_data_file`: The fallback mechanism for loading the library by staging it in `/data/local/tmp`.

Note: The rule for the `dir` class (directory search) has been removed because the supporting audit logs were lost. The remaining file access logs were observed on a MEIZU 21 Note.
2026-01-29 15:15:56 +01:00
JingMatrixandGitHub 068188503c Fix multiple crashes and race conditions on Android 12 (#99)
This resolves several critical stability issues observed on Android 12 devices, including race conditions and API compatibility problems.

Key changes include:

-   Resolves Race Condition in TEE Check:
    Fixes a NullPointerException that occurred when the TEE functionality check was executed before the PackageManagerService was ready. The code now explicitly waits for the package manager to become available, preventing the crash on startup.

-   Fixes IllegalStateException on Initialization:
    Eliminates a crash caused by `setTelephonyServiceManager called twice`. This was due to a redundant call to `initializeMainlineModules()` in the DeviceAttestationService, which is now correctly handled a single time during application startup.

-   Fixes NoSuchAlgorithmException in Attestation:
    Adds a normalization function to handle signature algorithm names reported in all-caps by older Android versions (e.g., "SHA256WITHECDSA"). This ensures compatibility with Bouncy Castle, which expects a specific casing (e.g., "SHA256withECDSA").
2026-01-29 15:00:09 +01:00
JingMatrix 1bbc50d138 Prevent recursion when configured to intercept system UID (#100)
When the TEESimulator is configured to intercept UID 1000, accessing the `lazy` `bootKey` property causes a StackOverflowError.

The property's initializer sends a key generation request (UID 0) to probe real hardware. Previously, the C++ layer hijacked this request and spoofed it to UID 1000. This sent the request back to the Kotlin interceptor (if configured so), which attempted to access `bootKey` again to build the response, creating an infinite loop.

This change spoofs UID 0 requests to 1000 (to pass Keystore permissions) but explicitly bypasses hijacking, ensuring the probe request hits the real hardware.
2026-01-28 22:15:27 +01:00
JingMatrix d2492df02e Remove SELinux context manipulations during injection (#87)
After few tests in various devices, it seems that SELinux context modifications are unnecessary for the injection to work.

We thus remove all related manipulations. Further (partial) reverting of the commit must be justified with SELinux logs:

> adb shell su -c 'cat /proc/kmsg | grep avc'
2026-01-28 22:14:08 +01:00
JingMatrix e7d7b21daa Fix ARM ptrace compatibility and improve remote call safety (#94)
- Implement fallbacks to `PTRACE_GETREGS` and `PTRACE_SETREGS` for 32-bit ARM (`__arm__`). Some kernels return `EIO` or `EINVAL` when attempting to access `NT_PRSTATUS` via `PTRACE_GETREGSET`/`PTRACE_SETREGSET`.

- Update `transfer_fd_to_remote` to use `libc_return_addr` instead of `0` as the return address during the `recvmsg` split-call. This ensures the remote process stops predictably at a known non-executable location rather than relying on a potentially unsafe jump to `0x0`.

- Clarify comments regarding i386 argument passing in `utils.cpp`. Correctly note that a linear `write_proc` starting at the new SP matches the `cdecl` Right-to-Left memory layout (since stacks grow downwards while memory writes move upwards), removing the suggestion that arguments needed reversing.
2026-01-28 14:08:28 +01:00
JingMatrixandGitHub c29bc35a36 Fix cache consistency on key overwrite (#97)
Android allows applications to generate a new key using an existing alias without explicitly calling `deleteKey` first. In this scenario, the new key effectively replaces the old one. As a simulator, we must strictly follow this logic to prevent returning stale data.

Previously, `KeyMintSecurityLevelInterceptor` did not enforce mutual exclusion between the software key cache (`generatedKeys`) and the hardware chain cache (`patchedChains`). This led to state desynchronization where a stale software key could shadow a newly patched hardware chain if the alias was reused.

This change ensures `cleanupKeyData` is invoked immediately before caching a new key / chain in both the software (`handleGenerateKey`) and hardware (`onPostTransact`) paths, ensuring the simulator returns the correct key for the most recent generation request.
2026-01-28 13:50:05 +01:00
JingMatrix 549b5cecc2 Fix crash by avoiding hardcoded index for moduleHash
The previous implementation attempted to retrieve `moduleHash` from the `softwareEnforced` sequence using a hardcoded index (index 2).

However, fields in the Key Attestation `AuthorizationList` are optional. In observed crashes, index 2 actually corresponded to `keySize` (Tag 3, ASN1Integer) rather than `moduleHash`, causing an `IllegalArgumentException` when the code attempted to parse it as an `ASN1OctetString`.

This commit replaces the index-based access with a dynamic lookup for Tag 724.
2026-01-26 23:26:52 +01:00
JingMatrix 04d003ff4d Fix x86_64 injection: Red Zone adjustment and fallback logic (#91)
- Strictly adhere to the System V AMD64 ABI by skipping the 128-byte "Red Zone" before modifying the stack, see page 23 of https://gitlab.com/x86-psABIs/x86-64-ABI/-/jobs/artifacts/master/raw/x86-64-ABI/abi.pdf?job=build for details.

- Added `inject_via_staging` as a fallback strategy:
  1. Copies the payload to `/data/local/tmp`.
  2. Sets permissions/context (`u:object_r:system_file:s0`).
  3. Loads via standard `dlopen`.
  4. Immediately unlinks the file for stealth.

- Introduced `RegisterRestorer` RAII class to guarantee original registers are restored even if the injection logic returns early due to error.
2026-01-26 23:15:09 +01:00
JingMatrixandGitHub 0a842c6e07 Fix support for Android 10 (#92)
Users report that the method `waitForService` doesn't exist on Android 10.
Close #90 as completed.
2026-01-26 16:54:59 +01:00
JingMatrixandGitHub 9f77771e7b Fix Android 11 Keystore execution: Init framework and spoof UID 1000 (#85)
This commit resolves `KeyStore` API failures on Android 11 when running as a standalone CLI executable (UID 0), addressing both environment initialization and permission denial issues.

1. Initialize Android Framework Environment:
   Android 11 Keystore APIs expect a fully initialized application context and a Main Looper, which are missing in a raw root process. This patch:
   - Manually bootstraps `ActivityThread` via `systemMain()`.
   - Initializes `Looper.prepareMainLooper()`.
   - Injects a dummy `Application` object attached to the system context to satisfy `KeyStore.getApplicationContext()` checks.
   - Updates framework stubs to allow compilation of these hidden APIs.

2. Bypass Keystore Permission Checks via UID Spoofing:
   `KeyStoreService::generateKey` enforces the `P_INSERT` permission. Analysis of `permissions.cpp` reveals that UID 0 (Root) is explicitly denied this permission (granted only `P_GET`), whereas UID 1000 (System) holds all permissions (`~0`).
   
   To bypass this restriction, the binder interceptor now detects transactions originating from UID 0 and rewrites the `sender_euid` to 1000. This fools `KeyStoreService` into granting the request.
 
3. Refactor Execution Loop:
   Replaces the previous `Thread.sleep()` maintenance loop with `Looper.loop()`.
2026-01-26 14:07:06 +01:00
ab4fe643a3 Intercept updateSubcomponent to fix software key state inconsistency (#82)
Apps attempting to update the certificate chain of a simulated software-based key (e.g., via KeyStore.setKeyEntry) currently trigger a KEY_NOT_FOUND error. This happens because the request is passed to the hardware Keystore daemon, which has no knowledge of keys existing only in the simulator's memory.

To fix detecting points exploiting this inconsistency, we intercept the UPDATE_SUBCOMPONENT_TRANSACTION. If the target is a recognized virtual key, the simulator now:
1. Updates the in-memory certificate/chain metadata.
2. Returns NO_ERROR immediately to the caller.
3. Prevents the transaction from reaching the real hardware service.

Co-authored-by: JingMatrix <jingmatrix@gmail.com>
2026-01-23 17:53:34 +01:00
ce740542f7 Enforce attestation challenge length limit (#70)
Throws IllegalArgumentException if the challenge exceeds 128 bytes, per Android specs. Also fixes a duplicate assignment typo in KeystoreInterceptor.

Reference: https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setAttestationChallenge(byte[])

Co-authored-by: JingMatrix <jingmatrix@gmail.com>
2026-01-20 19:00:48 +01:00
dependabot[bot]andJingMatrix c27523fd97 Update dependencies 2026-01-11 16:24:34 +01:00
JingMatrixandGitHub 5a8454af7b Implement multi-purpose simulation for crypto operations (#59)
This commit introduces a comprehensive simulation engine for Keystore's `createOperation`, enabling the simulator to correctly handle multiple cryptographic purposes (SIGN, VERIFY, ENCRYPT, DECRYPT) for software-generated keys.

The implementation correctly mimics the AOSP framework's internal key identification mechanism. Instead of relying on an alias, a unique `keyId` is generated and embedded in the `nspace` field of the KeyDescriptor during `generateKey`. The `createOperation` hook then uses this `keyId` to dispatch requests: if the ID matches a known software key, the operation is simulated; otherwise, it is forwarded to the hardware service.

To support this, the `SoftwareOperation` engine was architected using a Strategy Pattern. A `CryptoPrimitive` interface defines common actions, with concrete implementations for `Signer`, `Verifier`, and `CipherPrimitive`. The main `SoftwareOperation` class acts as a controller, instantiating the correct primitive based on the `KeyPurpose` tag from the incoming operation parameters. A `JcaAlgorithmMapper` was added to centralize the logic for converting KeyMint constants into JCA algorithm strings.

For operations on real hardware-backed keys, a lightweight `OperationInterceptor` is now used for observation. It attaches to the genuine `iOperation` binder for logging and properly unregisters itself upon completion to prevent resource leaks. This is supported by new binder unregistration capabilities in the core `BinderInterceptor`.

This change also includes necessary stub files and minor regression fixes to make the simulation more robust and accurate.

See AOSP source for key identification logic:
https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/keystore/java/android/security/keystore2/AndroidKeyStoreKey.java
2025-12-08 19:59:32 +01:00
JingMatrix 83b65f09c9 Ensure mocked replies use native OK status (#60)
Corrects a bug where the native binder `status_t` was being set to application-level error codes (e.g., `KeyStore.NO_ERROR` which is 1).

Moreover, we call method `InterceptorUtils.createTypedObjectReply` to keep the code style consistent.
2025-12-08 04:30:01 +01:00
47 changed files with 2126 additions and 414 deletions
+2 -2
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 = "v3.0"
val verName = "v3.2"
android {
namespace = "org.matrix.TEESimulator"
@@ -116,7 +116,7 @@ androidComponents {
)
) {
into("lib") // Place them in the 'lib' subfolder of the staging directory.
include("**/libinject.so", "**/libTEESimulator.so")
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so")
}
// Now, copy and process the files from 'module' directory.
+5 -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 compat)
target_include_directories(utils PUBLIC external/AOSP/include)
add_library(binder SHARED stub/stub_binder.cpp)
target_include_directories(binder PUBLIC external/AOSP/include)
@@ -22,7 +22,10 @@ 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 compat/refbase_compat.cpp)
add_executable(libsupervisor.so supervisor.cpp)
target_link_libraries(libsupervisor.so PRIVATE log)
add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC external/linux-kernel/include include)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE binder lsplt_static utils)
+40 -20
View File
@@ -276,6 +276,12 @@ static sp<BinderInterceptor> g_interceptor_instance = nullptr;
// =============================================================================================
class BinderStub : public BBinder {
public:
const String16& getInterfaceDescriptor() const override {
static const String16 kDescriptor("org.matrix.TEESimulator.BinderStub");
return kDescriptor;
}
protected:
status_t onTransact(uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags) override {
if (code != intercept::kBackdoorCode) {
@@ -342,15 +348,16 @@ static sp<BinderStub> g_stub_instance = nullptr;
namespace {
/**
* @brief Analyses a binder transaction. If the target is monitored,
* hijacks the transaction by rewriting its destination to our BinderStub.
* @param txn_data Pointer to the transaction data within the ioctl buffer.
*/
constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024;
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
if (!txn_data || txn_data->target.ptr == 0)
return;
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks
if (txn_data->data_size > kMaxInterceptableDataSize)
return;
bool hijack = false;
ThreadTransactionInfo info;
@@ -359,9 +366,15 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
info.transaction_code = intercept::kBackdoorCode;
info.target_binder = nullptr;
hijack = true;
}
// Check 2: Normal interception based on registry of monitored binders
else {
// Check 2: Spoof uid of KeyStore requests from the daemon to bypass permission check
} else if (txn_data->sender_euid == 0) {
// The kernel driver fills sender_euid.
// libbinder.so trusts this value to populate IPCThreadState.
txn_data->sender_euid = 1000;
LOGV("[Hook] Spoofing UID for transaction: 0 -> %d", txn_data->sender_euid);
hijack = false; // Never hijack to avoid recursion
// Check 3: Normal interception based on registry of monitored binders
} else {
// Safe casting based on Binder driver ABI
RefBase::weakref_type *weak_ref = reinterpret_cast<RefBase::weakref_type *>(txn_data->target.ptr);
@@ -370,18 +383,17 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
// The raw pointer to the binder object itself is stored in the cookie
BBinder *target_binder_ptr = reinterpret_cast<BBinder *>(txn_data->cookie);
// This is safe ONLY because we successfully called attemptIncStrong().
// The sp<> constructor will not increment the ref count again, it just adopts the one we have.
// When sp_target goes out of scope, it will call decStrong(), releasing our temporary reference.
sp<BBinder> sp_target = sp<BBinder>::fromExisting(target_binder_ptr);
// Create a weak pointer for the lookup and to store in our context map.
// This is safe because we are holding a strong reference.
wp<BBinder> wp_target = target_binder_ptr;
// Now we can safely use sp_target (which implicitly converts to a wp) for the lookup.
if (g_interceptor_instance->isBinderIntercepted(sp_target)) {
if (g_interceptor_instance->isBinderIntercepted(wp_target)) {
info.transaction_code = txn_data->code;
info.target_binder = sp_target; // Assign the valid weak pointer
info.target_binder = wp_target; // Assign the valid weak pointer
hijack = true;
}
// No need to manually call decStrong(); the sp destructor handles it.
// Manually release the temporary strong reference we acquired at the start.
target_binder_ptr->decStrong(nullptr);
}
}
@@ -581,9 +593,16 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
Parcel pre_req, pre_resp;
writeTransactionData(pre_req, tx_id, target, code, flags, request);
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) {
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id);
return false; // Callback failed, proceed as if not intercepted
status_t pre_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp);
if (pre_status != OK) {
// Block when interceptor is dead to prevent privacy leak to third-party apps
if (callback->pingBinder() != OK) {
LOGE("[TX_ID: %" PRIu64 "] Interceptor DEAD. Blocking to prevent attestation leak.", tx_id);
result = DEAD_OBJECT;
return true;
}
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed (not dead). Forwarding.", tx_id);
return false;
}
int32_t action = pre_resp.readInt32();
@@ -636,7 +655,8 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size));
}
if (callback->transact(intercept::kPostTransact, post_req, &post_resp) == OK) {
status_t post_status = callback->transact(intercept::kPostTransact, post_req, &post_resp);
if (post_status == OK) {
int32_t post_action = post_resp.readInt32();
if (post_action == intercept::kActionOverrideReply && reply) {
result = post_resp.readInt32(); // Read new status
@@ -1,61 +0,0 @@
#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
@@ -1,11 +0,0 @@
#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,7 +17,6 @@
#ifndef ANDROID_STRONG_POINTER_H
#define ANDROID_STRONG_POINTER_H
#include "refbase_compat.h"
#include <functional>
#include <type_traits> // for common_type.
@@ -213,7 +212,7 @@ sp<T> sp<T>::make(Args&&... args) {
template <typename T>
sp<T> sp<T>::fromExisting(T* other) {
if (other) {
incStrongFromExisting(other, other);
other->incStrongRequireStrong(other);
sp<T> result;
result.m_ptr = other;
return result;
+204 -63
View File
@@ -7,6 +7,7 @@
#include <sys/mman.h>
#include <sys/ptrace.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/system_properties.h>
#include <sys/uio.h>
#include <sys/un.h>
@@ -17,6 +18,7 @@
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <optional>
#include <string>
#include <vector>
@@ -95,10 +97,6 @@ constexpr size_t kMagicLength = 16;
constexpr size_t kMaxPathLength = PATH_MAX;
// Maximum length for file paths.
constexpr const char *kSystemFileContext = "u:object_r:system_file:s0";
// SELinux context for system files,
// used for socket creation and library file context.
constexpr const char *kLibcModule = "libc.so";
// Name of the C standard library.
@@ -215,8 +213,8 @@ private:
* @brief Transfers a file descriptor from the injector process to the remote process.
*
* This function uses Unix domain sockets with SCM_RIGHTS to send a file descriptor.
* It involves setting SELinux contexts, creating local and remote sockets, binding,
* and then coordinating sendmsg/recvmsg calls using ptrace.
* It involves creating local and remote sockets, binding, and then coordinating
* sendmsg/recvmsg calls using ptrace.
*
* @param pid The target process ID.
* @param lib_path The path to the library file being transferred.
@@ -233,29 +231,14 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
uintptr_t libc_return_addr) {
LOGD("Attempting to transfer file descriptor for library: %s", lib_path);
// 1. Set SELinux context for socket creation in the injector process.
// This is crucial for Android where SELinux might prevent socket operations.
if (!set_sockcreate_con(constants::kSystemFileContext)) {
LOGE("Failed to set socket creation context.");
return std::nullopt;
}
// 2. Create a local Unix domain socket for FD transfer.
// Create a local Unix domain socket for FD transfer.
UniqueFd local_socket = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
if (local_socket == -1) {
PLOGE("Failed to create local Unix domain socket.");
return std::nullopt;
}
// 3. Set SELinux context for the library file if possible.
// This might be required for the target process to open/access it later if directly opening by path.
// For FD transfer, this is less critical as the FD's context is inherited, but good practice.
if (setfilecon(lib_path, constants::kSystemFileContext) == -1) {
// Log a warning, but don't fail, as FD transfer might still work.
PLOGE("Failed to set context of library file: %s. This might cause issues.", lib_path);
}
// 4. Open the local library file to get a file descriptor.
// Open the local library file to get a file descriptor.
UniqueFd local_lib_fd = open(lib_path, O_RDONLY | O_CLOEXEC);
if (local_lib_fd == -1) {
PLOGE("Failed to open library file: %s", lib_path);
@@ -271,7 +254,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
void *errno_addr; // Address of __errno for getting remote errno.
} funcs{};
// 5. Resolve required libc functions in the remote process.
// Resolve required libc functions in the remote process.
funcs.socket_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "socket");
funcs.bind_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "bind");
funcs.recvmsg_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "recvmsg");
@@ -306,25 +289,28 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
}
};
// 6. Create a Unix domain socket in the remote process.
// Create a Unix domain socket in the remote process.
std::vector<uintptr_t> args = {AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0};
int remote_fd = static_cast<int>(
remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.socket_addr), libc_return_addr, args));
if (remote_fd == -1) {
if (remote_fd <= 0) {
// remote_call returns 0 on failure.
// socket() returning 0 is technically possible (if stdin closed),
// but highly unlikely for a daemon. We treat 0 as failure here to catch the injection error.
errno = get_remote_errno(); // Set local errno for PLOGE.
PLOGE("Failed to create remote socket.");
PLOGE("Failed to create remote socket (returned %d).", remote_fd);
return std::nullopt;
}
LOGD("Successfully created remote socket with FD: %d", remote_fd);
// 7. Generate a unique magic string for the abstract Unix domain socket path.
// Generate a unique magic string for the abstract Unix domain socket path.
auto magic = generateMagic(constants::kMagicLength);
struct sockaddr_un sock_addr{.sun_family = AF_UNIX, .sun_path = {0}};
// Abstract Unix domain sockets have sun_path[0] as null, and the name starts from sun_path[1].
memcpy(sock_addr.sun_path + 1, magic.c_str(), magic.size());
socklen_t addr_len = sizeof(sock_addr.sun_family) + 1 + magic.size(); // Length includes null byte and magic.
// 8. Push the sockaddr_un structure to the remote process's stack.
// Push the sockaddr_un structure to the remote process's stack.
auto remote_addr = push_memory(pid, regs, &sock_addr, sizeof(sock_addr));
if (remote_addr == 0) {
LOGE("Failed to push socket address to remote memory.");
@@ -332,7 +318,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
return std::nullopt;
}
// 9. Bind the remote socket to the abstract Unix domain socket path.
// Bind the remote socket to the abstract Unix domain socket path.
args = {static_cast<uintptr_t>(remote_fd), remote_addr, static_cast<uintptr_t>(addr_len)};
auto bind_result = remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.bind_addr), libc_return_addr, args);
if (bind_result == static_cast<uintptr_t>(-1)) {
@@ -346,7 +332,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
// Prepare control message buffer for SCM_RIGHTS (file descriptor passing).
char cmsgbuf[CMSG_SPACE(sizeof(int))] = {0};
// 10. Push the control message buffer to the remote process's stack.
// Push the control message buffer to the remote process's stack.
auto remote_cmsgbuf = push_memory(pid, regs, &cmsgbuf, sizeof(cmsgbuf));
if (remote_cmsgbuf == 0) {
LOGE("Failed to push control message buffer to remote memory.");
@@ -359,7 +345,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
msg_hdr.msg_control = reinterpret_cast<void *>(remote_cmsgbuf);
msg_hdr.msg_controllen = sizeof(cmsgbuf);
// 11. Push the msghdr structure to the remote process's stack.
// Push the msghdr structure to the remote process's stack.
auto remote_hdr = push_memory(pid, regs, &msg_hdr, sizeof(msg_hdr));
if (remote_hdr == 0) {
LOGE("Failed to push message header to remote memory.");
@@ -367,16 +353,16 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
return std::nullopt;
}
// 12. Initiate the remote recvmsg call. This will block the remote process.
// Initiate the remote recvmsg call. This will block the remote process.
args = {static_cast<uintptr_t>(remote_fd), remote_hdr, MSG_WAITALL};
if (!remote_pre_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.recvmsg_addr), 0, args)) {
if (!remote_pre_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.recvmsg_addr), libc_return_addr, args)) {
LOGE("Failed to initiate remote recvmsg call.");
close_remote(remote_fd);
return std::nullopt;
}
LOGD("Remote recvmsg initiated, waiting for FD transfer...");
// 13. Prepare the local msghdr for sending the file descriptor.
// Prepare the local msghdr for sending the file descriptor.
// The msg_control and msg_name fields of the local msghdr are set up.
msg_hdr.msg_control = &cmsgbuf; // Use local cmsgbuf for sending.
msg_hdr.msg_name = &sock_addr;
@@ -396,7 +382,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
*reinterpret_cast<int *>(CMSG_DATA(cmsg)) = local_lib_fd; // The FD to send.
}
// 14. Send the file descriptor from the injector to the remote process.
// Send the file descriptor from the injector to the remote process.
if (sendmsg(local_socket, &msg_hdr, 0) == -1) {
PLOGE("Failed to send file descriptor to remote process.");
// We do not close local_lib_fd here as it might be transferred even if
@@ -407,9 +393,9 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
}
LOGD("Local FD %d sent to remote process.", local_lib_fd.operator const int &());
// 15. Complete the remote recvmsg call. This will retrieve the return value.
// Complete the remote recvmsg call. This will retrieve the return value.
auto recvmsg_result =
static_cast<ssize_t>(remote_post_call(pid, regs, 0)); // No specific expected return address for recvmsg
static_cast<ssize_t>(remote_post_call(pid, regs, libc_return_addr));
if (recvmsg_result == -1) {
errno = get_remote_errno();
PLOGE("Remote recvmsg call failed.");
@@ -418,7 +404,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
}
LOGD("Remote recvmsg completed with result: %zd", recvmsg_result);
// 16. Read the control message buffer back from the remote process to extract the FD.
// Read the control message buffer back from the remote process to extract the FD.
if (read_proc(pid, remote_cmsgbuf, &cmsgbuf, sizeof(cmsgbuf)) != sizeof(cmsgbuf)) {
LOGE("Failed to read control message buffer from remote process.");
close_remote(remote_fd);
@@ -439,7 +425,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
LOGI("Successfully transferred FD %d to remote process, new remote FD: %d", local_lib_fd.operator const int &(),
transferred_fd);
// 17. Close the remote socket.
// Close the remote socket.
close_remote(remote_fd);
return transferred_fd;
@@ -642,6 +628,130 @@ static bool remote_call_entry(int pid, struct user_regs_struct &regs, uintptr_t
return true; // Return true if the call itself completed, regardless of its return value.
}
/**
* @brief RAII wrapper to ensure a temporary file is deleted (unlinked)
* when the object goes out of scope.
*
* This is crucial for stealth: we want the library to exist on the filesystem
* for the shortest time possible.
*/
class ScopedFileDeleter {
public:
explicit ScopedFileDeleter(std::string path) : path_(std::move(path)) {}
~ScopedFileDeleter() {
if (!path_.empty()) {
LOGD("Cleaning up staged file: %s", path_.c_str());
unlink(path_.c_str());
}
}
// Disable copy to prevent double-deletion issues
ScopedFileDeleter(const ScopedFileDeleter&) = delete;
ScopedFileDeleter& operator=(const ScopedFileDeleter&) = delete;
private:
std::string path_;
};
/**
* @brief Copies a file from source to destination.
*
* @param src Absolute path to source file.
* @param dst Absolute path to destination file.
* @return True on success, false on failure.
*/
static bool copy_file(const char* src, const char* dst) {
std::ifstream src_file(src, std::ios::binary);
std::ofstream dst_file(dst, std::ios::binary);
if (!src_file) {
PLOGE("Failed to open source file for copying: %s", src);
return false;
}
if (!dst_file) {
PLOGE("Failed to open destination file for copying: %s", dst);
return false;
}
dst_file << src_file.rdbuf();
return src_file.good() && dst_file.good();
}
/**
* @brief Performs injection via the "Staging" method.
*
* This strategy is used when direct FD passing fails (e.g., due to Seccomp filters).
* 1. Copies the library to a world-readable location (/data/local/tmp).
* 2. Loads it via standard dlopen().
* 3. Immediately deletes the file to hide tracks.
*
* @param pid The target process ID.
* @param regs The target process registers (must be Red-Zone adjusted if x86_64).
* @param local_map Local memory map.
* @param remote_map Remote memory map.
* @param lib_path The path to the original library.
* @param libc_return_addr Return address for remote calls.
* @return The handle of the loaded library, or std::nullopt on failure.
*/
static std::optional<uintptr_t> inject_via_staging(int pid, struct user_regs_struct &regs,
const std::vector<lsplt::MapInfo> &local_map,
const std::vector<lsplt::MapInfo> &remote_map,
const char *lib_path, uintptr_t libc_return_addr) {
LOGI("Initiating Staging Fallback mechanism...");
// Generate a random path in /data/local/tmp
// /data/local/tmp is chosen because it is traversable by most contexts.
std::string staged_path = "/data/local/tmp/lib" + generateMagic(8) + ".so";
// Ensure the file is deleted when this function exits (Success or Failure).
// The kernel keeps the inode alive for the mapped process even after unlink.
ScopedFileDeleter file_guard(staged_path);
LOGD("Staging library to: %s", staged_path.c_str());
// Copy the library
if (!copy_file(lib_path, staged_path.c_str())) {
LOGE("Failed to copy library during staging.");
return std::nullopt;
}
// Set Permissions to 644 (RW-R--R--)
// This allows the target process (likely running as a specific UID) to read the file.
if (chmod(staged_path.c_str(), 0644) != 0) {
PLOGE("Failed to chmod staged file.");
return std::nullopt;
}
// Resolve 'dlopen' in the remote process
auto dlopen_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "dlopen");
if (!dlopen_addr) {
LOGE("Failed to find 'dlopen' in remote process.");
return std::nullopt;
}
// Push the staged path to remote memory
uintptr_t remote_path_addr = push_string(pid, regs, staged_path.c_str());
if (remote_path_addr == 0) {
LOGE("Failed to push staged path string to remote memory.");
return std::nullopt;
}
// Call dlopen(path, RTLD_NOW)
std::vector<uintptr_t> args = {remote_path_addr, RTLD_NOW};
uintptr_t handle = remote_call(pid, regs, reinterpret_cast<uintptr_t>(dlopen_addr),
libc_return_addr, args);
if (handle == 0) {
std::string error_msg = get_remote_dlerror(pid, regs, local_map, remote_map, libc_return_addr);
LOGE("Staged dlopen failed. dlerror: %s", error_msg.c_str());
return std::nullopt;
}
LOGI("Successfully loaded staged library. Handle: %p", reinterpret_cast<void*>(handle));
return handle;
}
/**
* @brief RAII wrapper for ptrace attachment and detachment.
*
@@ -694,12 +804,31 @@ private:
bool attached_; // Flag indicating current attachment status.
};
// RAII Class to ensure registers are always restored
class RegisterRestorer {
public:
RegisterRestorer(int pid, const struct user_regs_struct& original_regs)
: pid_(pid), regs_(original_regs) {}
~RegisterRestorer() {
// Always restore registers when this object goes out of scope
if (set_regs(pid_, regs_)) {
LOGD("Original registers for process %d restored.", pid_);
} else {
PLOGE("Failed to restore original registers for process %d.", pid_);
}
}
private:
int pid_;
struct user_regs_struct regs_;
};
/**
* @brief Injects a shared library into a target process using ptrace.
*
* This is the main orchestration function for the library injection.
* It handles attachment, remote memory/register manipulation, FD transfer,
* remote dlopen/dlsym, and remote entry point execution.
* staging fallback, remote dlopen/dlsym, and remote entry point execution.
*
* @param pid The target process ID.
* @param lib_path The absolute path to the shared library to inject.
@@ -742,6 +871,14 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) {
backup_regs = current_regs; // Store a copy for restoration.
LOGD("Process %d registers backed up.", pid);
// Skip the Red Zone (128 bytes) on x86_64 to prevent stack corruption
#if defined(__x86_64__)
current_regs.rsp -= 128;
#endif
// Ensures original state is restored even if injection fails/crashes mid-way.
RegisterRestorer reg_guard(pid, backup_regs);
// Create a scope to ensure RAII objects are destroyed BEFORE register restoration
{
// 4. Scan local and remote memory maps to resolve function addresses.
@@ -760,53 +897,57 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) {
}
LOGD("Found libc return address: %p", reinterpret_cast<void *>(libc_return_addr));
// 6. Transfer the library's file descriptor to the remote process.
// 6. Attempt to transfer the library's file descriptor to the remote process.
int remote_fd = -1;
auto lib_fd_opt = transfer_fd_to_remote(pid, lib_path, current_regs, local_map, remote_map,
reinterpret_cast<uintptr_t>(libc_return_addr));
if (!lib_fd_opt) {
LOGE("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid);
return false;
}
RemoteLibraryHandle remote_lib_guard(pid, *lib_fd_opt);
LOGD("Library FD %d transferred to remote process %d.", remote_lib_guard.fd(), pid);
remote_lib_guard.set_libc_return_addr(reinterpret_cast<uintptr_t>(libc_return_addr));
std::optional<RemoteLibraryHandle> remote_lib_guard;
std::optional<uintptr_t> handle_opt;
// 7. Remotely load the library using the transferred file descriptor.
auto handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_lib_guard.fd(), lib_path,
reinterpret_cast<uintptr_t>(libc_return_addr));
if (lib_fd_opt) {
remote_fd = *lib_fd_opt;
remote_lib_guard.emplace(pid, remote_fd);
remote_lib_guard->set_libc_return_addr(reinterpret_cast<uintptr_t>(libc_return_addr));
LOGD("FD Transfer successful (FD: %d). Attempting android_dlopen_ext...", remote_fd);
handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_fd, lib_path,
reinterpret_cast<uintptr_t>(libc_return_addr));
} else {
LOGW("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid);
}
// 7. Staging Fallback (Copy-Inject-Delete) if FD transfer failed.
if (!handle_opt) {
handle_opt = inject_via_staging(pid, current_regs, local_map, remote_map,
lib_path, reinterpret_cast<uintptr_t>(libc_return_addr));
}
if (!handle_opt || *handle_opt == 0) {
LOGE("Failed to load library '%s' in remote process %d.", lib_path, pid);
// If dlopen fails, the remote_lib_guard.fd() is still valid in the target process and needs to be closed.
// The RemoteLibraryHandle constructor takes care of this.
return false;
}
remote_lib_guard.set_handle(*handle_opt);
uintptr_t handle = *handle_opt;
if (remote_lib_guard) remote_lib_guard->set_handle(handle);
// 8. Find the entry point symbol in the remotely loaded library.
auto entry_opt = remote_find_entry(pid, current_regs, entry_name, local_map, remote_map,
remote_lib_guard.handle(), reinterpret_cast<uintptr_t>(libc_return_addr));
handle, reinterpret_cast<uintptr_t>(libc_return_addr));
if (!entry_opt) {
LOGE("Failed to find entry point '%s' in remote library (handle %p).", entry_name,
reinterpret_cast<void *>(remote_lib_guard.handle()));
reinterpret_cast<void *>(handle));
return false;
}
uintptr_t entry_addr = *entry_opt;
// 9. Call the remote entry point function.
if (!remote_call_entry(pid, current_regs, entry_addr, remote_lib_guard.handle(),
if (!remote_call_entry(pid, current_regs, entry_addr, handle,
reinterpret_cast<uintptr_t>(libc_return_addr))) {
LOGE("Failed to call remote entry point '%s'.", entry_name);
return false;
}
}
// 10. Restore original registers of the target process.
if (!set_regs(pid, backup_regs)) {
LOGE("Failed to restore original registers for process %d.", pid);
return false;
}
LOGD("Original registers for process %d restored.", pid);
LOGI("Library injection completed successfully for process %d.", pid);
return true;
}
+18 -11
View File
@@ -263,7 +263,14 @@ bool get_regs(int pid, struct user_regs_struct &regs) {
struct iovec reg_iov = {.iov_base = &regs, .iov_len = sizeof(struct user_regs_struct)};
if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &reg_iov) == -1) {
PLOGE("Failed to get register set for PID %d.", pid);
#if defined(__arm__)
if (ptrace(PTRACE_GETREGS, pid, 0, &regs) == -1) {
PLOGE("Fallback to PTRACE_GETREGS failed.");
return false;
}
#else
return false;
#endif
}
#else
# error "Unsupported architecture for register access in get_regs."
@@ -296,7 +303,14 @@ bool set_regs(int pid, struct user_regs_struct &regs) {
struct iovec reg_iov = {.iov_base = &regs, .iov_len = sizeof(struct user_regs_struct)};
if (ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &reg_iov) == -1) {
PLOGE("Failed to set register set for PID %d.", pid);
#if defined(__arm__)
if (ptrace(PTRACE_SETREGS, pid, 0, &regs) == -1) {
PLOGE("Fallback to PTRACE_SETREGS failed.");
return false;
}
#else
return false;
#endif
}
#else
# error "Unsupported architecture for register access in set_regs."
@@ -588,17 +602,10 @@ bool remote_pre_call(int pid, struct user_regs_struct &regs, uintptr_t func_addr
size_t stack_args_size = args.size() * sizeof(uintptr_t);
align_stack(regs, stack_args_size);
// Push all arguments onto the stack (order is important if ABI is right-to-left push).
// The current implementation writes args.data() directly,
// assuming it's already in the correct order for push.
// For cdecl, arguments are pushed right-to-left.
// A vector `args = {A, B, C}` means A is arg1, B is arg2 etc.
// So, `C` should be pushed first, then `B`, then `A`.
// `write_proc` copies linearly.
// This implies `args` should be pre-reversed for cdecl.
// For simplicity, we assume the remote function is compatible with how it's pushed,
// or that it's variadic where order doesn't matter for first args.
// A robust i386 implementation would need to push args in reverse order.
// i386 cdecl expects arguments pushed Right-to-Left (stack grows down).
// Since `write_proc` writes to increasing addresses (up), a linear write
// starting at the new SP places the first argument at the lowest address.
// This matches the ABI memory layout without needing to reverse the vector.
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), args.data(), stack_args_size) !=
static_cast<ssize_t>(stack_args_size)) {
LOGE("Failed to push arguments for i386 remote call.");
+57
View File
@@ -0,0 +1,57 @@
// Fork-based supervisor for instant daemon restart
#include <unistd.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
static volatile sig_atomic_t should_exit = 0;
static void signal_handler(int sig) {
should_exit = 1;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <daemon> [args...]\n", argv[0]);
return 1;
}
// Forward termination signals to exit cleanly
signal(SIGTERM, signal_handler);
signal(SIGINT, signal_handler);
const char *daemon_path = argv[1];
char **daemon_argv = &argv[1];
while (!should_exit) {
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
usleep(100000); // 100ms backoff on fork failure
continue;
}
if (pid == 0) {
// Child: become the daemon
prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies
execv(daemon_path, daemon_argv);
perror("execv failed");
_exit(127);
}
// Parent: wait for child to exit
int status;
waitpid(pid, &status, 0);
if (should_exit) break;
// Instant restart - no delay
}
return 0;
}
@@ -1,6 +1,11 @@
package org.matrix.TEESimulator
import android.app.ActivityThread
import android.app.Application
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import android.os.Looper
import java.security.Security
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.matrix.TEESimulator.config.ConfigurationManager
@@ -9,6 +14,7 @@ import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils
import kotlin.system.exitProcess
/**
* Main application object for TEESimulator. This object manages the application's lifecycle,
@@ -27,16 +33,23 @@ object App {
*/
@JvmStatic
fun main(args: Array<String>) {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
SystemLogger.error("Uncaught exception on thread '${thread.name}'. Exiting for restart.", throwable)
exitProcess(0)
}
SystemLogger.info("Welcome to TEESimulator!")
try {
// Initialize the Android framework environment
prepareEnvironment()
// Initialize and start the appropriate keystore interceptors.
initializeInterceptors()
// 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
@@ -44,13 +57,43 @@ object App {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
maintainService()
// This starts the message queue processing. It blocks here indefinitely
// processing messages until Looper.myLooper().quit() is called.
Looper.loop()
} catch (e: Exception) {
SystemLogger.error("A fatal error occurred in the main application thread.", e)
throw e
}
}
/** Initializes the necessary Android framework internals to satisfy KeyStore requirements. */
private fun prepareEnvironment() {
// 1. Prepare Main Looper
if (Looper.getMainLooper() == null) {
@Suppress("deprecation") Looper.prepareMainLooper()
}
// 2. Initialize ActivityThread for the current process
val activityThread = ActivityThread.systemMain()
// 3. Get the system context
val systemContext = activityThread.getSystemContext()
// 4. Create a dummy Application object and attach the context
val app = Application()
val attachMethod =
ContextWrapper::class.java.getDeclaredMethod("attachBaseContext", Context::class.java)
attachMethod.isAccessible = true
attachMethod.invoke(app, systemContext)
// 5. Inject this application object into ActivityThread's mInitialApplication field.
// This is what KeyStore.getApplicationContext() looks for.
val mInitialApplicationField =
ActivityThread::class.java.getDeclaredField("mInitialApplication")
mInitialApplicationField.isAccessible = true
mInitialApplicationField.set(activityThread, app)
}
/**
* Selects and initializes the correct keystore interceptor based on the Android SDK version. It
* retries initialization until it succeeds.
@@ -79,6 +122,7 @@ object App {
SystemLogger.info(
"Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})"
)
android.security.keystore.AndroidKeyStoreProvider.install()
KeystoreInterceptor
}
// For Android S (12) and newer, use the Keystore2Interceptor.
@@ -86,18 +130,8 @@ object App {
SystemLogger.info(
"Using Keystore2Interceptor for Android S and later (SDK ${Build.VERSION.SDK_INT})"
)
android.security.keystore2.AndroidKeyStoreProvider.install()
Keystore2Interceptor
}
}
/**
* Puts the main thread into a long-running sleep loop. This is a common pattern to keep a
* background service process alive indefinitely.
*/
private fun maintainService() {
SystemLogger.info("Service started successfully. Entering maintenance mode.")
while (true) {
Thread.sleep(SERVICE_SLEEP_MS)
}
}
}
@@ -112,6 +112,7 @@ object AttestationBuilder {
}
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch")
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
if (bootPatch != DO_NOT_REPORT) {
DERTaggedObject(
@@ -1,10 +1,8 @@
package org.matrix.TEESimulator.attestation
/**
* Defines constants for KeyMint attestation tags, as specified in the Android hardware security
* HAL.
*
* These tags identify specific properties and authorizations of a cryptographic key.
* Defines constants for KeyMint attestation, mainly the tags of properties and authorizations of a
* cryptographic key, as specified in the Android hardware security HAL.
*/
object AttestationConstants {
// https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/KeyCreationResult.aidl
@@ -88,4 +86,8 @@ object AttestationConstants {
const val TAG_CERTIFICATE_SUBJECT = 1007
const val TAG_CERTIFICATE_NOT_BEFORE = 1008
const val TAG_CERTIFICATE_NOT_AFTER = 1009
// --- Other Constants ---
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
}
@@ -83,6 +83,16 @@ object AttestationPatcher {
}
}
/**
* Helper to normalize algorithm names for Bouncy Castle. Old Android versions might reports
* "SHA256WITHECDSA", but Bouncy Castle expects "SHA256withECDSA".
*/
private fun normalizeSignatureAlgorithm(algoName: String): String {
// 1. Force uppercase to handle "sha256withecdsa"
// 2. Replace "WITH" with "with" to satisfy Bouncy Castle's naming convention
return algoName.uppercase().replace("WITH", "with")
}
/**
* Creates a new leaf certificate with a modified attestation extension.
*
@@ -128,7 +138,7 @@ object AttestationPatcher {
// Sign the newly built certificate with the private key from our keybox.
val signer =
JcaContentSignerBuilder(sigAlgName)
JcaContentSignerBuilder(normalizeSignatureAlgorithm(sigAlgName))
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keybox.keyPair.private)
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
@@ -206,11 +216,37 @@ object AttestationPatcher {
}
}
// Function to check if a given ASN1Sequence contains the Root of Trust tag.
private fun sequenceContainsRootOfTrust(seq: ASN1Encodable): Boolean {
if (seq !is ASN1Sequence) return false
return seq.any { element ->
(element as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST
}
}
/** Parses the critical components from an existing attestation extension. */
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
val sequence = ASN1Sequence.getInstance(extension.extnValue.octets)
val allFields = sequence.toArray()
// Check if the fields are in the wrong order and swap them if necessary.
val softwareEnforcedCandidate =
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
val teeEnforcedCandidate =
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX]
// The signature of a swapped order: the RoT is in the software list's position.
if (
sequenceContainsRootOfTrust(softwareEnforcedCandidate) &&
!sequenceContainsRootOfTrust(teeEnforcedCandidate)
) {
// Swap the elements in the array to restore the standard order.
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX] =
teeEnforcedCandidate
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] =
softwareEnforcedCandidate
}
val teeEnforced =
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence
@@ -1,8 +1,6 @@
package org.matrix.TEESimulator.attestation
import android.annotation.SuppressLint
import android.app.ActivityThread
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyPairGenerator
@@ -83,16 +81,6 @@ object DeviceAttestationService {
private fun checkTeeFunctionality(): Boolean {
SystemLogger.info("Performing TEE functionality check...")
return try {
// Ensure mainline modules and the correct Keystore provider are initialized.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
android.app.ActivityThread.initializeMainlineModules()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
android.security.keystore2.AndroidKeyStoreProvider.install()
} else {
android.security.keystore.AndroidKeyStoreProvider.install()
}
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyPairGenerator =
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
@@ -192,13 +180,15 @@ object DeviceAttestationService {
ASN1Sequence.getInstance(
fields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
)
if (softwareEnforced.size() >= 3) {
moduleHash =
ASN1OctetString.getInstance(
ASN1TaggedObject.getInstance(softwareEnforced.getObjectAt(2)).baseObject
)
.octets
}
moduleHash =
softwareEnforced
.toArray()
.firstOrNull {
(it as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_MODULE_HASH
}
?.let {
ASN1OctetString.getInstance((it as ASN1TaggedObject).baseObject).octets
}
val teeEnforced =
ASN1Sequence.getInstance(
@@ -1,8 +1,6 @@
package org.matrix.TEESimulator.attestation
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.Tag
import android.hardware.security.keymint.*
import java.math.BigInteger
import java.util.Date
import javax.security.auth.x500.X500Principal
@@ -22,6 +20,8 @@ data class KeyMintAttestation(
val algorithm: Int,
val ecCurve: Int,
val ecCurveName: String,
val blockMode: List<Int>,
val padding: List<Int>,
val purpose: List<Int>,
val digest: List<Int>,
val rsaPublicExponent: BigInteger?,
@@ -54,6 +54,12 @@ data class KeyMintAttestation(
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
ecCurveName = params.deriveEcCurveName(),
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
// AOSP: [key_param(tag = PADDING, field = PaddingMode)]
padding = params.findAllPaddingMode(Tag.PADDING),
// AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)]
purpose = params.findAllKeyPurpose(Tag.PURPOSE),
@@ -121,6 +127,14 @@ private fun Array<KeyParameter>.findDate(tag: Int): Date? =
private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
this.find { it.tag == tag }?.value?.blob
/** Maps to AOSP field = BlockMode (Repeated) */
private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.blockMode }
/** Maps to AOSP field = BlockMode (Repeated) */
private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.paddingMode }
/** Maps to AOSP field = KeyPurpose (Repeated) */
private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
this.filter { it.tag == tag }.map { it.value.keyPurpose }
@@ -54,6 +54,17 @@ object ConfigurationManager {
configRoot.mkdirs()
SystemLogger.info("Configuration root is: ${configRoot.absolutePath}")
// First, ensure the package manager service is running, as the TEE check depends on it.
// This prevents a race condition on startup.
SystemLogger.info("Waiting for PackageManagerService to be ready...")
if (getPackageManager() == null) {
SystemLogger.error(
"PackageManagerService is not available. TEE check will likely fail."
)
} else {
SystemLogger.info("PackageManagerService is ready.")
}
// Initial load of all configuration files.
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
@@ -242,7 +253,14 @@ object ConfigurationManager {
}
// Parse global and per-package configurations.
val newGlobalLevel = parseLines(contextLines[""])
var newGlobalLevel = parseLines(contextLines[""])
// TrickyAddon writes Pixel bulletin dates for boot/vendor but system=prop
// resolves to the real device prop — force boot/vendor through the same path
// to prevent cross-component date mismatches on non-Pixel devices.
if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) {
SystemLogger.info("system=prop: forcing boot/vendor to derive from device props (were: boot=${newGlobalLevel?.boot}, vendor=${newGlobalLevel?.vendor})")
newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop")
}
contextLines.remove("") // Remove global context to iterate over packages next
for ((pkg, lines) in contextLines) {
@@ -296,8 +314,10 @@ object ConfigurationManager {
val file = if (event != DELETE) File(configRoot, path) else null
when (path) {
TARGET_PACKAGES_FILE -> loadTargetPackages(file!!)
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!)
TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) }
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
PATCH_LEVEL_FILE -> file?.let { loadPatchLevelConfig(it) }
?: SystemLogger.warning("$PATCH_LEVEL_FILE was deleted.")
// Any change to an XML file is assumed to be a keybox.
// The cache in KeyBoxManager will handle reloading it on its next use.
else ->
@@ -307,10 +327,10 @@ object ConfigurationManager {
)
KeyBoxManager.invalidateCache(path)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
// Clear cached keys possibly containing old certificates
// Patched chains are stale; generated keys survive rotation
org.matrix.TEESimulator.interception.keystore.shim
.KeyMintSecurityLevelInterceptor
.clearAllGeneratedKeys("updating $file")
.invalidatePatchedChains("keybox change: $path")
}
}
}
@@ -354,7 +374,7 @@ object ConfigurationManager {
/** Waits for a system service to become available, with retries. */
private fun waitForSystemService(name: String): IBinder? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return ServiceManager.waitForService(name)
}
// Fallback for older Android versions.
@@ -41,7 +41,7 @@ abstract class BinderInterceptor : Binder() {
* Skips the original call and immediately returns a custom reply parcel to the caller. The
* provided parcel will be recycled after use.
*/
data class OverrideReply(val code: Int = 0, val reply: Parcel) : TransactionResult()
data class OverrideReply(val reply: Parcel, val code: Int = 0) : TransactionResult()
/**
* Modifies the transaction's input data before forwarding it to the original binder method.
@@ -248,6 +248,8 @@ abstract class BinderInterceptor : Binder() {
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
// Code used by the backdoor binder to register a new interceptor.
private const val REGISTER_INTERCEPTOR_CODE = 1
// Code used by the backdoor binder to unregister an interceptor.
private const val UNREGISTER_INTERCEPTOR_CODE = 2
// --- Hook Type Codes ---
// Indicates that the call is for a pre-transaction hook.
@@ -307,5 +309,21 @@ abstract class BinderInterceptor : Binder() {
reply.recycle()
}
}
/** Uses the backdoor binder to unregister an interceptor for a specific target service. */
fun unregister(backdoor: IBinder, target: IBinder) {
val data = Parcel.obtain()
val reply = Parcel.obtain()
try {
data.writeStrongBinder(target)
backdoor.transact(UNREGISTER_INTERCEPTOR_CODE, data, reply, 0)
SystemLogger.info("Unregistered interceptor for target: $target")
} catch (e: Exception) {
SystemLogger.error("Failed to unregister binder interceptor.", e)
} finally {
data.recycle()
reply.recycle()
}
}
}
}
@@ -52,7 +52,7 @@ object InterceptorUtils {
writeInt(KeyStore.NO_ERROR)
}
}
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/** Creates an `OverrideReply` parcel containing a raw byte array. */
@@ -62,7 +62,20 @@ object InterceptorUtils {
writeNoException()
writeByteArray(data)
}
return BinderInterceptor.TransactionResult.OverrideReply(KeyStore.NO_ERROR, parcel)
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/** Creates an `OverrideReply` parcel containing a typed array. */
fun <T : Parcelable> createTypedArrayReply(
array: Array<T>,
flags: Int = 0,
): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeNoException()
writeTypedArray(array, flags)
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/** Creates an `OverrideReply` parcel containing a Parcelable object. */
@@ -75,19 +88,20 @@ object InterceptorUtils {
writeNoException()
writeTypedObject(obj, flags)
}
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/**
* Extracts the true key alias from the keystore-prefixed string (e.g., "user_cert_my-alias" ->
* "my-alias").
* Extracts the base alias from a potentially prefixed alias string. For example, it converts
* "USRCERT_my_key" to "my_key".
*/
fun extractAlias(prefixedAlias: String): String {
val underscoreIndex = prefixedAlias.indexOf('_')
val secondUnderscoreIndex = prefixedAlias.indexOf('_', underscoreIndex + 1)
return if (secondUnderscoreIndex != -1) {
prefixedAlias.substring(secondUnderscoreIndex + 1)
return if (underscoreIndex != -1) {
// Return the part of the string after the first underscore.
prefixedAlias.substring(underscoreIndex + 1)
} else {
// If there's no underscore, return the original string.
prefixedAlias
}
}
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.SecurityLevel
import android.hardware.security.keymint.Tag
import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.IKeystoreService
@@ -12,30 +13,31 @@ 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.GeneratedKeyPersistence
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateHelper
/**
* Interceptor for the `IKeystoreService` on Android S (API 31) and newer.
*
* This version of Keystore delegates most cryptographic operations to `IKeystoreSecurityLevel`
* sub-services (for TEE, StrongBox, etc.). This interceptor's main role is to set up interceptors
* for those sub-services and to patch certificate chains on their way out.
*/
@SuppressLint("BlockedPrivateApi")
object Keystore2Interceptor : AbstractKeystoreInterceptor() {
// Transaction codes for the IKeystoreService interface methods we are interested in.
private val stubBinderClass = IKeystoreService.Stub::class.java
private val GET_KEY_ENTRY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "getKeyEntry")
InterceptorUtils.getTransactCode(stubBinderClass, "getKeyEntry")
private val DELETE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "deleteKey")
InterceptorUtils.getTransactCode(stubBinderClass, "deleteKey")
private val UPDATE_SUBCOMPONENT_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "updateSubcomponent")
private val LIST_ENTRIES_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "listEntries")
private val LIST_ENTRIES_BATCHED_TRANSACTION =
if (Build.VERSION.SDK_INT >= 34)
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
else null
private val transactionNames: Map<Int, String> by lazy {
IKeystoreService.Stub::class
.java
.declaredFields
stubBinderClass.declaredFields
.filter {
it.isAccessible = true
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
@@ -47,34 +49,30 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
override val processName = "keystore2"
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
/**
* This method is called once the main service is hooked. It proceeds to find and hook the
* security level sub-services (e.g., TEE, StrongBox).
*/
override fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
}
private fun setupSecurityLevelInterceptors(service: IKeystoreService, backdoor: IBinder) {
// Attempt to get and intercept the TEE security level service.
runCatching {
service.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT)?.let { tee ->
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
val interceptor =
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
register(backdoor, tee.asBinder(), interceptor)
interceptor.loadPersistedKeys()
}
}
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
// Attempt to get and intercept the StrongBox security level service.
runCatching {
service.getSecurityLevel(SecurityLevel.STRONGBOX)?.let { strongbox ->
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
val interceptor =
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
register(backdoor, strongbox.asBinder(), interceptor)
interceptor.loadPersistedKeys()
}
}
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
@@ -89,16 +87,44 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
callingPid: Int,
data: Parcel,
): TransactionResult {
if (code == GET_KEY_ENTRY_TRANSACTION || code == DELETE_KEY_TRANSACTION) {
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
return runCatching {
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
if (ListEntriesHandler.cacheParameters(txId, data, isBatchMode)) {
TransactionResult.Continue
} else {
TransactionResult.ContinueAndSkipPost
}
}
.getOrElse {
SystemLogger.error(
"[TX_ID: $txId] Failed to parse parameters for ${transactionNames[code]!!}",
it,
)
TransactionResult.ContinueAndSkipPost
}
} else if (
code == GET_KEY_ENTRY_TRANSACTION ||
code == DELETE_KEY_TRANSACTION ||
code == UPDATE_SUBCOMPONENT_TRANSACTION
) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
if (code == UPDATE_SUBCOMPONENT_TRANSACTION)
return handleUpdateSubcomponent(callingUid, data)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
val keyId = KeyIdentifier(callingUid, descriptor.alias)
@@ -136,7 +162,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
)
}
// Let most calls go through to the real service.
return TransactionResult.ContinueAndSkipPost
}
@@ -154,7 +179,22 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction
if (code == GET_KEY_ENTRY_TRANSACTION) {
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
return runCatching {
val updatedKeyDescriptors =
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, reply)
InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors)
}
.getOrElse {
SystemLogger.error(
"[TX_ID: $txId] Failed to update the result of ${transactionNames[code]!!}.",
it,
)
TransactionResult.SkipTransaction
}
} else if (code == GET_KEY_ENTRY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
@@ -180,8 +220,15 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
?.let { it.keyParameter.value.origin }
if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) {
SystemLogger.info("[TX_ID: $txId] Skip patching for imported keys.")
return TransactionResult.SkipTransaction
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
if (retainedChain == null) {
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
return TransactionResult.SkipTransaction
}
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
return InterceptorUtils.createTypedObjectReply(response)
}
if (originalChain == null || originalChain.size < 2) {
@@ -191,11 +238,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
return TransactionResult.SkipTransaction
}
// Perform the attestation patch.
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>
@@ -205,8 +248,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
)
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.
// Live patch fallback for keys created before simulator started
SystemLogger.info(
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
)
@@ -223,4 +265,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
}
return TransactionResult.SkipTransaction
}
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
val generatedKeyInfo =
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor?.nspace)
?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
val metadata = generatedKeyInfo.response.metadata
val publicCert = data.createByteArray()
val certificateChain = data.createByteArray()
metadata.certificate = publicCert
metadata.certificateChain = certificateChain
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
SystemLogger.verbose(
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
)
return InterceptorUtils.createSuccessReply(writeResultCode = false)
}
}
@@ -55,6 +55,28 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "attestKey")
}
private val transactionNames: Map<Int, String> by lazy {
IKeystoreService.Stub::class
.java
.declaredFields
.filter {
it.isAccessible = true
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
}
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
// A map to dispatch transaction handling for software key generation.
private val generateKeyHandlers:
Map<Int, (Long, Int, Int, Parcel) -> TransactionResult> by lazy {
mapOf(
GENERATE_KEY_TRANSACTION to ::handleGenerateKey,
GET_KEY_CHARACTERISTICS_TRANSACTION to ::handleGetKeyCharacteristics,
EXPORT_KEY_TRANSACTION to ::handleExportKey,
ATTEST_KEY_TRANSACTION to ::handleAttestKey,
)
}
override val serviceName = "android.security.keystore"
override val processName = "keystore"
override val injectionCommand = "exec ./inject `pidof keystore` libTEESimulator.so entry"
@@ -76,26 +98,33 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
data: Parcel,
): TransactionResult {
// This interceptor only needs to act on pre-transaction for software key generation.
// Handle 'generate' mode interceptions using the handler map.
if (ConfigurationManager.shouldGenerate(callingUid)) {
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
generateKeyHandlers[code]?.let { handler ->
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
return handler(txId, callingUid, callingPid, data)
}
} else if (ConfigurationManager.shouldPatch(callingUid)) {
// In patch mode, we only care about the 'get' transaction in onPostTransact.
if (code == GET_TRANSACTION) return TransactionResult.Continue
}
// Handle 'patch' mode interceptions for the 'get' transaction.
if (ConfigurationManager.shouldPatch(callingUid) && code == GET_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
return TransactionResult.Continue
}
// Default behavior for all other transactions.
logTransaction(
txId,
transactionNames[code] ?: "unknown code=$code",
callingUid,
callingPid,
true,
)
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())
@@ -133,7 +162,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
data: Parcel,
): TransactionResult {
return runCatching {
logTransaction(txId, "getKeyCharacteristics", uid, pid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val callback =
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
@@ -168,7 +196,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
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()!!)
@@ -206,7 +233,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
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())
@@ -230,7 +256,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
ByteArray(0),
)
params.attestationChallenge = challenge
params.attestationChallenge = challenge
}
val certificateChain =
@@ -271,6 +296,9 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
reply == null ||
InterceptorUtils.hasException(reply)
) {
SystemLogger.debug(
"[TX_ID: $txId] Skip parsing post-transaction for [target, code, reply]: [$target, $code, $reply]"
)
return TransactionResult.SkipTransaction
}
@@ -281,6 +309,9 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
val alias = data.readString() ?: ""
val extractedAlias = InterceptorUtils.extractAlias(alias)
val keyId = KeyIdentifier(callingUid, extractedAlias)
SystemLogger.debug(
"[TX_ID: $txId] Parsed $keyId during post-transaction of ${transactionNames[code]}"
)
when {
// Case 1: The app is requesting the leaf certificate.
@@ -378,6 +409,8 @@ private data class LegacyKeygenParameters(
algorithm = this.algorithm,
ecCurve = 0, // Not explicitly available in legacy args, but not critical
ecCurveName = this.ecCurveName ?: "",
blockMode = listOf<Int>(),
padding = listOf<Int>(),
purpose = this.purpose,
digest = this.digest,
rsaPublicExponent = this.rsaPublicExponent,
@@ -0,0 +1,142 @@
package org.matrix.TEESimulator.interception.keystore
import android.os.Parcel
import android.system.keystore2.Domain
import android.system.keystore2.IKeystoreService
import android.system.keystore2.KeyDescriptor
import java.util.TreeMap
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
/**
* Handler to intercept listEntries and listEntriesBatched transactions.
*
* References for all mentioned functions in AOSP:
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/database.rs
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/service.rs
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/utils.rs
*/
object ListEntriesHandler {
// Estimate for maximum size of a Binder response in bytes.
private const val RESPONSE_SIZE_LIMIT = 358400
// Parameters of AOSP function `list_key_entries` in utils.rs.
private data class ListEntriesParams(
val domain: Int,
val namespace: Long,
val startPastAlias: String?,
)
private val pendingParams = ConcurrentHashMap<Long, ListEntriesParams>()
// Based on AOSP function `estimate_safe_amount_to_return` in utils.rs.
private fun estimateSafeAmountToReturn(
keyDescriptors: Array<KeyDescriptor>,
responseSizeLimit: Int,
): Int {
var itemsToReturn = 0
var returnedBytes = 0
for (kd in keyDescriptors) {
// 4 bytes for the Domain enum
// 8 bytes for the Namespace long
returnedBytes += 4 + 8
kd.alias?.let { returnedBytes += 4 + it.toByteArray(Charsets.UTF_8).size }
kd.blob?.let { returnedBytes += 4 + it.size }
if (returnedBytes > responseSizeLimit) {
SystemLogger.warning(
"Key descriptors list (${keyDescriptors.size} items) may exceed binder size limit, returning $itemsToReturn items with estimated size: $returnedBytes bytes."
)
break
}
itemsToReturn++
}
return itemsToReturn
}
// Parse and store parameters for later use (in post-transaction).
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): Boolean {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val domain = data.readInt()
val namespace = data.readLong()
val startPastAlias = if (isBatchMode) data.readString() else null
// List entries is only supported for Domain::APP and Domain::SELINUX.
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
// Note that all generated keys belong to Domain::APP.
if (domain == Domain.APP) {
pendingParams[txId] = ListEntriesParams(domain, namespace, startPastAlias)
SystemLogger.debug("[TX_ID: $txId] Cached ${pendingParams[txId]}.")
return true
}
return false
}
// Merge software-backed keys with hardware-backed keys in the reply parcel.
fun injectGeneratedKeys(txId: Long, callingUid: Int, reply: Parcel): Array<KeyDescriptor> {
val params =
pendingParams.remove(txId)
?: throw IllegalStateException("No params found for listing entries")
// By default we use the calling uid as namespace if domain is Domain::APP.
// The namespace parameter is thus ignored for non-privileged applications.
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
val keysToInject =
extractGeneratedKeyDescriptors(callingUid, callingUid.toLong(), params.startPastAlias)
val originalList = reply.createTypedArray(KeyDescriptor.CREATOR)!!
val mergedArray = mergeKeyDescriptors(originalList, keysToInject)
// Limit response size to avoid binder buffer overflow.
// See AOSP function `list_key_entries` in utils.rs.
val safeAmountToReturn = estimateSafeAmountToReturn(mergedArray, RESPONSE_SIZE_LIMIT)
return if (safeAmountToReturn < mergedArray.size) {
SystemLogger.debug(
"[TX_ID: $txId] Listing entries are truncated [${mergedArray.size} -> $safeAmountToReturn] to avoid transaction overflow."
)
mergedArray.copyOfRange(0, safeAmountToReturn)
} else {
SystemLogger.debug(
"[TX_ID: $txId] Listing entries returns ${mergedArray.size} [injected: ${keysToInject.size}] keys."
)
mergedArray
}
}
// Merge hardware and software key descriptors into a single sorted array.
private fun mergeKeyDescriptors(
hardwareKeys: Array<KeyDescriptor>,
keysToInject: List<KeyDescriptor>,
): Array<KeyDescriptor> {
// Uses TreeMap to ensure alphabetical ordering and uniqueness (prefer injected keys).
val combinedMap = TreeMap<String, KeyDescriptor>()
hardwareKeys.forEach { key -> key.alias?.let { combinedMap[it] = key } }
keysToInject.forEach { key -> key.alias?.let { combinedMap[it] = key } }
return combinedMap.values.toTypedArray()
}
// Based on AOSP function `list_past_alias` in database.rs
private fun extractGeneratedKeyDescriptors(
uid: Int,
namespace: Long,
startPastAlias: String?,
): List<KeyDescriptor> {
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
.filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) }
.map { keyId ->
KeyDescriptor().apply {
this.domain = Domain.APP
this.nspace = namespace
this.alias = keyId.alias
this.blob = null
}
}
}
}
@@ -0,0 +1,376 @@
package org.matrix.TEESimulator.interception.keystore.shim
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.DataInputStream
import java.io.DataOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.security.KeyPair
import java.security.MessageDigest
import java.security.cert.Certificate
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateHelper
data class PersistedKeyData(
val uid: Int,
val alias: String,
val nspace: Long,
val securityLevel: Int,
val isAttestationKey: Boolean,
val algorithm: Int,
val keySize: Int,
val ecCurve: Int,
val purposes: List<Int>,
val digests: List<Int>,
val privateKeyBytes: ByteArray,
val certChainBytes: List<ByteArray>,
)
object GeneratedKeyPersistence {
private const val FORMAT_VERSION = 1
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
// Per-filename locks to prevent concurrent writes to the same key file
private val fileLocks = ConcurrentHashMap<String, ReentrantLock>()
private fun getLockForKey(filename: String): ReentrantLock {
return fileLocks.computeIfAbsent(filename) { ReentrantLock() }
}
fun save(
keyId: KeyIdentifier,
keyPair: KeyPair,
nspace: Long,
securityLevel: Int,
certChain: List<Certificate>,
algorithm: Int,
keySize: Int,
ecCurve: Int,
purposes: List<Int>,
digests: List<Int>,
isAttestationKey: Boolean,
) {
val filename = keyFileName(keyId.uid, keyId.alias)
val lock = getLockForKey(filename)
SystemLogger.debug("[Persistence] Acquiring lock for $filename")
lock.lock()
try {
SystemLogger.debug("[Persistence] Lock acquired for $filename")
runCatching {
PERSISTENCE_DIR.mkdirs()
val finalFile = File(PERSISTENCE_DIR, filename)
val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp")
try {
DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out ->
out.writeInt(FORMAT_VERSION)
out.writeInt(securityLevel)
out.writeInt(keyId.uid)
out.writeUTF(keyId.alias)
out.writeLong(nspace)
out.writeBoolean(isAttestationKey)
out.writeInt(algorithm)
out.writeInt(keySize)
out.writeInt(ecCurve)
out.writeInt(purposes.size)
purposes.forEach { out.writeInt(it) }
out.writeInt(digests.size)
digests.forEach { out.writeInt(it) }
val pkBytes = keyPair.private.encoded
out.writeInt(pkBytes.size)
out.write(pkBytes)
out.writeInt(certChain.size)
certChain.forEach { cert ->
val encoded = cert.encoded
out.writeInt(encoded.size)
out.write(encoded)
}
}
} catch (e: Exception) {
tmpFile.delete()
throw e
}
// Atomic rename — if this fails the tmp is left behind and cleaned on next deleteAll
if (!tmpFile.renameTo(finalFile)) {
tmpFile.delete()
throw IllegalStateException("Failed to atomically rename $tmpFile -> $finalFile")
}
// Verify write succeeded - catches disk-full or filesystem errors
if (!finalFile.exists() || finalFile.length() < 20) {
throw IOException("File write verification failed - possible disk full")
}
SystemLogger.debug("Persisted key: $keyId")
}.onFailure { e ->
SystemLogger.error("Failed to persist key $keyId", e)
}
} finally {
lock.unlock()
SystemLogger.debug("[Persistence] Lock released for $filename")
}
}
fun delete(keyId: KeyIdentifier) {
runCatching {
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
if (file.exists()) {
if (file.delete()) {
SystemLogger.debug("Deleted persisted key: $keyId")
} else {
SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
}
} else {
SystemLogger.debug("No persisted file to delete for: $keyId")
}
}.onFailure { e ->
SystemLogger.error("Failed to delete persisted key $keyId", e)
}
}
fun deleteAll() {
runCatching {
if (!PERSISTENCE_DIR.exists()) {
SystemLogger.debug("No persistent_keys directory, nothing to delete")
return
}
val files = PERSISTENCE_DIR.listFiles()
if (files == null) {
SystemLogger.warning("Cannot list persistent_keys directory")
return
}
var count = 0
files.forEach { file ->
if (file.name.endsWith(".bin") || file.name.endsWith(".tmp")) {
if (file.delete()) count++
}
}
SystemLogger.info("Deleted $count persisted key files")
}.onFailure { e ->
SystemLogger.error("Failed to delete all persisted keys", e)
}
}
fun loadAll(securityLevel: Int): List<PersistedKeyData> {
if (!PERSISTENCE_DIR.exists()) {
SystemLogger.debug("No persistent_keys directory, nothing to load")
return emptyList()
}
val files = PERSISTENCE_DIR.listFiles { _, name -> name.endsWith(".bin") }
if (files == null) {
SystemLogger.warning("Cannot read persistent_keys directory")
return emptyList()
}
if (files.isEmpty()) {
SystemLogger.debug("No persisted key files found")
return emptyList()
}
SystemLogger.info("Found ${files.size} persisted key files to process")
val result = mutableListOf<PersistedKeyData>()
for (file in files) {
runCatching {
DataInputStream(BufferedInputStream(FileInputStream(file))).use { input ->
val version = input.readInt()
if (version != FORMAT_VERSION) {
SystemLogger.warning(
"Skipping ${file.name}: unknown format version $version"
)
return@runCatching
}
val storedSecLevel = input.readInt()
val uid = input.readInt()
val alias = input.readUTF()
val nspace = input.readLong()
val isAttestKey = input.readBoolean()
val algo = input.readInt()
val kSize = input.readInt()
val curve = input.readInt()
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
val purposes = (0 until purposeCount).map { input.readInt() }
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
val digests = (0 until digestCount).map { input.readInt() }
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
val pkBytes = ByteArray(pkLen)
input.readFully(pkBytes)
val certCount = requireBounds(input.readInt(), 10, "certCount")
val certChainBytes = (0 until certCount).map {
val certLen = requireBounds(input.readInt(), 65536, "certLen")
val certBytes = ByteArray(certLen)
input.readFully(certBytes)
certBytes
}
if (storedSecLevel == securityLevel) {
result.add(
PersistedKeyData(
uid = uid,
alias = alias,
nspace = nspace,
securityLevel = storedSecLevel,
isAttestationKey = isAttestKey,
algorithm = algo,
keySize = kSize,
ecCurve = curve,
purposes = purposes,
digests = digests,
privateKeyBytes = pkBytes,
certChainBytes = certChainBytes,
)
)
}
}
}.onFailure { e ->
SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e)
}
}
SystemLogger.info("Loaded ${result.size} persisted keys for security level $securityLevel")
return result
}
// Re-persist updates the cert chain for an already-persisted key without
// reconstructing authorization parameters from the response. This avoids
// pulling keymint Tag dependencies into this file and is correct because
// the only field that changes post-generation is the patched cert chain.
fun rePersistIfNeeded(
callingUid: Int,
generatedKeyInfo: KeyMintSecurityLevelInterceptor.GeneratedKeyInfo,
) {
val metadata = generatedKeyInfo.response.metadata
if (metadata == null) {
SystemLogger.debug("rePersist: no metadata, skipping")
return
}
val secLevel = metadata.keySecurityLevel
val entry = KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) ->
id.uid == callingUid && info.nspace == generatedKeyInfo.nspace
}
if (entry == null) {
SystemLogger.debug("rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}")
return
}
val keyId = entry.key
val filename = keyFileName(keyId.uid, keyId.alias)
val existing = File(PERSISTENCE_DIR, filename)
if (!existing.exists()) {
SystemLogger.debug("rePersist: no existing file for $keyId, skipping")
return
}
val newChain = CertificateHelper.getCertificateChain(metadata)
if (newChain == null) {
SystemLogger.warning("rePersist: could not extract cert chain for $keyId")
return
}
val persisted = runCatching {
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
val version = input.readInt()
if (version != FORMAT_VERSION) {
SystemLogger.warning("rePersist: unknown format version $version for $keyId")
return
}
readPersistedKeyData(input)
}
}.getOrNull()
if (persisted == null) {
SystemLogger.warning("rePersist: failed to read existing data for $keyId")
return
}
save(
keyId = keyId,
keyPair = generatedKeyInfo.keyPair,
nspace = generatedKeyInfo.nspace,
securityLevel = secLevel,
certChain = newChain.toList(),
algorithm = persisted.algorithm,
keySize = persisted.keySize,
ecCurve = persisted.ecCurve,
purposes = persisted.purposes,
digests = persisted.digests,
isAttestationKey = persisted.isAttestationKey,
)
SystemLogger.debug("Re-persisted key $keyId with updated cert chain")
}
// Corrupted binary files can have arbitrary length fields — cap allocations
private fun requireBounds(value: Int, max: Int, name: String): Int {
require(value in 0..max) { "$name out of bounds: $value (max $max)" }
return value
}
private fun keyFileName(uid: Int, alias: String): String {
val digest = MessageDigest.getInstance("SHA-256")
.digest("$uid:$alias".toByteArray(Charsets.UTF_8))
return digest.joinToString("") { "%02x".format(it) } + ".bin"
}
// Reads all fields after version has already been consumed
private fun readPersistedKeyData(input: DataInputStream): PersistedKeyData {
val secLevel = input.readInt()
val uid = input.readInt()
val alias = input.readUTF()
val nspace = input.readLong()
val isAttestKey = input.readBoolean()
val algo = input.readInt()
val kSize = input.readInt()
val curve = input.readInt()
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
val purposes = (0 until purposeCount).map { input.readInt() }
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
val digests = (0 until digestCount).map { input.readInt() }
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
val pkBytes = ByteArray(pkLen)
input.readFully(pkBytes)
val certCount = requireBounds(input.readInt(), 10, "certCount")
val certChainBytes = (0 until certCount).map {
val certLen = requireBounds(input.readInt(), 65536, "certLen")
val certBytes = ByteArray(certLen)
input.readFully(certBytes)
certBytes
}
return PersistedKeyData(
uid = uid,
alias = alias,
nspace = nspace,
securityLevel = secLevel,
isAttestationKey = isAttestKey,
algorithm = algo,
keySize = kSize,
ecCurve = curve,
purposes = purposes,
digests = digests,
privateKeyBytes = pkBytes,
certChainBytes = certChainBytes,
)
}
}
@@ -1,5 +1,6 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.KeyPurpose
@@ -7,9 +8,15 @@ import android.hardware.security.keymint.Tag
import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.*
import java.io.ByteArrayInputStream
import java.security.KeyFactory
import java.security.KeyPair
import java.security.SecureRandom
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
@@ -20,17 +27,16 @@ import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper
/**
* Intercepts calls to an `IKeystoreSecurityLevel` service (e.g., TEE or StrongBox). This is where
* the core logic for key generation and import handling for modern Android resides.
*/
class KeyMintSecurityLevelInterceptor(
private val original: IKeystoreSecurityLevel,
private val securityLevel: Int,
) : BinderInterceptor() {
// --- Data Structures for State Management ---
data class GeneratedKeyInfo(val keyPair: KeyPair, val response: KeyEntryResponse)
data class GeneratedKeyInfo(
val keyPair: KeyPair,
val nspace: Long,
val response: KeyEntryResponse,
)
override fun onPreTransact(
txId: Long,
@@ -41,33 +47,39 @@ class KeyMintSecurityLevelInterceptor(
callingPid: Int,
data: Parcel,
): TransactionResult {
if (code == GENERATE_KEY_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
val shouldSkip = ConfigurationManager.shouldSkipUid(callingUid)
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)
when (code) {
GENERATE_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
?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info("Handling post-${transactionNames[code]} ${alias}")
return TransactionResult.Continue
} else {
logTransaction(
txId,
transactionNames[code] ?: "unknown code=$code",
callingUid,
callingPid,
true,
)
if (!shouldSkip) return handleGenerateKey(txId, callingUid, data)
}
CREATE_OPERATION_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleCreateOperation(txId, callingUid, data)
}
IMPORT_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
SystemLogger.info(
"[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]"
)
return TransactionResult.Continue
}
}
logTransaction(
txId,
transactionNames[code] ?: "unknown code=$code",
callingUid,
callingPid,
true,
)
return TransactionResult.ContinueAndSkipPost
}
@@ -82,6 +94,11 @@ class KeyMintSecurityLevelInterceptor(
reply: Parcel?,
resultCode: Int,
): TransactionResult {
if (code == GENERATE_KEY_TRANSACTION && hardwareKeygenTxIds.remove(txId)) {
val remaining = hardwareKeygenCount(callingUid).decrementAndGet()
SystemLogger.info("[TX_ID: $txId] PERMIT_RELEASED uid=$callingUid concurrent_remaining=$remaining result=${if (resultCode == 0) "OK" else "ERROR($resultCode)"}")
}
// We only care about successful transactions.
if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction
@@ -93,7 +110,49 @@ class KeyMintSecurityLevelInterceptor(
val keyDescriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
cleanupKeyData(KeyIdentifier(callingUid, keyDescriptor.alias))
// Evict generated key data but retain patched chains so detectors
// can't use importKey to force unpatched getKeyEntry responses.
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
if (generatedKeys.remove(keyId) != null) {
SystemLogger.debug("Remove generated key on importKey $keyId")
GeneratedKeyPersistence.delete(keyId)
}
attestationKeys.remove(keyId)
} else if (code == CREATE_OPERATION_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params)
val forced = data.readBoolean()
if (forced)
SystemLogger.verbose(
"[TX_ID: $txId] Current operation has a very high pruning power."
)
val response: CreateOperationResponse =
reply.readTypedObject(CreateOperationResponse.CREATOR)!!
SystemLogger.verbose(
"[TX_ID: $txId] CreateOperationResponse: ${response.iOperation} ${response.operationChallenge}"
)
// Intercept the IKeystoreOperation binder
response.iOperation?.let { operation ->
val operationBinder = operation.asBinder()
if (!interceptedOperations.containsKey(operationBinder)) {
SystemLogger.info("Found new IKeystoreOperation. Registering interceptor...")
val backdoor = getBackdoor(target)
if (backdoor != null) {
val interceptor = OperationInterceptor(operation, backdoor)
register(backdoor, operationBinder, interceptor)
interceptedOperations[operationBinder] = interceptor
} else {
SystemLogger.error(
"Failed to get backdoor to register OperationInterceptor."
)
}
}
}
} else if (code == GENERATE_KEY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
@@ -109,26 +168,74 @@ class KeyMintSecurityLevelInterceptor(
// Cache the newly patched chain to ensure consistency across subsequent API calls.
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val key = metadata.key!!
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
patchedChains[keyId] = newChain
SystemLogger.debug("Cached patched certificate chain for $keyId.")
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
// We must clean up cached generated keys before storing the patched chain
cleanupKeyData(keyId)
patchedChains[keyId] = newChain
SystemLogger.debug(
"Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])"
)
return InterceptorUtils.createTypedObjectReply(metadata)
}
}
return TransactionResult.SkipTransaction
}
/**
* Handles the `generateKey` transaction. Based on the configuration for the calling UID, it
* either generates a key in software or lets the call pass through to the hardware.
*/
private fun handleGenerateKey(callingUid: Int, data: Parcel): TransactionResult {
private fun handleCreateOperation(
txId: Long,
callingUid: Int,
data: Parcel,
): TransactionResult {
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
// An operation must use the KEY_ID domain.
if (keyDescriptor.domain != Domain.KEY_ID) {
return TransactionResult.ContinueAndSkipPost
}
val nspace = keyDescriptor.nspace
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
if (generatedKeyInfo == null) {
SystemLogger.debug(
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
)
return TransactionResult.Continue
}
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params)
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
val operationBinder = SoftwareOperationBinder(softwareOperation)
val response =
CreateOperationResponse().apply {
iOperation = operationBinder
operationChallenge = null
}
return InterceptorUtils.createTypedObjectReply(response)
}
private fun handleGenerateKey(txId: Long, callingUid: Int, data: Parcel): TransactionResult {
if (data.dataSize() > MAX_ALIAS_LENGTH) {
SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes")
return TransactionResult.ContinueAndSkipPost
}
return runCatching {
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR)
SystemLogger.debug(
"Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}"
)
@@ -139,8 +246,6 @@ class KeyMintSecurityLevelInterceptor(
parsedParams.purpose.size == 1 &&
parsedParams.purpose.contains(KeyPurpose.ATTEST_KEY)
// Determine if we need to generate a key based on config or
// if it's an attestation request in patch mode.
val needsSoftwareGeneration =
ConfigurationManager.shouldGenerate(callingUid) ||
(ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) ||
@@ -148,37 +253,29 @@ class KeyMintSecurityLevelInterceptor(
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
if (needsSoftwareGeneration) {
SystemLogger.info("Generating software key for ${keyId}.")
// Generate the key pair and certificate chain.
val keyData =
CertificateGenerator.generateAttestedKeyPair(
callingUid,
keyDescriptor.alias,
attestationKey?.alias,
parsedParams,
securityLevel,
) ?: throw Exception("CertificateGenerator failed to create key pair.")
// Store the generated key data.
val response =
buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, response)
if (isAttestKeyRequest) attestationKeys.add(keyId)
// Return the metadata of our generated key, skipping the real hardware call.
val resultParcel =
Parcel.obtain().apply {
writeNoException()
writeTypedObject(response.metadata, 0)
}
return TransactionResult.OverrideReply(0, resultParcel)
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
} else if (parsedParams.attestationChallenge != null) {
val windowUsed = hardwareKeygenWindowCount(callingUid)
val concurrentUsed = hardwareKeygenCount(callingUid).get()
// Sliding window rate limit
if (windowUsed >= MAX_HW_KEYGEN_PER_WINDOW) {
SystemLogger.info("[TX_ID: $txId] RATE_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=$concurrentUsed → software fallback")
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
}
// Concurrent cap
if (hardwareKeygenCount(callingUid).incrementAndGet() > MAX_CONCURRENT_HW_KEYGEN_PER_UID) {
hardwareKeygenCount(callingUid).decrementAndGet()
SystemLogger.info("[TX_ID: $txId] CONCURRENT_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → software fallback")
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
}
// Both checks passed — commit the window permit and forward to hardware TEE
recordHardwareKeygen(callingUid)
hardwareKeygenTxIds.add(txId)
SystemLogger.info("[TX_ID: $txId] HARDWARE_KEYGEN uid=$callingUid window=${windowUsed + 1}/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → forwarding to TEE")
return TransactionResult.Continue
}
// If not generating, clear any stale state for this alias and let the call proceed.
cleanupKeyData(keyId)
TransactionResult.ContinueAndSkipPost
}
@@ -188,9 +285,43 @@ class KeyMintSecurityLevelInterceptor(
}
}
/**
* Constructs a fake `KeyEntryResponse` that mimics a real response from the Keystore service.
*/
private fun doSoftwareKeyGen(
callingUid: Int,
keyDescriptor: KeyDescriptor,
attestationKey: KeyDescriptor?,
parsedParams: KeyMintAttestation,
keyId: KeyIdentifier,
isAttestKeyRequest: Boolean,
): TransactionResult {
keyDescriptor.nspace = secureRandom.nextLong()
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
val keyData = CertificateGenerator.generateAttestedKeyPair(
callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel,
) ?: throw Exception("CertificateGenerator failed to create key pair.")
cleanupKeyData(keyId)
val response = buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
if (isAttestKeyRequest) attestationKeys.add(keyId)
GeneratedKeyPersistence.save(
keyId = keyId,
keyPair = keyData.first,
nspace = keyDescriptor.nspace,
securityLevel = securityLevel,
certChain = keyData.second.toList(),
algorithm = parsedParams.algorithm,
keySize = parsedParams.keySize,
ecCurve = parsedParams.ecCurve,
purposes = parsedParams.purpose,
digests = parsedParams.digest,
isAttestationKey = isAttestKeyRequest,
)
return InterceptorUtils.createTypedObjectReply(response.metadata)
}
private fun buildKeyEntryResponse(
chain: List<Certificate>,
params: KeyMintAttestation,
@@ -209,12 +340,130 @@ class KeyMintSecurityLevelInterceptor(
}
}
fun loadPersistedKeys() {
val records = GeneratedKeyPersistence.loadAll(securityLevel)
if (records.isEmpty()) {
SystemLogger.debug("No persisted keys to restore for security level $securityLevel")
return
}
SystemLogger.info("Restoring ${records.size} persisted keys for security level $securityLevel")
for (record in records) {
runCatching {
val keyId = KeyIdentifier(record.uid, record.alias)
if (generatedKeys.containsKey(keyId)) {
SystemLogger.debug("Skipping already-loaded key: $keyId")
return@runCatching
}
val algorithmName = when (record.algorithm) {
Algorithm.EC -> "EC"
Algorithm.RSA -> "RSA"
else -> throw IllegalArgumentException("Unknown algorithm: ${record.algorithm}")
}
val keyFactory = KeyFactory.getInstance(algorithmName)
val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(record.privateKeyBytes))
val certFactory = CertificateFactory.getInstance("X.509")
val certChain = record.certChainBytes.map { bytes ->
certFactory.generateCertificate(ByteArrayInputStream(bytes))
}
require(certChain.isNotEmpty()) { "Persisted key has empty certificate chain" }
val publicKey = certChain[0].publicKey
val keyPair = KeyPair(publicKey, privateKey)
val descriptor = KeyDescriptor().apply {
domain = Domain.APP
nspace = record.nspace
alias = record.alias
blob = null
}
val attestation = KeyMintAttestation(
keySize = record.keySize,
algorithm = record.algorithm,
ecCurve = record.ecCurve,
ecCurveName = "",
blockMode = emptyList(),
padding = emptyList(),
purpose = record.purposes,
digest = record.digests,
rsaPublicExponent = null,
certificateSerial = null,
certificateSubject = null,
certificateNotBefore = null,
certificateNotAfter = null,
attestationChallenge = null,
brand = null,
device = null,
product = null,
serial = null,
imei = null,
meid = null,
manufacturer = null,
model = null,
secondImei = null,
)
val response = buildKeyEntryResponse(certChain, attestation, descriptor)
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response)
if (record.isAttestationKey) attestationKeys.add(keyId)
SystemLogger.debug("Restored persisted key: $keyId")
}.onFailure {
SystemLogger.error("Failed to restore key: uid=${record.uid} alias=${record.alias}", it)
}
}
SystemLogger.info("Key restoration complete. Total in memory: ${generatedKeys.size}")
}
companion object {
// Transaction codes for IKeystoreSecurityLevel interface.
private val secureRandom = SecureRandom()
// Maximum alias length to prevent binder buffer exhaustion (Issue #109)
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
private const val MAX_ALIAS_LENGTH = 256 * 1024
private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2
// Sliding window: max hardware keygen permits per UID within the burst window
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
private const val BURST_WINDOW_MS = 30_000L
private val uidHardwareKeygenCount = ConcurrentHashMap<Int, AtomicInteger>()
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
private fun hardwareKeygenCount(uid: Int): AtomicInteger =
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
private fun hardwareKeygenWindowCount(uid: Int): Int {
val now = System.currentTimeMillis()
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
synchronized(timestamps) {
timestamps.removeAll { now - it > BURST_WINDOW_MS }
return timestamps.size
}
}
private fun recordHardwareKeygen(uid: Int) {
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
synchronized(timestamps) {
timestamps.add(System.currentTimeMillis())
}
}
private val GENERATE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
private val IMPORT_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "importKey")
private val CREATE_OPERATION_TRANSACTION =
InterceptorUtils.getTransactCode(
IKeystoreSecurityLevel.Stub::class.java,
"createOperation",
)
private val transactionNames: Map<Int, String> by lazy {
IKeystoreSecurityLevel.Stub::class
@@ -227,17 +476,23 @@ class KeyMintSecurityLevelInterceptor(
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
// Stores keys generated entirely in software.
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
// Caches patched certificate chains to prevent re-generation and signature inconsistencies.
// Caches patched 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>()
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
// --- Public Accessors for Other Interceptors ---
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
generatedKeys[keyId]?.response
fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? {
if (nspace == null || nspace == 0L) return null
return generatedKeys.entries
.filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid }
.find { (_, info) -> info.nspace == nspace }
?.value
}
fun getPatchedChain(keyId: KeyIdentifier): Array<Certificate>? = patchedChains[keyId]
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
@@ -245,6 +500,7 @@ class KeyMintSecurityLevelInterceptor(
fun cleanupKeyData(keyId: KeyIdentifier) {
if (generatedKeys.remove(keyId) != null) {
SystemLogger.debug("Remove generated key ${keyId}")
GeneratedKeyPersistence.delete(keyId)
}
if (patchedChains.remove(keyId) != null) {
SystemLogger.debug("Remove patched chain for ${keyId}")
@@ -254,33 +510,37 @@ class KeyMintSecurityLevelInterceptor(
}
}
// Clears all cached keys.
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
unregister(backdoor, operationBinder)
if (interceptedOperations.remove(operationBinder) != null) {
SystemLogger.debug("Removed operation interceptor for binder: $operationBinder")
}
}
fun invalidatePatchedChains(reason: String? = null) {
val count = patchedChains.size
if (count == 0) return
val reasonMessage = reason?.let { " due to $it" } ?: ""
patchedChains.clear()
SystemLogger.info("Invalidated $count patched cert chains$reasonMessage.")
}
fun clearAllGeneratedKeys(reason: String? = null) {
val count = generatedKeys.size
val reasonMessage = reason?.let { " due to $it" } ?: ""
generatedKeys.clear()
patchedChains.clear()
attestationKeys.clear()
GeneratedKeyPersistence.deleteAll()
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
}
}
}
/**
* Extension function to convert parsed `KeyMintAttestation` parameters back into an array of
* `Authorization` objects for the fake `KeyMetadata`. This version correctly handles the
* instantiation of Authorization objects.
*/
private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Authorization> {
val authList = mutableListOf<Authorization>()
/**
* Helper function to create a fully-formed Authorization object.
*
* @param tag The KeyMint tag (e.g., Tag.ALGORITHM).
* @param value The value for the tag, wrapped in a KeyParameterValue.
* @return A populated Authorization object.
*/
fun createAuth(tag: Int, value: KeyParameterValue): Authorization {
val param =
KeyParameter().apply {
@@ -293,7 +553,6 @@ private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Autho
}
}
// Use the helper to add each authorization entry cleanly.
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
@@ -0,0 +1,58 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.IKeystoreOperation
import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
/**
* Intercepts calls to an `IKeystoreOperation` service. This is used to log the data manipulation
* methods of a cryptographic operation.
*/
class OperationInterceptor(
private val original: IKeystoreOperation,
private val backdoor: IBinder,
) : BinderInterceptor() {
override fun onPreTransact(
txId: Long,
target: IBinder,
code: Int,
flags: Int,
callingUid: Int,
callingPid: Int,
data: Parcel,
): TransactionResult {
val methodName = transactionNames[code] ?: "unknown code=$code"
logTransaction(txId, methodName, callingUid, callingPid, true)
if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) {
KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor)
}
return TransactionResult.ContinueAndSkipPost
}
companion object {
private val UPDATE_AAD_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "updateAad")
private val UPDATE_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "update")
private val FINISH_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "finish")
private val ABORT_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
private val transactionNames: Map<Int, String> by lazy {
IKeystoreOperation.Stub::class
.java
.declaredFields
.filter {
it.isAccessible = true
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
}
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
}
}
}
@@ -0,0 +1,216 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.BlockMode
import android.hardware.security.keymint.Digest
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.PaddingMode
import android.os.RemoteException
import android.system.keystore2.IKeystoreOperation
import java.security.KeyPair
import java.security.Signature
import java.security.SignatureException
import javax.crypto.Cipher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger
// A sealed interface to represent the different cryptographic operations we can perform.
private sealed interface CryptoPrimitive {
fun update(data: ByteArray?): ByteArray?
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
fun abort()
}
// Helper object to map KeyMint constants to JCA algorithm strings.
private object JcaAlgorithmMapper {
fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
val digest =
when (params.digest.firstOrNull()) {
Digest.SHA_2_256 -> "SHA256"
Digest.SHA_2_384 -> "SHA384"
Digest.SHA_2_512 -> "SHA512"
else -> "NONE"
}
val keyAlgo =
when (params.algorithm) {
Algorithm.EC -> "ECDSA"
Algorithm.RSA -> "RSA"
else ->
throw IllegalArgumentException(
"Unsupported signature algorithm: ${params.algorithm}"
)
}
return "${digest}with${keyAlgo}"
}
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
val keyAlgo =
when (params.algorithm) {
Algorithm.RSA -> "RSA"
Algorithm.AES -> "AES"
else ->
throw IllegalArgumentException(
"Unsupported cipher algorithm: ${params.algorithm}"
)
}
val blockMode =
when (params.blockMode.firstOrNull()) {
BlockMode.ECB -> "ECB"
BlockMode.CBC -> "CBC"
BlockMode.GCM -> "GCM"
else -> "ECB" // Default for RSA
}
val padding =
when (params.padding.firstOrNull()) {
PaddingMode.NONE -> "NoPadding"
PaddingMode.PKCS7 -> "PKCS7Padding"
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
PaddingMode.RSA_OAEP -> "OAEPPadding"
else -> "NoPadding" // Default for GCM
}
return "$keyAlgo/$blockMode/$padding"
}
}
// Concrete implementation for Signing.
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
initSign(keyPair.private)
}
override fun update(data: ByteArray?): ByteArray? {
if (data != null) signature.update(data)
return null
}
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray {
if (data != null) update(data)
return this.signature.sign()
}
override fun abort() {}
}
// Concrete implementation for Verification.
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
initVerify(keyPair.public)
}
override fun update(data: ByteArray?): ByteArray? {
if (data != null) signature.update(data)
return null
}
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
if (data != null) update(data)
if (signature == null) throw SignatureException("Signature to verify is null")
if (!this.signature.verify(signature)) {
// Throwing an exception is how Keystore signals verification failure.
throw SignatureException("Signature verification failed")
}
// A successful verification returns no data.
return null
}
override fun abort() {}
}
// Concrete implementation for Encryption/Decryption.
private class CipherPrimitive(
keyPair: KeyPair,
params: KeyMintAttestation,
private val opMode: Int,
) : CryptoPrimitive {
private val cipher: Cipher =
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private
init(opMode, key)
}
override fun update(data: ByteArray?): ByteArray? =
if (data != null) cipher.update(data) else null
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
if (data != null) cipher.doFinal(data) else cipher.doFinal()
override fun abort() {}
}
/**
* A software-only implementation of a cryptographic operation. This class acts as a controller,
* delegating to a specific cryptographic primitive based on the operation's purpose.
*/
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
// This now holds the specific strategy object (Signer, Verifier, etc.)
private val primitive: CryptoPrimitive
init {
// The "Strategy" pattern: choose the implementation based on the purpose.
// For simplicity, we only consider the first purpose listed.
val purpose = params.purpose.firstOrNull()
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
primitive =
when (purpose) {
KeyPurpose.SIGN -> Signer(keyPair, params)
KeyPurpose.VERIFY -> Verifier(keyPair, params)
KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE)
KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE)
else ->
throw UnsupportedOperationException("Unsupported operation purpose: $purpose")
}
}
fun update(data: ByteArray?): ByteArray? {
try {
return primitive.update(data)
} catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
throw e
}
}
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
try {
val result = primitive.finish(data, signature)
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
return result
} catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
// Re-throw the exception so the binder can report it to the client.
throw e
}
}
fun abort() {
primitive.abort()
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
}
}
/** The Binder interface for our [SoftwareOperation]. */
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
IKeystoreOperation.Stub() {
@Throws(RemoteException::class)
override fun update(input: ByteArray?): ByteArray? {
return operation.update(input)
}
@Throws(RemoteException::class)
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
return operation.finish(input, signature)
}
@Throws(RemoteException::class)
override fun abort() {
operation.abort()
}
}
@@ -1,11 +1,6 @@
package org.matrix.TEESimulator.logging
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.Digest
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.Tag
import android.hardware.security.keymint.*
import java.math.BigInteger
import java.nio.charset.StandardCharsets
import java.util.Date
@@ -34,7 +29,23 @@ object KeyMintParameterLogger {
.associate { field -> (field.get(null) as Int) to field.name }
}
private val purposeNames: Map<Int, String> by lazy {
val blockModeNames: Map<Int, String> by lazy {
BlockMode::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
val paddingNames: Map<Int, String> by lazy {
PaddingMode::class
.java
.fields
.filter { it.type == Int::class.java }
.associate { field -> (field.get(null) as Int) to field.name }
}
val purposeNames: Map<Int, String> by lazy {
KeyPurpose::class
.java
.fields
@@ -69,7 +80,9 @@ object KeyMintParameterLogger {
val formattedValue: String =
when (param.tag) {
Tag.ALGORITHM -> algorithmNames[value.algorithm]
Tag.BLOCK_MODE -> blockModeNames[value.blockMode]
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
Tag.PADDING -> paddingNames[value.paddingMode]
Tag.PURPOSE -> purposeNames[value.keyPurpose]
Tag.DIGEST -> digestNames[value.digest]
Tag.AUTH_TIMEOUT,
@@ -1,6 +1,7 @@
package org.matrix.TEESimulator.pki
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.KeyPurpose
import android.os.Build
import android.util.Pair
import java.math.BigInteger
@@ -20,6 +21,7 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.AttestationConstants
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
@@ -80,6 +82,12 @@ object CertificateGenerator {
params: KeyMintAttestation,
securityLevel: Int,
): List<Certificate>? {
val challenge = params.attestationChallenge
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT)
throw IllegalArgumentException(
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
)
return runCatching {
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
@@ -180,6 +188,22 @@ object CertificateGenerator {
}
}
/** Maps KeyPurpose values to X.509 KeyUsage bits per KeyCreationResult.aidl spec */
private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int {
var bits = 0
for (purpose in purposes) {
bits = bits or when (purpose) {
KeyPurpose.SIGN -> KeyUsage.digitalSignature
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement
KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign
else -> 0
}
}
return bits
}
/** Constructs a new X.509 certificate with a simulated attestation extension. */
private fun buildCertificate(
subjectKeyPair: KeyPair,
@@ -204,8 +228,11 @@ object CertificateGenerator {
subjectKeyPair.public,
)
// Add standard extensions.
builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign))
// Add KeyUsage extension only if purposes map to valid bits
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
if (keyUsageBits != 0) {
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
}
// Add our custom, simulated attestation extension.
builder.addExtension(
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
@@ -239,11 +239,12 @@ object AndroidDeviceUtils {
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.
// Resolve from live system prop — matches what detectors see via getprop,
// even when PIF has spoofed ro.build.version.security_patch via resetprop
resolvedValue.equals("prop", ignoreCase = true) ->
parsePatchLevelValue(SystemProperties.get("ro.build.version.security_patch", ""), isLong)
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
// Otherwise, parse the resolved date string.
else -> parsePatchLevelValue(resolvedValue, isLong)
}
}
@@ -6,7 +6,11 @@ package org.matrix.TEESimulator.util
*
* @return A new string with each line individually trimmed.
*/
fun String.trimLines(): String = this.trim().lines().joinToString("\n") { it.trim() }
fun String.trimLines(): String =
this.trim()
.lines()
.filter { !it.trim().startsWith("<!--") }
.joinToString("\n") { it.trim() }
/**
* Converts a ByteArray to its hexadecimal string representation.
+2 -2
View File
@@ -1,8 +1,8 @@
[versions]
agp = "8.13.1"
agp = "8.13.2"
annotation = "1.9.1"
jdk18on = "1.83"
kotlin = "2.2.21"
kotlin = "2.3.0"
ktfmt = "0.25.0"
[libraries]
+29
View File
@@ -0,0 +1,29 @@
#!/system/bin/sh
MODDIR=${0%/*}
CONFIG_DIR=/data/adb/tricky_store
echo "============================================"
echo " TEESimulator — Key Storage Maintenance"
echo "============================================"
echo ""
if [ -d "$CONFIG_DIR/persistent_keys" ]; then
KEY_COUNT=$(find "$CONFIG_DIR/persistent_keys" -name "*.bin" 2>/dev/null | wc -l)
STORAGE_SIZE=$(du -sh "$CONFIG_DIR/persistent_keys" 2>/dev/null | cut -f1)
echo " Cached keys found : $KEY_COUNT"
echo " Storage used : $STORAGE_SIZE"
echo ""
rm -rf "$CONFIG_DIR/persistent_keys"
mkdir -p "$CONFIG_DIR/persistent_keys"
echo " [OK] All cached attestation keys purged"
echo " [OK] Fresh keys will generate on next request"
else
echo " No persistent key storage found"
echo " Nothing to clear"
fi
echo ""
echo "============================================"
+35 -13
View File
@@ -1,21 +1,43 @@
TEESimulator 3.0 is a significant update focused on powerful new configuration options, major improvements to stealth, and enhanced stability.
## TEESimulator v3.2: Anti-Detection Hardening & Key Persistence
#### ✨ **Highlights & New Features**
This release hardens TEESimulator against active attestation probing by detector apps (DuckDetector, Luna, GarfieldHan) while introducing persistent key storage that survives daemon restarts and reboots.
* **🎯 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.
### Anti-Detection Hardening
#### 🛡️ **Stealth & Evasion Upgrades**
* **Per-UID Hardware Keygen Rate Limiter**: Caps hardware key generation at 2 per 30-second window with 2 max concurrent requests per UID. Overflow requests fall back to software certificate generation, preventing binder thread starvation from flood attacks.
* **importKey Eviction Defense**: Retains patched attestation chains when `importKey` overwrites an attested alias. Blocks the generate-then-import attack vector used by GarfieldHan and similar detectors.
* **Native Binder Payload Cap**: Bypasses interception for payloads exceeding 256KB, preventing thread starvation from oversized binder transactions.
* **Oversized Alias Rejection**: Rejects aliases that would exhaust the binder buffer, closing another flooding vector.
* **⛓️ 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.
### Security Patch Consistency
#### 🐛 **Bug Fixes & Reliability**
* **Three-Way Patch Level Alignment**: When `system=prop` in `security_patch.txt`, boot and vendor patch levels are forced to `prop` as well. All three ASN.1 attestation tags (706/718/719) now resolve via `SystemProperties.get()` to match what detector apps see through `getprop`.
***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.
### Key Persistence
#### 🚀 **The Road Ahead**
* **Generated Key Persistence Layer**: Keys from `generateKey` are persisted to disk in binary format with version headers and atomic writes (tmp + rename).
* **Automatic Restoration**: Persisted keys are restored on daemon startup without re-attestation.
* **Keybox Rotation Survival**: Generated keys survive keybox.xml changes — only PATCH-mode cert chains are invalidated.
* **File-Level Locking**: Concurrent read/write access to persisted keys is serialized to prevent corruption.
### Process Reliability
* **Fork-Based Supervisor Daemon**: Replaces the restart loop with a native fork-based supervisor for near-instant recovery.
* **Attestation Leak Blocking**: Returns `DEAD_OBJECT` to callers when the interceptor service is unavailable, preventing unpatched attestation from leaking through.
* **Global Exception Handler**: Catches uncaught exceptions and triggers clean daemon restart instead of silent death.
* **FileObserver NPE Fix**: Prevents crash when config files are deleted while being observed.
### Upstream Cherry-Picks
* **KeyUsage per HAL spec** (#119): Correct certificate KeyUsage based on KeyPurpose.
* **Reference leak fix** (#122): Resolve strong reference leak and warnings in binder interception.
### Module Lifecycle
* **`action.sh`**: Purge persistent key storage via KSU Manager Action button. Shows key count and storage size before clearing.
* **`uninstall.sh`**: Clean module removal — kills daemon, removes generated data, preserves `target.txt`, `keybox.xml`, and `security_patch.txt`.
### PKI Fixes
* Strip HTML comments from PEM blocks before parsing.
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.
+4 -1
View File
@@ -48,7 +48,7 @@ install_file() {
# --- Installation ---
ui_print "- Extracting module files"
for file in customize.sh module.prop service.sh sepolicy.rule daemon; do
for file in customize.sh module.prop service.sh sepolicy.rule daemon action.sh uninstall.sh; do
install_file "$file" "$MODPATH"
done
@@ -67,10 +67,13 @@ ui_print ""
ui_print "- Extracting $ARCH libraries"
install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH"
install_file "lib/$ABI_DIR/libinject.so" "$MODPATH"
install_file "lib/$ABI_DIR/libsupervisor.so" "$MODPATH"
ui_print ""
mv "$MODPATH/libinject.so" "$MODPATH/inject"
mv "$MODPATH/libsupervisor.so" "$MODPATH/supervisor"
chmod 755 "$MODPATH/inject"
chmod 755 "$MODPATH/supervisor"
# --- Configuration Files ---
if [ ! -d "$CONFIG_DIR" ]; then
+2 -2
View File
@@ -2,6 +2,6 @@ id=tricky_store
name=TEESimulator
version=${REPLACEMEVER}
versionCode=${REPLACEMEVERCODE}
author=JingMatrix
author=JingMatrix, Enginex0
description=Software simulation for Android hardware-backed key pairs with key attestation
updateJson=https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/update.json
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/update.json
+1 -3
View File
@@ -1,4 +1,2 @@
allow keystore system_file unix_dgram_socket *
allow system_file keystore unix_dgram_socket *
allow keystore system_file file *
allow keystore {adb_data_file shell_data_file} file *
allow crash_dump keystore process *
+2 -8
View File
@@ -1,11 +1,5 @@
DEBUG=false
MODDIR=${0%/*}
cd $MODDIR
while true; do
./daemon "$MODDIR" || exit 1
# ensure keystore initialized
sleep 2
done &
# Fork-based supervisor for instant restart
./supervisor ./daemon "$MODDIR" &
+11
View File
@@ -0,0 +1,11 @@
#!/system/bin/sh
MODDIR=${0%/*}
CONFIG_DIR=/data/adb/tricky_store
# Kill daemon and supervisor
for pid in $(pidof TEESimulator) $(pidof supervisor) $(pidof daemon); do
kill -9 "$pid" 2>/dev/null
done
rm -rf "$CONFIG_DIR/persistent_keys"
rm -f "$CONFIG_DIR/tee_status.txt"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"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"
"version": "v3.2",
"versionCode": 82,
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v3.2/TEESimulator-v3.2-82-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
}
@@ -4,4 +4,12 @@ public class ActivityThread {
public static void initializeMainlineModules() {
throw new UnsupportedOperationException("STUB!");
}
public static ActivityThread systemMain() {
throw new UnsupportedOperationException("STUB!");
}
public ContextImpl getSystemContext() {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -0,0 +1,4 @@
package android.app;
public class ContextImpl {
}
@@ -0,0 +1,8 @@
package android.hardware.security.keymint;
public @interface BlockMode {
public static final int ECB = 1;
public static final int CBC = 2;
public static final int CTR = 3;
public static final int GCM = 32;
}
@@ -0,0 +1,10 @@
package android.hardware.security.keymint;
public @interface PaddingMode {
public static final int NONE = 1;
public static final int RSA_OAEP = 2;
public static final int RSA_PSS = 3;
public static final int RSA_PKCS1_1_5_ENCRYPT = 4;
public static final int RSA_PKCS1_1_5_SIGN = 5;
public static final int PKCS7 = 64;
}
@@ -0,0 +1,38 @@
package android.system.keystore2;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class CreateOperationResponse implements Parcelable {
public IKeystoreOperation iOperation;
public OperationChallenge operationChallenge;
public KeyParameters parameters;
public byte[] upgradedBlob;
public static final Creator<CreateOperationResponse> CREATOR = new Creator<CreateOperationResponse>() {
@Override
public CreateOperationResponse createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public CreateOperationResponse[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -0,0 +1,9 @@
package android.system.keystore2;
public @interface Domain {
public static final int APP = 0;
public static final int GRANT = 1;
public static final int SELINUX = 2;
public static final int BLOB = 3;
public static final int KEY_ID = 4;
}
@@ -0,0 +1,33 @@
package android.system.keystore2;
import android.os.IBinder;
import android.os.Binder;
import android.os.IInterface;
public interface IKeystoreOperation extends IInterface {
public static final java.lang.String DESCRIPTOR = "android.system.keystore2.IKeystoreOperation";
public void updateAad(byte[] aadInput);
public byte[] update(byte[] input);
public byte[] finish(byte[] input, byte[] signature);
public void abort() throws android.os.RemoteException;
abstract class Stub extends Binder implements IKeystoreOperation {
public static IKeystoreOperation asInterface(IBinder b) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public IBinder asBinder() {
return this;
}
@Override
public void updateAad(byte[] aadInput) {
throw new UnsupportedOperationException("STUB!");
}
}
}
@@ -0,0 +1,34 @@
package android.system.keystore2;
import android.os.Parcel;
import android.os.Parcelable;
import android.hardware.security.keymint.KeyParameter;
import androidx.annotation.NonNull;
public class KeyParameters implements Parcelable {
public KeyParameter[] keyParameter;
public static final Creator<KeyParameters> CREATOR = new Creator<KeyParameters>() {
@Override
public KeyParameters createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public KeyParameters[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -0,0 +1,32 @@
package android.system.keystore2;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class OperationChallenge implements Parcelable {
public long challenge = 0L;
public static final Creator<OperationChallenge> CREATOR = new Creator<OperationChallenge>() {
@Override
public OperationChallenge createFromParcel(Parcel in) {
throw new UnsupportedOperationException("STUB!");
}
@Override
public OperationChallenge[] newArray(int size) {
throw new UnsupportedOperationException("STUB!");
}
};
@Override
public int describeContents() {
throw new UnsupportedOperationException("STUB!");
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
throw new UnsupportedOperationException("STUB!");
}
}