Compare commits
21
Commits
v6.0.0-162
...
v3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f50004d9a5 | ||
|
|
8c10cf71ce | ||
|
|
1ed3d9ad6e | ||
|
|
4107127506 | ||
|
|
e36c4e351c | ||
|
|
1546c3bba0 | ||
|
|
81a8ce0c60 | ||
|
|
7c4df3e237 | ||
|
|
1ac08411be | ||
|
|
a11a5e41a2 | ||
|
|
c367aa5efc | ||
|
|
88781ff31d | ||
|
|
409fb5fcc3 | ||
|
|
0cf8f70544 | ||
|
|
7ecea09ec6 | ||
|
|
887c5fc666 | ||
|
|
ce0ca18d98 | ||
|
|
fa28e9fc71 | ||
|
|
c3822197b1 | ||
|
|
f276806096 | ||
|
|
9aa4a33c5e |
@@ -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.1"
|
||||
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.
|
||||
|
||||
@@ -22,6 +22,9 @@ 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_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)
|
||||
|
||||
@@ -348,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;
|
||||
|
||||
@@ -592,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();
|
||||
@@ -647,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
|
||||
|
||||
@@ -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.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Main application object for TEESimulator. This object manages the application's lifecycle,
|
||||
@@ -32,6 +33,11 @@ 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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -253,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) {
|
||||
@@ -307,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 ->
|
||||
@@ -318,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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-23
@@ -13,23 +13,16 @@ 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() {
|
||||
private val stubBinderClass = IKeystoreService.Stub::class.java
|
||||
|
||||
// Transaction codes for the IKeystoreService interface methods we are interested in.
|
||||
private val GET_KEY_ENTRY_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "getKeyEntry")
|
||||
private val DELETE_KEY_TRANSACTION =
|
||||
@@ -56,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) }
|
||||
@@ -173,7 +162,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
)
|
||||
}
|
||||
|
||||
// Let most calls go through to the real service.
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
@@ -232,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) {
|
||||
@@ -243,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>
|
||||
@@ -257,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."
|
||||
)
|
||||
@@ -290,6 +280,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
|
||||
metadata.certificate = publicCert
|
||||
metadata.certificateChain = certificateChain
|
||||
|
||||
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
|
||||
|
||||
SystemLogger.verbose(
|
||||
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
|
||||
)
|
||||
|
||||
+376
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+206
-80
@@ -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,10 +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
|
||||
@@ -21,16 +27,11 @@ 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 nspace: Long,
|
||||
@@ -52,7 +53,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
GENERATE_KEY_TRANSACTION -> {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||
|
||||
if (!shouldSkip) return handleGenerateKey(callingUid, data)
|
||||
if (!shouldSkip) return handleGenerateKey(txId, callingUid, data)
|
||||
}
|
||||
CREATE_OPERATION_TRANSACTION -> {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||
@@ -93,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
|
||||
@@ -104,7 +110,14 @@ 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)
|
||||
|
||||
@@ -172,11 +185,6 @@ class KeyMintSecurityLevelInterceptor(
|
||||
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(
|
||||
txId: Long,
|
||||
callingUid: Int,
|
||||
@@ -217,15 +225,17 @@ class KeyMintSecurityLevelInterceptor(
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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}"
|
||||
)
|
||||
@@ -236,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) ||
|
||||
@@ -245,37 +253,29 @@ class KeyMintSecurityLevelInterceptor(
|
||||
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
|
||||
|
||||
if (needsSoftwareGeneration) {
|
||||
keyDescriptor.nspace = secureRandom.nextLong()
|
||||
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)
|
||||
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
|
||||
}
|
||||
@@ -285,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,
|
||||
@@ -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 {
|
||||
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 =
|
||||
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
|
||||
private val IMPORT_KEY_TRANSACTION =
|
||||
@@ -331,30 +476,16 @@ 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>()
|
||||
// Stores interceptors for active cryptographic operations.
|
||||
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
|
||||
|
||||
// --- Public Accessors for Other Interceptors ---
|
||||
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
|
||||
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? {
|
||||
// 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
|
||||
return generatedKeys.entries
|
||||
.filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid }
|
||||
@@ -369,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}")
|
||||
@@ -379,7 +511,6 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
|
||||
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
|
||||
// Unregister from the native hook layer first.
|
||||
unregister(backdoor, operationBinder)
|
||||
|
||||
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) {
|
||||
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 {
|
||||
@@ -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.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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 10–12**).
|
||||
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
|
||||
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:
|
||||
### Anti-Detection Hardening
|
||||
|
||||
* **✅ Full Crypto Operations (`createOperation`)**: The simulator now correctly handles `SIGN`, `VERIFY`, `ENCRYPT`, and `DECRYPT` purposes for software-generated keys.
|
||||
* **🔗 Certificate Chain Updates (`updateSubcomponent`)**: Added support for applications updating the certificate chain of virtual keys (e.g., via `KeyStore.setKeyEntry`).
|
||||
* **📋 Enumeration Support (`listEntries`)**: Generated keys are now properly visible in enumeration APIs like `KeyStore.aliases()`, thanks to the implementation of `listEntries` and `listEntriesBatched`.
|
||||
* **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.
|
||||
|
||||
### 🔧 Compatibility & Stability
|
||||
We’ve ironed out crashes and architecture-specific bugs to ensure a smooth experience across more devices:
|
||||
### Security Patch Consistency
|
||||
|
||||
* **Android 10**: Fixed a crash caused by the missing `waitForService` method.
|
||||
* **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`.
|
||||
* **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`.
|
||||
|
||||
### 🚀 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
@@ -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
@@ -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
|
||||
|
||||
+2
-8
@@ -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" &
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "v3.1",
|
||||
"versionCode": 59,
|
||||
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v3.1/TEESimulator-v3.1-59-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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user