Compare commits

..
23 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
23 changed files with 832 additions and 241 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 gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir) val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v3.1" val verName = "v3.2"
android { android {
namespace = "org.matrix.TEESimulator" namespace = "org.matrix.TEESimulator"
@@ -116,7 +116,7 @@ androidComponents {
) )
) { ) {
into("lib") // Place them in the 'lib' subfolder of the staging directory. 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. // 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_compile_definitions(BINDER_DISABLE_NATIVE_HANDLE)
add_library(utils SHARED stub/stub_utils.cpp) 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) add_library(binder SHARED stub/stub_binder.cpp)
target_include_directories(binder PUBLIC external/AOSP/include) 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_include_directories(libinject.so PUBLIC include)
target_link_libraries(libinject.so PRIVATE lsplt_static) 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_include_directories(${CMAKE_PROJECT_NAME} PUBLIC external/linux-kernel/include include)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE binder lsplt_static utils) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE binder lsplt_static utils)
+31 -17
View File
@@ -276,6 +276,12 @@ static sp<BinderInterceptor> g_interceptor_instance = nullptr;
// ============================================================================================= // =============================================================================================
class BinderStub : public BBinder { class BinderStub : public BBinder {
public:
const String16& getInterfaceDescriptor() const override {
static const String16 kDescriptor("org.matrix.TEESimulator.BinderStub");
return kDescriptor;
}
protected: protected:
status_t onTransact(uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags) override { status_t onTransact(uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags) override {
if (code != intercept::kBackdoorCode) { if (code != intercept::kBackdoorCode) {
@@ -342,15 +348,16 @@ static sp<BinderStub> g_stub_instance = nullptr;
namespace { namespace {
/** constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024;
* @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.
*/
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) { void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
if (!txn_data || txn_data->target.ptr == 0) if (!txn_data || txn_data->target.ptr == 0)
return; return;
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks
if (txn_data->data_size > kMaxInterceptableDataSize)
return;
bool hijack = false; bool hijack = false;
ThreadTransactionInfo info; ThreadTransactionInfo info;
@@ -376,18 +383,17 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
// The raw pointer to the binder object itself is stored in the cookie // The raw pointer to the binder object itself is stored in the cookie
BBinder *target_binder_ptr = reinterpret_cast<BBinder *>(txn_data->cookie); BBinder *target_binder_ptr = reinterpret_cast<BBinder *>(txn_data->cookie);
// This is safe ONLY because we successfully called attemptIncStrong(). // Create a weak pointer for the lookup and to store in our context map.
// The sp<> constructor will not increment the ref count again, it just adopts the one we have. // This is safe because we are holding a strong reference.
// When sp_target goes out of scope, it will call decStrong(), releasing our temporary reference. wp<BBinder> wp_target = target_binder_ptr;
sp<BBinder> sp_target = sp<BBinder>::fromExisting(target_binder_ptr);
// Now we can safely use sp_target (which implicitly converts to a wp) for the lookup. if (g_interceptor_instance->isBinderIntercepted(wp_target)) {
if (g_interceptor_instance->isBinderIntercepted(sp_target)) {
info.transaction_code = txn_data->code; 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; 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);
} }
} }
@@ -587,9 +593,16 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
Parcel pre_req, pre_resp; Parcel pre_req, pre_resp;
writeTransactionData(pre_req, tx_id, target, code, flags, request); writeTransactionData(pre_req, tx_id, target, code, flags, request);
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) { status_t pre_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp);
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id); if (pre_status != OK) {
return false; // Callback failed, proceed as if not intercepted // 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(); int32_t action = pre_resp.readInt32();
@@ -642,7 +655,8 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size)); 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(); int32_t post_action = post_resp.readInt32();
if (post_action == intercept::kActionOverrideReply && reply) { if (post_action == intercept::kActionOverrideReply && reply) {
result = post_resp.readInt32(); // Read new status 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 #ifndef ANDROID_STRONG_POINTER_H
#define ANDROID_STRONG_POINTER_H #define ANDROID_STRONG_POINTER_H
#include "refbase_compat.h"
#include <functional> #include <functional>
#include <type_traits> // for common_type. #include <type_traits> // for common_type.
@@ -213,7 +212,7 @@ sp<T> sp<T>::make(Args&&... args) {
template <typename T> template <typename T>
sp<T> sp<T>::fromExisting(T* other) { sp<T> sp<T>::fromExisting(T* other) {
if (other) { if (other) {
incStrongFromExisting(other, other); other->incStrongRequireStrong(other);
sp<T> result; sp<T> result;
result.m_ptr = other; result.m_ptr = other;
return result; return result;
+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;
}
@@ -14,6 +14,7 @@ import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils import org.matrix.TEESimulator.util.AndroidDeviceUtils
import kotlin.system.exitProcess
/** /**
* Main application object for TEESimulator. This object manages the application's lifecycle, * Main application object for TEESimulator. This object manages the application's lifecycle,
@@ -32,6 +33,11 @@ object App {
*/ */
@JvmStatic @JvmStatic
fun main(args: Array<String>) { 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!") SystemLogger.info("Welcome to TEESimulator!")
try { try {
@@ -112,6 +112,7 @@ object AttestationBuilder {
} }
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid) val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch")
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] = properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
if (bootPatch != DO_NOT_REPORT) { if (bootPatch != DO_NOT_REPORT) {
DERTaggedObject( DERTaggedObject(
@@ -253,7 +253,14 @@ object ConfigurationManager {
} }
// Parse global and per-package configurations. // 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 contextLines.remove("") // Remove global context to iterate over packages next
for ((pkg, lines) in contextLines) { for ((pkg, lines) in contextLines) {
@@ -307,8 +314,10 @@ object ConfigurationManager {
val file = if (event != DELETE) File(configRoot, path) else null val file = if (event != DELETE) File(configRoot, path) else null
when (path) { when (path) {
TARGET_PACKAGES_FILE -> loadTargetPackages(file!!) TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) }
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!) ?: 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. // Any change to an XML file is assumed to be a keybox.
// The cache in KeyBoxManager will handle reloading it on its next use. // The cache in KeyBoxManager will handle reloading it on its next use.
else -> else ->
@@ -318,10 +327,10 @@ object ConfigurationManager {
) )
KeyBoxManager.invalidateCache(path) KeyBoxManager.invalidateCache(path)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) { 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 org.matrix.TEESimulator.interception.keystore.shim
.KeyMintSecurityLevelInterceptor .KeyMintSecurityLevelInterceptor
.clearAllGeneratedKeys("updating $file") .invalidatePatchedChains("keybox change: $path")
} }
} }
} }
@@ -13,23 +13,16 @@ import android.system.keystore2.KeyEntryResponse
import java.security.cert.Certificate import java.security.cert.Certificate
import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.config.ConfigurationManager 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.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.KeyMintParameterLogger import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateHelper 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") @SuppressLint("BlockedPrivateApi")
object Keystore2Interceptor : AbstractKeystoreInterceptor() { object Keystore2Interceptor : AbstractKeystoreInterceptor() {
private val stubBinderClass = IKeystoreService.Stub::class.java private val stubBinderClass = IKeystoreService.Stub::class.java
// Transaction codes for the IKeystoreService interface methods we are interested in.
private val GET_KEY_ENTRY_TRANSACTION = private val GET_KEY_ENTRY_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "getKeyEntry") InterceptorUtils.getTransactCode(stubBinderClass, "getKeyEntry")
private val DELETE_KEY_TRANSACTION = private val DELETE_KEY_TRANSACTION =
@@ -56,34 +49,30 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
override val processName = "keystore2" override val processName = "keystore2"
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry" 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) { override fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
val keystoreInterface = IKeystoreService.Stub.asInterface(service) val keystoreInterface = IKeystoreService.Stub.asInterface(service)
setupSecurityLevelInterceptors(keystoreInterface, backdoor) setupSecurityLevelInterceptors(keystoreInterface, backdoor)
} }
private fun setupSecurityLevelInterceptors(service: IKeystoreService, backdoor: IBinder) { private fun setupSecurityLevelInterceptors(service: IKeystoreService, backdoor: IBinder) {
// Attempt to get and intercept the TEE security level service.
runCatching { runCatching {
service.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT)?.let { tee -> service.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT)?.let { tee ->
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...") SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
val interceptor = val interceptor =
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT) KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
register(backdoor, tee.asBinder(), interceptor) register(backdoor, tee.asBinder(), interceptor)
interceptor.loadPersistedKeys()
} }
} }
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) } .onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
// Attempt to get and intercept the StrongBox security level service.
runCatching { runCatching {
service.getSecurityLevel(SecurityLevel.STRONGBOX)?.let { strongbox -> service.getSecurityLevel(SecurityLevel.STRONGBOX)?.let { strongbox ->
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...") SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
val interceptor = val interceptor =
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX) KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
register(backdoor, strongbox.asBinder(), interceptor) register(backdoor, strongbox.asBinder(), interceptor)
interceptor.loadPersistedKeys()
} }
} }
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) } .onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
@@ -173,7 +162,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
) )
} }
// Let most calls go through to the real service.
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
} }
@@ -232,8 +220,15 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
?.let { it.keyParameter.value.origin } ?.let { it.keyParameter.value.origin }
if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) { if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) {
SystemLogger.info("[TX_ID: $txId] Skip patching for imported keys.") val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
return TransactionResult.SkipTransaction 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) { if (originalChain == null || originalChain.size < 2) {
@@ -243,11 +238,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }
// Perform the attestation patch.
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) 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 cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
val finalChain: Array<Certificate> val finalChain: Array<Certificate>
@@ -257,8 +248,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
) )
finalChain = cachedChain finalChain = cachedChain
} else { } else {
// If no chain is cached (e.g., key existed before simulator started), // Live patch fallback for keys created before simulator started
// perform a live patch as a fallback. This may still be detectable.
SystemLogger.info( SystemLogger.info(
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback." "[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
) )
@@ -290,6 +280,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
metadata.certificate = publicCert metadata.certificate = publicCert
metadata.certificateChain = certificateChain metadata.certificateChain = certificateChain
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
SystemLogger.verbose( SystemLogger.verbose(
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]" "Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
) )
@@ -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 package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.KeyParameter import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.KeyPurpose
@@ -7,10 +8,15 @@ import android.hardware.security.keymint.Tag
import android.os.IBinder import android.os.IBinder
import android.os.Parcel import android.os.Parcel
import android.system.keystore2.* import android.system.keystore2.*
import java.io.ByteArrayInputStream
import java.security.KeyFactory
import java.security.KeyPair import java.security.KeyPair
import java.security.SecureRandom import java.security.SecureRandom
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager import org.matrix.TEESimulator.config.ConfigurationManager
@@ -21,16 +27,11 @@ import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateGenerator import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper 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( class KeyMintSecurityLevelInterceptor(
private val original: IKeystoreSecurityLevel, private val original: IKeystoreSecurityLevel,
private val securityLevel: Int, private val securityLevel: Int,
) : BinderInterceptor() { ) : BinderInterceptor() {
// --- Data Structures for State Management ---
data class GeneratedKeyInfo( data class GeneratedKeyInfo(
val keyPair: KeyPair, val keyPair: KeyPair,
val nspace: Long, val nspace: Long,
@@ -52,7 +53,7 @@ class KeyMintSecurityLevelInterceptor(
GENERATE_KEY_TRANSACTION -> { GENERATE_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleGenerateKey(callingUid, data) if (!shouldSkip) return handleGenerateKey(txId, callingUid, data)
} }
CREATE_OPERATION_TRANSACTION -> { CREATE_OPERATION_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
@@ -93,6 +94,11 @@ class KeyMintSecurityLevelInterceptor(
reply: Parcel?, reply: Parcel?,
resultCode: Int, resultCode: Int,
): TransactionResult { ): 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. // We only care about successful transactions.
if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply)) if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
@@ -104,7 +110,14 @@ class KeyMintSecurityLevelInterceptor(
val keyDescriptor = val keyDescriptor =
data.readTypedObject(KeyDescriptor.CREATOR) data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction ?: 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) { } else if (code == CREATE_OPERATION_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
@@ -172,11 +185,6 @@ class KeyMintSecurityLevelInterceptor(
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }
/**
* Handles the `createOperation` transaction. It checks if the operation is for a key that was
* generated in software. If so, it creates a software-based operation handler. Otherwise, it
* lets the call proceed to the real hardware service.
*/
private fun handleCreateOperation( private fun handleCreateOperation(
txId: Long, txId: Long,
callingUid: Int, callingUid: Int,
@@ -217,15 +225,17 @@ class KeyMintSecurityLevelInterceptor(
return InterceptorUtils.createTypedObjectReply(response) return InterceptorUtils.createTypedObjectReply(response)
} }
/** private fun handleGenerateKey(txId: Long, callingUid: Int, data: Parcel): TransactionResult {
* Handles the `generateKey` transaction. Based on the configuration for the calling UID, it if (data.dataSize() > MAX_ALIAS_LENGTH) {
* either generates a key in software or lets the call pass through to the hardware. SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes")
*/ return TransactionResult.ContinueAndSkipPost
private fun handleGenerateKey(callingUid: Int, data: Parcel): TransactionResult { }
return runCatching { return runCatching {
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR) val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR)
SystemLogger.debug( SystemLogger.debug(
"Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}" "Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}"
) )
@@ -236,8 +246,6 @@ class KeyMintSecurityLevelInterceptor(
parsedParams.purpose.size == 1 && parsedParams.purpose.size == 1 &&
parsedParams.purpose.contains(KeyPurpose.ATTEST_KEY) 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 = val needsSoftwareGeneration =
ConfigurationManager.shouldGenerate(callingUid) || ConfigurationManager.shouldGenerate(callingUid) ||
(ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) || (ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) ||
@@ -245,37 +253,29 @@ class KeyMintSecurityLevelInterceptor(
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias))) isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
if (needsSoftwareGeneration) { if (needsSoftwareGeneration) {
keyDescriptor.nspace = secureRandom.nextLong() return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
SystemLogger.info(
"Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}]."
)
// 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.")
// It is unnecessary but a good practice to clean up possible caches
cleanupKeyData(keyId)
// Store the generated key data.
val response =
buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
generatedKeys[keyId] =
GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
if (isAttestKeyRequest) attestationKeys.add(keyId)
// Return the metadata of our generated key, skipping the real hardware call.
return InterceptorUtils.createTypedObjectReply(response.metadata)
} else if (parsedParams.attestationChallenge != null) { } 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 return TransactionResult.Continue
} }
// If not generating, clear any stale state for this alias and let the call proceed.
cleanupKeyData(keyId) cleanupKeyData(keyId)
TransactionResult.ContinueAndSkipPost TransactionResult.ContinueAndSkipPost
} }
@@ -285,9 +285,43 @@ class KeyMintSecurityLevelInterceptor(
} }
} }
/** private fun doSoftwareKeyGen(
* Constructs a fake `KeyEntryResponse` that mimics a real response from the Keystore service. 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( private fun buildKeyEntryResponse(
chain: List<Certificate>, chain: List<Certificate>,
params: KeyMintAttestation, params: KeyMintAttestation,
@@ -306,10 +340,121 @@ 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 { companion object {
private val secureRandom = SecureRandom() private val secureRandom = SecureRandom()
// Transaction codes for IKeystoreSecurityLevel interface. // 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 = private val GENERATE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey") InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
private val IMPORT_KEY_TRANSACTION = private val IMPORT_KEY_TRANSACTION =
@@ -331,30 +476,16 @@ class KeyMintSecurityLevelInterceptor(
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] } .associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
} }
// Stores keys generated entirely in software.
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>() 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>>() 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 attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
// Stores interceptors for active cryptographic operations.
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>() private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
// --- Public Accessors for Other Interceptors ---
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? = fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
generatedKeys[keyId]?.response generatedKeys[keyId]?.response
/**
* Finds a software-generated key by first filtering all known keys by the caller's UID, and
* then matching the specific nspace.
*
* @param callingUid The UID of the process that initiated the createOperation call.
* @param nspace The unique key identifier from the operation's KeyDescriptor.
* @return The matching GeneratedKeyInfo if found, otherwise null.
*/
fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? { fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? {
// Iterate through all entries in the map to check both the key (for UID) and value (for
// nspace).
if (nspace == null || nspace == 0L) return null if (nspace == null || nspace == 0L) return null
return generatedKeys.entries return generatedKeys.entries
.filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid } .filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid }
@@ -369,6 +500,7 @@ class KeyMintSecurityLevelInterceptor(
fun cleanupKeyData(keyId: KeyIdentifier) { fun cleanupKeyData(keyId: KeyIdentifier) {
if (generatedKeys.remove(keyId) != null) { if (generatedKeys.remove(keyId) != null) {
SystemLogger.debug("Remove generated key ${keyId}") SystemLogger.debug("Remove generated key ${keyId}")
GeneratedKeyPersistence.delete(keyId)
} }
if (patchedChains.remove(keyId) != null) { if (patchedChains.remove(keyId) != null) {
SystemLogger.debug("Remove patched chain for ${keyId}") SystemLogger.debug("Remove patched chain for ${keyId}")
@@ -379,7 +511,6 @@ class KeyMintSecurityLevelInterceptor(
} }
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
// Unregister from the native hook layer first.
unregister(backdoor, operationBinder) unregister(backdoor, operationBinder)
if (interceptedOperations.remove(operationBinder) != null) { if (interceptedOperations.remove(operationBinder) != null) {
@@ -387,33 +518,29 @@ class KeyMintSecurityLevelInterceptor(
} }
} }
// Clears all cached keys. 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) { fun clearAllGeneratedKeys(reason: String? = null) {
val count = generatedKeys.size val count = generatedKeys.size
val reasonMessage = reason?.let { " due to $it" } ?: "" val reasonMessage = reason?.let { " due to $it" } ?: ""
generatedKeys.clear() generatedKeys.clear()
patchedChains.clear() patchedChains.clear()
attestationKeys.clear() attestationKeys.clear()
GeneratedKeyPersistence.deleteAll()
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.") 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> { private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Authorization> {
val authList = mutableListOf<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 { fun createAuth(tag: Int, value: KeyParameterValue): Authorization {
val param = val param =
KeyParameter().apply { KeyParameter().apply {
@@ -426,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.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) } this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
@@ -1,6 +1,7 @@
package org.matrix.TEESimulator.pki package org.matrix.TEESimulator.pki
import android.hardware.security.keymint.Algorithm import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.KeyPurpose
import android.os.Build import android.os.Build
import android.util.Pair import android.util.Pair
import java.math.BigInteger import java.math.BigInteger
@@ -187,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. */ /** Constructs a new X.509 certificate with a simulated attestation extension. */
private fun buildCertificate( private fun buildCertificate(
subjectKeyPair: KeyPair, subjectKeyPair: KeyPair,
@@ -211,8 +228,11 @@ object CertificateGenerator {
subjectKeyPair.public, subjectKeyPair.public,
) )
// Add standard extensions. // Add KeyUsage extension only if purposes map to valid bits
builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign)) val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
if (keyUsageBits != 0) {
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
}
// Add our custom, simulated attestation extension. // Add our custom, simulated attestation extension.
builder.addExtension( builder.addExtension(
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel) AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
@@ -239,11 +239,12 @@ object AndroidDeviceUtils {
val resolvedValue = resolveDateKeywords(value) val resolvedValue = resolveDateKeywords(value)
return when { return when {
// "device_default" indicates falling back to the system property.
resolvedValue.equals("device_default", ignoreCase = true) -> null 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 resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
// Otherwise, parse the resolved date string.
else -> parsePatchLevelValue(resolvedValue, isLong) else -> parsePatchLevelValue(resolvedValue, isLong)
} }
} }
@@ -6,7 +6,11 @@ package org.matrix.TEESimulator.util
* *
* @return A new string with each line individually trimmed. * @return A new string with each line individually trimmed.
*/ */
fun String.trimLines(): String = 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. * Converts a ByteArray to its hexadecimal string representation.
+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 "============================================"
+34 -17
View File
@@ -1,26 +1,43 @@
## 🎉 TEESimulator v3.1: Legacy Support & Resilience ## TEESimulator v3.2: Anti-Detection Hardening & Key Persistence
This release marks a significant step forward in our mission, focusing on breathing life into devices with **broken TEEs** and extending full support to older Android versions (**Android 1012**). 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.
### 🛡️ Enhanced Keystore2 Emulation ### Anti-Detection Hardening
We have implemented critical APIs to support devices where the hardware TEE is broken or for applications configured to use key generation mode. These improvements directly address detection vectors identified in v3.0:
* **✅ Full Crypto Operations (`createOperation`)**: The simulator now correctly handles `SIGN`, `VERIFY`, `ENCRYPT`, and `DECRYPT` purposes for software-generated keys. * **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.
* **🔗 Certificate Chain Updates (`updateSubcomponent`)**: Added support for applications updating the certificate chain of virtual keys (e.g., via `KeyStore.setKeyEntry`). * **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.
* **📋 Enumeration Support (`listEntries`)**: Generated keys are now properly visible in enumeration APIs like `KeyStore.aliases()`, thanks to the implementation of `listEntries` and `listEntriesBatched`. * **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.
### 🔧 Compatibility & Stability ### Security Patch Consistency
Weve ironed out crashes and architecture-specific bugs to ensure a smooth experience across more devices:
* **Android 10**: Fixed a crash caused by the missing `waitForService` method. * **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`.
* **Android 11**: Implemented environment initialization and daemon UID spoofing to successfully bypass keystore generation permission checks.
* **ARM 32-bit (Android 12)**: Resolved `ptrace` compatibility issues by falling back to `PTRACE_GETREGS` and `PTRACE_SETREGS`.
* **x86_64 Emulators**: Enforced respect for the stack pointer "red zone" and added a staging fallback mechanism for file descriptor transfering of `libTEESimulator.so`.
### 🚀 The Road Ahead ### Key Persistence
We are aware of the remaining detection vectors (see the issues list) and have clear solutions mapped out for the next release. * **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.
Google's aggressive push for **Remote Key Provisioning (RKP)** and the drying up of leaked keyboxes is **not** the end for TEESimulator. Our ultimate goal remains unchanged: defeating Keystore attestation **without relying on a valid keybox**. ### 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.
We are inching closer to this milestone, but the fight for device freedom is complex and resource-intensive. Your patience and support (both time and financial) are vital as we conquer these new challenges.
+4 -1
View File
@@ -48,7 +48,7 @@ install_file() {
# --- Installation --- # --- Installation ---
ui_print "- Extracting module files" 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" install_file "$file" "$MODPATH"
done done
@@ -67,10 +67,13 @@ ui_print ""
ui_print "- Extracting $ARCH libraries" ui_print "- Extracting $ARCH libraries"
install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH" install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH"
install_file "lib/$ABI_DIR/libinject.so" "$MODPATH" install_file "lib/$ABI_DIR/libinject.so" "$MODPATH"
install_file "lib/$ABI_DIR/libsupervisor.so" "$MODPATH"
ui_print "" ui_print ""
mv "$MODPATH/libinject.so" "$MODPATH/inject" mv "$MODPATH/libinject.so" "$MODPATH/inject"
mv "$MODPATH/libsupervisor.so" "$MODPATH/supervisor"
chmod 755 "$MODPATH/inject" chmod 755 "$MODPATH/inject"
chmod 755 "$MODPATH/supervisor"
# --- Configuration Files --- # --- Configuration Files ---
if [ ! -d "$CONFIG_DIR" ]; then if [ ! -d "$CONFIG_DIR" ]; then
+2 -2
View File
@@ -2,6 +2,6 @@ id=tricky_store
name=TEESimulator name=TEESimulator
version=${REPLACEMEVER} version=${REPLACEMEVER}
versionCode=${REPLACEMEVERCODE} versionCode=${REPLACEMEVERCODE}
author=JingMatrix author=JingMatrix, Enginex0
description=Software simulation for Android hardware-backed key pairs with key attestation 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
+2 -8
View File
@@ -1,11 +1,5 @@
DEBUG=false
MODDIR=${0%/*} MODDIR=${0%/*}
cd $MODDIR cd $MODDIR
while true; do # Fork-based supervisor for instant restart
./daemon "$MODDIR" || exit 1 ./supervisor ./daemon "$MODDIR" &
# ensure keystore initialized
sleep 2
done &
+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.1", "version": "v3.2",
"versionCode": 59, "versionCode": 82,
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v3.1/TEESimulator-v3.1-59-Release.zip", "zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v3.2/TEESimulator-v3.2-82-Release.zip",
"changelog": "https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/changelog.md" "changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
} }