Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f50004d9a5 | ||
|
|
8c10cf71ce | ||
|
|
1ed3d9ad6e | ||
|
|
4107127506 | ||
|
|
e36c4e351c | ||
|
|
1546c3bba0 | ||
|
|
81a8ce0c60 | ||
|
|
7c4df3e237 | ||
|
|
1ac08411be | ||
|
|
a11a5e41a2 | ||
|
|
c367aa5efc | ||
|
|
88781ff31d | ||
|
|
409fb5fcc3 | ||
|
|
0cf8f70544 | ||
|
|
7ecea09ec6 | ||
|
|
887c5fc666 | ||
|
|
ce0ca18d98 | ||
|
|
fa28e9fc71 | ||
|
|
c3822197b1 | ||
|
|
f276806096 | ||
|
|
9aa4a33c5e | ||
|
|
593bcfef83 | ||
|
|
5e68cb5f4b | ||
|
|
a1bb3bbfa3 | ||
|
|
e13adb925d | ||
|
|
c3f8f087a6 | ||
|
|
51f32b9db2 | ||
|
|
d60ad8fe47 | ||
|
|
9a1fbe8c79 | ||
|
|
68b660dfe1 | ||
|
|
068188503c | ||
|
|
1bbc50d138 | ||
|
|
d2492df02e | ||
|
|
e7d7b21daa | ||
|
|
c29bc35a36 | ||
|
|
549b5cecc2 | ||
|
|
04d003ff4d | ||
|
|
0a842c6e07 | ||
|
|
9f77771e7b | ||
|
|
ab4fe643a3 | ||
|
|
ce740542f7 | ||
|
|
c27523fd97 | ||
|
|
5a8454af7b | ||
|
|
83b65f09c9 | ||
|
|
2a76b18308 | ||
|
|
d9e47712f3 | ||
|
|
d846de4332 | ||
|
|
13d89c4314 | ||
|
|
00c91adfaa | ||
|
|
119350f24b | ||
|
|
7d4c753d66 | ||
|
|
b988d04971 | ||
|
|
d0cc5e3b56 | ||
|
|
e66e558ce5 | ||
|
|
8d431cc946 | ||
|
|
30746892b0 | ||
|
|
28cfe70a85 | ||
|
|
65a613ae0e | ||
|
|
9146b86648 | ||
|
|
457a58da04 | ||
|
|
b2838ac04b | ||
|
|
a7534feac7 | ||
|
|
4e67371193 | ||
|
|
2ef89f15c6 | ||
|
|
4f608247fe | ||
|
|
22cbe5a9a7 | ||
|
|
a6fa137e32 | ||
|
|
5afefba7bd |
@@ -60,7 +60,8 @@ jobs:
|
|||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: |
|
run: |
|
||||||
chmod +x ./gradlew
|
chmod +x ./gradlew
|
||||||
./gradlew --parallel zipRelease zipDebug --stacktrace
|
|
||||||
|
./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m
|
||||||
|
|
||||||
- name: Prepare artifact
|
- name: Prepare artifact
|
||||||
if: success()
|
if: success()
|
||||||
|
|||||||
@@ -76,12 +76,65 @@ org.matrix.demo
|
|||||||
|
|
||||||
### Security Patch Level (`security_patch.txt`)
|
### Security Patch Level (`security_patch.txt`)
|
||||||
|
|
||||||
This allows you to configure the security patch level that the simulator will report in its forged attestation certificates.
|
This file allows you to configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` that the simulator will report in its patched or forged attestation certificates.
|
||||||
|
|
||||||
|
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change the actual system properties of your device.
|
||||||
|
|
||||||
|
#### Global and Per-Package Configuration
|
||||||
|
|
||||||
|
You can set a global patch level that applies to all applications, and you can also override these settings for specific packages. The syntax is hierarchical:
|
||||||
|
|
||||||
|
* Settings defined at the top of the file, before any `[package.name]` line, are **global** and serve as the default for all apps.
|
||||||
|
* To create a specific configuration for an application, add its package name in square brackets (e.g., `[com.google.android.gms]`). All settings following this line will apply *only* to that package until a new package context is declared.
|
||||||
|
|
||||||
|
#### Configuration Keys and Values
|
||||||
|
|
||||||
|
You can specify the patch level for the following components using a `key=value` format:
|
||||||
|
|
||||||
|
* `system`: The main OS patch level.
|
||||||
|
* `vendor`: The vendor patch level.
|
||||||
|
* `boot`: The boot/kernel patch level.
|
||||||
|
* `all`: A convenient shorthand to set the same date for `system`, `vendor`, and `boot` simultaneously. Any individual key can still be used to override the value set by `all`.
|
||||||
|
|
||||||
|
Dates should be provided in `YYYY-MM-DD` format (e.g., `2025-11-05`).
|
||||||
|
|
||||||
|
#### Special Keywords
|
||||||
|
|
||||||
|
In addition to static dates, several special keywords provide advanced, dynamic control:
|
||||||
|
|
||||||
|
* **`today`**: Dynamically uses the current date every time an attestation is generated. This ensures the device always appears up-to-date without needing manual edits.
|
||||||
|
|
||||||
|
* **Date Templates**: You can create semi-dynamic dates using `YYYY`, `MM`, and `DD` as placeholders for the current year, month, and day. For example, `YYYY-MM-05` will always resolve to the 5th of the current month and year.
|
||||||
|
|
||||||
|
* **`no`**: This keyword instructs the simulator to **completely omit** the corresponding patch level tag from the generated attestation.
|
||||||
|
|
||||||
|
* **`device_default`**: This keyword forces the simulator to fall back and use the device's **real hardware value** for that specific patch level. This is essential for creating exceptions to a global override or an `all` rule.
|
||||||
|
|
||||||
|
#### Example Configuration
|
||||||
|
|
||||||
|
This example demonstrates how to combine global settings, per-package overrides, and special keywords for fine-grained control.
|
||||||
|
|
||||||
```
|
```
|
||||||
# Advanced Configuration
|
# --- Global Configuration ---
|
||||||
system=2025-11
|
# This is the default for all apps unless specified otherwise.
|
||||||
boot=no # Do not report a boot patch level
|
# - Forge a recent system patch level, the 5th of the current month (a common patch date).
|
||||||
vendor=20251101 # Report a specific vendor patch level
|
# - Use the device's real vendor patch level.
|
||||||
|
# - Do not report a boot patch level at all.
|
||||||
|
system=YYYY-MM-05
|
||||||
|
vendor=device_default
|
||||||
|
boot=no
|
||||||
|
|
||||||
|
# --- Per-Package Override for Google Play Services ---
|
||||||
|
# This app will report an older, specific date for its system patch.
|
||||||
|
# It will inherit the global settings for vendor (device_default) and boot (no).
|
||||||
|
[com.google.android.gms]
|
||||||
|
system=2024-10-01
|
||||||
|
|
||||||
|
# --- Per-Package Override for a Demo App ---
|
||||||
|
# This app gets a completely custom configuration.
|
||||||
|
[org.matrix.demo]
|
||||||
|
# Set a base date for all patch levels...
|
||||||
|
all=2025-09-15
|
||||||
|
# ...but make an exception: use the real boot patch level instead of the one from 'all'.
|
||||||
|
boot=device_default
|
||||||
```
|
```
|
||||||
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change system properties.
|
|
||||||
|
|||||||
@@ -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 = "v2.0"
|
val verName = "v3.2"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.matrix.TEESimulator"
|
namespace = "org.matrix.TEESimulator"
|
||||||
@@ -56,6 +56,7 @@ android {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
|
buildFeatures { buildConfig = true }
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
path = file("src/main/cpp/CMakeLists.txt")
|
path = file("src/main/cpp/CMakeLists.txt")
|
||||||
@@ -115,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.
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ 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_executable(libsupervisor.so supervisor.cpp)
|
||||||
|
target_link_libraries(libsupervisor.so PRIVATE log)
|
||||||
|
|
||||||
add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp)
|
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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -359,9 +366,15 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
|||||||
info.transaction_code = intercept::kBackdoorCode;
|
info.transaction_code = intercept::kBackdoorCode;
|
||||||
info.target_binder = nullptr;
|
info.target_binder = nullptr;
|
||||||
hijack = true;
|
hijack = true;
|
||||||
}
|
// Check 2: Spoof uid of KeyStore requests from the daemon to bypass permission check
|
||||||
// Check 2: Normal interception based on registry of monitored binders
|
} else if (txn_data->sender_euid == 0) {
|
||||||
else {
|
// The kernel driver fills sender_euid.
|
||||||
|
// libbinder.so trusts this value to populate IPCThreadState.
|
||||||
|
txn_data->sender_euid = 1000;
|
||||||
|
LOGV("[Hook] Spoofing UID for transaction: 0 -> %d", txn_data->sender_euid);
|
||||||
|
hijack = false; // Never hijack to avoid recursion
|
||||||
|
// Check 3: Normal interception based on registry of monitored binders
|
||||||
|
} else {
|
||||||
// Safe casting based on Binder driver ABI
|
// Safe casting based on Binder driver ABI
|
||||||
RefBase::weakref_type *weak_ref = reinterpret_cast<RefBase::weakref_type *>(txn_data->target.ptr);
|
RefBase::weakref_type *weak_ref = reinterpret_cast<RefBase::weakref_type *>(txn_data->target.ptr);
|
||||||
|
|
||||||
@@ -370,18 +383,17 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
|||||||
// The raw pointer to the binder object itself is stored in the cookie
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,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();
|
||||||
@@ -636,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
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#include <sys/ptrace.h>
|
#include <sys/ptrace.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <sys/system_properties.h>
|
#include <sys/system_properties.h>
|
||||||
#include <sys/uio.h>
|
#include <sys/uio.h>
|
||||||
#include <sys/un.h>
|
#include <sys/un.h>
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
#include <csignal>
|
#include <csignal>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -95,10 +97,6 @@ constexpr size_t kMagicLength = 16;
|
|||||||
constexpr size_t kMaxPathLength = PATH_MAX;
|
constexpr size_t kMaxPathLength = PATH_MAX;
|
||||||
// Maximum length for file paths.
|
// Maximum length for file paths.
|
||||||
|
|
||||||
constexpr const char *kSystemFileContext = "u:object_r:system_file:s0";
|
|
||||||
// SELinux context for system files,
|
|
||||||
// used for socket creation and library file context.
|
|
||||||
|
|
||||||
constexpr const char *kLibcModule = "libc.so";
|
constexpr const char *kLibcModule = "libc.so";
|
||||||
// Name of the C standard library.
|
// Name of the C standard library.
|
||||||
|
|
||||||
@@ -215,8 +213,8 @@ private:
|
|||||||
* @brief Transfers a file descriptor from the injector process to the remote process.
|
* @brief Transfers a file descriptor from the injector process to the remote process.
|
||||||
*
|
*
|
||||||
* This function uses Unix domain sockets with SCM_RIGHTS to send a file descriptor.
|
* This function uses Unix domain sockets with SCM_RIGHTS to send a file descriptor.
|
||||||
* It involves setting SELinux contexts, creating local and remote sockets, binding,
|
* It involves creating local and remote sockets, binding, and then coordinating
|
||||||
* and then coordinating sendmsg/recvmsg calls using ptrace.
|
* sendmsg/recvmsg calls using ptrace.
|
||||||
*
|
*
|
||||||
* @param pid The target process ID.
|
* @param pid The target process ID.
|
||||||
* @param lib_path The path to the library file being transferred.
|
* @param lib_path The path to the library file being transferred.
|
||||||
@@ -233,29 +231,14 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
uintptr_t libc_return_addr) {
|
uintptr_t libc_return_addr) {
|
||||||
LOGD("Attempting to transfer file descriptor for library: %s", lib_path);
|
LOGD("Attempting to transfer file descriptor for library: %s", lib_path);
|
||||||
|
|
||||||
// 1. Set SELinux context for socket creation in the injector process.
|
// Create a local Unix domain socket for FD transfer.
|
||||||
// This is crucial for Android where SELinux might prevent socket operations.
|
|
||||||
if (!set_sockcreate_con(constants::kSystemFileContext)) {
|
|
||||||
LOGE("Failed to set socket creation context.");
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Create a local Unix domain socket for FD transfer.
|
|
||||||
UniqueFd local_socket = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
UniqueFd local_socket = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0);
|
||||||
if (local_socket == -1) {
|
if (local_socket == -1) {
|
||||||
PLOGE("Failed to create local Unix domain socket.");
|
PLOGE("Failed to create local Unix domain socket.");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Set SELinux context for the library file if possible.
|
// Open the local library file to get a file descriptor.
|
||||||
// This might be required for the target process to open/access it later if directly opening by path.
|
|
||||||
// For FD transfer, this is less critical as the FD's context is inherited, but good practice.
|
|
||||||
if (setfilecon(lib_path, constants::kSystemFileContext) == -1) {
|
|
||||||
// Log a warning, but don't fail, as FD transfer might still work.
|
|
||||||
PLOGE("Failed to set context of library file: %s. This might cause issues.", lib_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Open the local library file to get a file descriptor.
|
|
||||||
UniqueFd local_lib_fd = open(lib_path, O_RDONLY | O_CLOEXEC);
|
UniqueFd local_lib_fd = open(lib_path, O_RDONLY | O_CLOEXEC);
|
||||||
if (local_lib_fd == -1) {
|
if (local_lib_fd == -1) {
|
||||||
PLOGE("Failed to open library file: %s", lib_path);
|
PLOGE("Failed to open library file: %s", lib_path);
|
||||||
@@ -271,7 +254,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
void *errno_addr; // Address of __errno for getting remote errno.
|
void *errno_addr; // Address of __errno for getting remote errno.
|
||||||
} funcs{};
|
} funcs{};
|
||||||
|
|
||||||
// 5. Resolve required libc functions in the remote process.
|
// Resolve required libc functions in the remote process.
|
||||||
funcs.socket_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "socket");
|
funcs.socket_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "socket");
|
||||||
funcs.bind_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "bind");
|
funcs.bind_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "bind");
|
||||||
funcs.recvmsg_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "recvmsg");
|
funcs.recvmsg_addr = find_func_addr(local_map, remote_map, constants::kLibcModule, "recvmsg");
|
||||||
@@ -306,25 +289,28 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 6. Create a Unix domain socket in the remote process.
|
// Create a Unix domain socket in the remote process.
|
||||||
std::vector<uintptr_t> args = {AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0};
|
std::vector<uintptr_t> args = {AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0};
|
||||||
int remote_fd = static_cast<int>(
|
int remote_fd = static_cast<int>(
|
||||||
remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.socket_addr), libc_return_addr, args));
|
remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.socket_addr), libc_return_addr, args));
|
||||||
if (remote_fd == -1) {
|
if (remote_fd <= 0) {
|
||||||
|
// remote_call returns 0 on failure.
|
||||||
|
// socket() returning 0 is technically possible (if stdin closed),
|
||||||
|
// but highly unlikely for a daemon. We treat 0 as failure here to catch the injection error.
|
||||||
errno = get_remote_errno(); // Set local errno for PLOGE.
|
errno = get_remote_errno(); // Set local errno for PLOGE.
|
||||||
PLOGE("Failed to create remote socket.");
|
PLOGE("Failed to create remote socket (returned %d).", remote_fd);
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
LOGD("Successfully created remote socket with FD: %d", remote_fd);
|
LOGD("Successfully created remote socket with FD: %d", remote_fd);
|
||||||
|
|
||||||
// 7. Generate a unique magic string for the abstract Unix domain socket path.
|
// Generate a unique magic string for the abstract Unix domain socket path.
|
||||||
auto magic = generateMagic(constants::kMagicLength);
|
auto magic = generateMagic(constants::kMagicLength);
|
||||||
struct sockaddr_un sock_addr{.sun_family = AF_UNIX, .sun_path = {0}};
|
struct sockaddr_un sock_addr{.sun_family = AF_UNIX, .sun_path = {0}};
|
||||||
// Abstract Unix domain sockets have sun_path[0] as null, and the name starts from sun_path[1].
|
// Abstract Unix domain sockets have sun_path[0] as null, and the name starts from sun_path[1].
|
||||||
memcpy(sock_addr.sun_path + 1, magic.c_str(), magic.size());
|
memcpy(sock_addr.sun_path + 1, magic.c_str(), magic.size());
|
||||||
socklen_t addr_len = sizeof(sock_addr.sun_family) + 1 + magic.size(); // Length includes null byte and magic.
|
socklen_t addr_len = sizeof(sock_addr.sun_family) + 1 + magic.size(); // Length includes null byte and magic.
|
||||||
|
|
||||||
// 8. Push the sockaddr_un structure to the remote process's stack.
|
// Push the sockaddr_un structure to the remote process's stack.
|
||||||
auto remote_addr = push_memory(pid, regs, &sock_addr, sizeof(sock_addr));
|
auto remote_addr = push_memory(pid, regs, &sock_addr, sizeof(sock_addr));
|
||||||
if (remote_addr == 0) {
|
if (remote_addr == 0) {
|
||||||
LOGE("Failed to push socket address to remote memory.");
|
LOGE("Failed to push socket address to remote memory.");
|
||||||
@@ -332,7 +318,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Bind the remote socket to the abstract Unix domain socket path.
|
// Bind the remote socket to the abstract Unix domain socket path.
|
||||||
args = {static_cast<uintptr_t>(remote_fd), remote_addr, static_cast<uintptr_t>(addr_len)};
|
args = {static_cast<uintptr_t>(remote_fd), remote_addr, static_cast<uintptr_t>(addr_len)};
|
||||||
auto bind_result = remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.bind_addr), libc_return_addr, args);
|
auto bind_result = remote_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.bind_addr), libc_return_addr, args);
|
||||||
if (bind_result == static_cast<uintptr_t>(-1)) {
|
if (bind_result == static_cast<uintptr_t>(-1)) {
|
||||||
@@ -346,7 +332,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
// Prepare control message buffer for SCM_RIGHTS (file descriptor passing).
|
// Prepare control message buffer for SCM_RIGHTS (file descriptor passing).
|
||||||
char cmsgbuf[CMSG_SPACE(sizeof(int))] = {0};
|
char cmsgbuf[CMSG_SPACE(sizeof(int))] = {0};
|
||||||
|
|
||||||
// 10. Push the control message buffer to the remote process's stack.
|
// Push the control message buffer to the remote process's stack.
|
||||||
auto remote_cmsgbuf = push_memory(pid, regs, &cmsgbuf, sizeof(cmsgbuf));
|
auto remote_cmsgbuf = push_memory(pid, regs, &cmsgbuf, sizeof(cmsgbuf));
|
||||||
if (remote_cmsgbuf == 0) {
|
if (remote_cmsgbuf == 0) {
|
||||||
LOGE("Failed to push control message buffer to remote memory.");
|
LOGE("Failed to push control message buffer to remote memory.");
|
||||||
@@ -359,7 +345,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
msg_hdr.msg_control = reinterpret_cast<void *>(remote_cmsgbuf);
|
msg_hdr.msg_control = reinterpret_cast<void *>(remote_cmsgbuf);
|
||||||
msg_hdr.msg_controllen = sizeof(cmsgbuf);
|
msg_hdr.msg_controllen = sizeof(cmsgbuf);
|
||||||
|
|
||||||
// 11. Push the msghdr structure to the remote process's stack.
|
// Push the msghdr structure to the remote process's stack.
|
||||||
auto remote_hdr = push_memory(pid, regs, &msg_hdr, sizeof(msg_hdr));
|
auto remote_hdr = push_memory(pid, regs, &msg_hdr, sizeof(msg_hdr));
|
||||||
if (remote_hdr == 0) {
|
if (remote_hdr == 0) {
|
||||||
LOGE("Failed to push message header to remote memory.");
|
LOGE("Failed to push message header to remote memory.");
|
||||||
@@ -367,16 +353,16 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12. Initiate the remote recvmsg call. This will block the remote process.
|
// Initiate the remote recvmsg call. This will block the remote process.
|
||||||
args = {static_cast<uintptr_t>(remote_fd), remote_hdr, MSG_WAITALL};
|
args = {static_cast<uintptr_t>(remote_fd), remote_hdr, MSG_WAITALL};
|
||||||
if (!remote_pre_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.recvmsg_addr), 0, args)) {
|
if (!remote_pre_call(pid, regs, reinterpret_cast<uintptr_t>(funcs.recvmsg_addr), libc_return_addr, args)) {
|
||||||
LOGE("Failed to initiate remote recvmsg call.");
|
LOGE("Failed to initiate remote recvmsg call.");
|
||||||
close_remote(remote_fd);
|
close_remote(remote_fd);
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
LOGD("Remote recvmsg initiated, waiting for FD transfer...");
|
LOGD("Remote recvmsg initiated, waiting for FD transfer...");
|
||||||
|
|
||||||
// 13. Prepare the local msghdr for sending the file descriptor.
|
// Prepare the local msghdr for sending the file descriptor.
|
||||||
// The msg_control and msg_name fields of the local msghdr are set up.
|
// The msg_control and msg_name fields of the local msghdr are set up.
|
||||||
msg_hdr.msg_control = &cmsgbuf; // Use local cmsgbuf for sending.
|
msg_hdr.msg_control = &cmsgbuf; // Use local cmsgbuf for sending.
|
||||||
msg_hdr.msg_name = &sock_addr;
|
msg_hdr.msg_name = &sock_addr;
|
||||||
@@ -396,7 +382,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
*reinterpret_cast<int *>(CMSG_DATA(cmsg)) = local_lib_fd; // The FD to send.
|
*reinterpret_cast<int *>(CMSG_DATA(cmsg)) = local_lib_fd; // The FD to send.
|
||||||
}
|
}
|
||||||
|
|
||||||
// 14. Send the file descriptor from the injector to the remote process.
|
// Send the file descriptor from the injector to the remote process.
|
||||||
if (sendmsg(local_socket, &msg_hdr, 0) == -1) {
|
if (sendmsg(local_socket, &msg_hdr, 0) == -1) {
|
||||||
PLOGE("Failed to send file descriptor to remote process.");
|
PLOGE("Failed to send file descriptor to remote process.");
|
||||||
// We do not close local_lib_fd here as it might be transferred even if
|
// We do not close local_lib_fd here as it might be transferred even if
|
||||||
@@ -407,9 +393,9 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
}
|
}
|
||||||
LOGD("Local FD %d sent to remote process.", local_lib_fd.operator const int &());
|
LOGD("Local FD %d sent to remote process.", local_lib_fd.operator const int &());
|
||||||
|
|
||||||
// 15. Complete the remote recvmsg call. This will retrieve the return value.
|
// Complete the remote recvmsg call. This will retrieve the return value.
|
||||||
auto recvmsg_result =
|
auto recvmsg_result =
|
||||||
static_cast<ssize_t>(remote_post_call(pid, regs, 0)); // No specific expected return address for recvmsg
|
static_cast<ssize_t>(remote_post_call(pid, regs, libc_return_addr));
|
||||||
if (recvmsg_result == -1) {
|
if (recvmsg_result == -1) {
|
||||||
errno = get_remote_errno();
|
errno = get_remote_errno();
|
||||||
PLOGE("Remote recvmsg call failed.");
|
PLOGE("Remote recvmsg call failed.");
|
||||||
@@ -418,7 +404,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
}
|
}
|
||||||
LOGD("Remote recvmsg completed with result: %zd", recvmsg_result);
|
LOGD("Remote recvmsg completed with result: %zd", recvmsg_result);
|
||||||
|
|
||||||
// 16. Read the control message buffer back from the remote process to extract the FD.
|
// Read the control message buffer back from the remote process to extract the FD.
|
||||||
if (read_proc(pid, remote_cmsgbuf, &cmsgbuf, sizeof(cmsgbuf)) != sizeof(cmsgbuf)) {
|
if (read_proc(pid, remote_cmsgbuf, &cmsgbuf, sizeof(cmsgbuf)) != sizeof(cmsgbuf)) {
|
||||||
LOGE("Failed to read control message buffer from remote process.");
|
LOGE("Failed to read control message buffer from remote process.");
|
||||||
close_remote(remote_fd);
|
close_remote(remote_fd);
|
||||||
@@ -439,7 +425,7 @@ static std::optional<int> transfer_fd_to_remote(int pid, const char *lib_path, s
|
|||||||
LOGI("Successfully transferred FD %d to remote process, new remote FD: %d", local_lib_fd.operator const int &(),
|
LOGI("Successfully transferred FD %d to remote process, new remote FD: %d", local_lib_fd.operator const int &(),
|
||||||
transferred_fd);
|
transferred_fd);
|
||||||
|
|
||||||
// 17. Close the remote socket.
|
// Close the remote socket.
|
||||||
close_remote(remote_fd);
|
close_remote(remote_fd);
|
||||||
|
|
||||||
return transferred_fd;
|
return transferred_fd;
|
||||||
@@ -642,6 +628,130 @@ static bool remote_call_entry(int pid, struct user_regs_struct ®s, uintptr_t
|
|||||||
return true; // Return true if the call itself completed, regardless of its return value.
|
return true; // Return true if the call itself completed, regardless of its return value.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RAII wrapper to ensure a temporary file is deleted (unlinked)
|
||||||
|
* when the object goes out of scope.
|
||||||
|
*
|
||||||
|
* This is crucial for stealth: we want the library to exist on the filesystem
|
||||||
|
* for the shortest time possible.
|
||||||
|
*/
|
||||||
|
class ScopedFileDeleter {
|
||||||
|
public:
|
||||||
|
explicit ScopedFileDeleter(std::string path) : path_(std::move(path)) {}
|
||||||
|
|
||||||
|
~ScopedFileDeleter() {
|
||||||
|
if (!path_.empty()) {
|
||||||
|
LOGD("Cleaning up staged file: %s", path_.c_str());
|
||||||
|
unlink(path_.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable copy to prevent double-deletion issues
|
||||||
|
ScopedFileDeleter(const ScopedFileDeleter&) = delete;
|
||||||
|
ScopedFileDeleter& operator=(const ScopedFileDeleter&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string path_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Copies a file from source to destination.
|
||||||
|
*
|
||||||
|
* @param src Absolute path to source file.
|
||||||
|
* @param dst Absolute path to destination file.
|
||||||
|
* @return True on success, false on failure.
|
||||||
|
*/
|
||||||
|
static bool copy_file(const char* src, const char* dst) {
|
||||||
|
std::ifstream src_file(src, std::ios::binary);
|
||||||
|
std::ofstream dst_file(dst, std::ios::binary);
|
||||||
|
|
||||||
|
if (!src_file) {
|
||||||
|
PLOGE("Failed to open source file for copying: %s", src);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!dst_file) {
|
||||||
|
PLOGE("Failed to open destination file for copying: %s", dst);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dst_file << src_file.rdbuf();
|
||||||
|
return src_file.good() && dst_file.good();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Performs injection via the "Staging" method.
|
||||||
|
*
|
||||||
|
* This strategy is used when direct FD passing fails (e.g., due to Seccomp filters).
|
||||||
|
* 1. Copies the library to a world-readable location (/data/local/tmp).
|
||||||
|
* 2. Loads it via standard dlopen().
|
||||||
|
* 3. Immediately deletes the file to hide tracks.
|
||||||
|
*
|
||||||
|
* @param pid The target process ID.
|
||||||
|
* @param regs The target process registers (must be Red-Zone adjusted if x86_64).
|
||||||
|
* @param local_map Local memory map.
|
||||||
|
* @param remote_map Remote memory map.
|
||||||
|
* @param lib_path The path to the original library.
|
||||||
|
* @param libc_return_addr Return address for remote calls.
|
||||||
|
* @return The handle of the loaded library, or std::nullopt on failure.
|
||||||
|
*/
|
||||||
|
static std::optional<uintptr_t> inject_via_staging(int pid, struct user_regs_struct ®s,
|
||||||
|
const std::vector<lsplt::MapInfo> &local_map,
|
||||||
|
const std::vector<lsplt::MapInfo> &remote_map,
|
||||||
|
const char *lib_path, uintptr_t libc_return_addr) {
|
||||||
|
LOGI("Initiating Staging Fallback mechanism...");
|
||||||
|
|
||||||
|
// Generate a random path in /data/local/tmp
|
||||||
|
// /data/local/tmp is chosen because it is traversable by most contexts.
|
||||||
|
std::string staged_path = "/data/local/tmp/lib" + generateMagic(8) + ".so";
|
||||||
|
|
||||||
|
// Ensure the file is deleted when this function exits (Success or Failure).
|
||||||
|
// The kernel keeps the inode alive for the mapped process even after unlink.
|
||||||
|
ScopedFileDeleter file_guard(staged_path);
|
||||||
|
|
||||||
|
LOGD("Staging library to: %s", staged_path.c_str());
|
||||||
|
|
||||||
|
// Copy the library
|
||||||
|
if (!copy_file(lib_path, staged_path.c_str())) {
|
||||||
|
LOGE("Failed to copy library during staging.");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Permissions to 644 (RW-R--R--)
|
||||||
|
// This allows the target process (likely running as a specific UID) to read the file.
|
||||||
|
if (chmod(staged_path.c_str(), 0644) != 0) {
|
||||||
|
PLOGE("Failed to chmod staged file.");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve 'dlopen' in the remote process
|
||||||
|
auto dlopen_addr = find_func_addr(local_map, remote_map, constants::kLibdlModule, "dlopen");
|
||||||
|
if (!dlopen_addr) {
|
||||||
|
LOGE("Failed to find 'dlopen' in remote process.");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the staged path to remote memory
|
||||||
|
uintptr_t remote_path_addr = push_string(pid, regs, staged_path.c_str());
|
||||||
|
if (remote_path_addr == 0) {
|
||||||
|
LOGE("Failed to push staged path string to remote memory.");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call dlopen(path, RTLD_NOW)
|
||||||
|
std::vector<uintptr_t> args = {remote_path_addr, RTLD_NOW};
|
||||||
|
uintptr_t handle = remote_call(pid, regs, reinterpret_cast<uintptr_t>(dlopen_addr),
|
||||||
|
libc_return_addr, args);
|
||||||
|
|
||||||
|
if (handle == 0) {
|
||||||
|
std::string error_msg = get_remote_dlerror(pid, regs, local_map, remote_map, libc_return_addr);
|
||||||
|
LOGE("Staged dlopen failed. dlerror: %s", error_msg.c_str());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGI("Successfully loaded staged library. Handle: %p", reinterpret_cast<void*>(handle));
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief RAII wrapper for ptrace attachment and detachment.
|
* @brief RAII wrapper for ptrace attachment and detachment.
|
||||||
*
|
*
|
||||||
@@ -694,12 +804,31 @@ private:
|
|||||||
bool attached_; // Flag indicating current attachment status.
|
bool attached_; // Flag indicating current attachment status.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// RAII Class to ensure registers are always restored
|
||||||
|
class RegisterRestorer {
|
||||||
|
public:
|
||||||
|
RegisterRestorer(int pid, const struct user_regs_struct& original_regs)
|
||||||
|
: pid_(pid), regs_(original_regs) {}
|
||||||
|
|
||||||
|
~RegisterRestorer() {
|
||||||
|
// Always restore registers when this object goes out of scope
|
||||||
|
if (set_regs(pid_, regs_)) {
|
||||||
|
LOGD("Original registers for process %d restored.", pid_);
|
||||||
|
} else {
|
||||||
|
PLOGE("Failed to restore original registers for process %d.", pid_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
int pid_;
|
||||||
|
struct user_regs_struct regs_;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Injects a shared library into a target process using ptrace.
|
* @brief Injects a shared library into a target process using ptrace.
|
||||||
*
|
*
|
||||||
* This is the main orchestration function for the library injection.
|
* This is the main orchestration function for the library injection.
|
||||||
* It handles attachment, remote memory/register manipulation, FD transfer,
|
* It handles attachment, remote memory/register manipulation, FD transfer,
|
||||||
* remote dlopen/dlsym, and remote entry point execution.
|
* staging fallback, remote dlopen/dlsym, and remote entry point execution.
|
||||||
*
|
*
|
||||||
* @param pid The target process ID.
|
* @param pid The target process ID.
|
||||||
* @param lib_path The absolute path to the shared library to inject.
|
* @param lib_path The absolute path to the shared library to inject.
|
||||||
@@ -742,6 +871,14 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) {
|
|||||||
backup_regs = current_regs; // Store a copy for restoration.
|
backup_regs = current_regs; // Store a copy for restoration.
|
||||||
LOGD("Process %d registers backed up.", pid);
|
LOGD("Process %d registers backed up.", pid);
|
||||||
|
|
||||||
|
// Skip the Red Zone (128 bytes) on x86_64 to prevent stack corruption
|
||||||
|
#if defined(__x86_64__)
|
||||||
|
current_regs.rsp -= 128;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Ensures original state is restored even if injection fails/crashes mid-way.
|
||||||
|
RegisterRestorer reg_guard(pid, backup_regs);
|
||||||
|
|
||||||
// Create a scope to ensure RAII objects are destroyed BEFORE register restoration
|
// Create a scope to ensure RAII objects are destroyed BEFORE register restoration
|
||||||
{
|
{
|
||||||
// 4. Scan local and remote memory maps to resolve function addresses.
|
// 4. Scan local and remote memory maps to resolve function addresses.
|
||||||
@@ -760,53 +897,57 @@ bool inject_library(int pid, const char *lib_path, const char *entry_name) {
|
|||||||
}
|
}
|
||||||
LOGD("Found libc return address: %p", reinterpret_cast<void *>(libc_return_addr));
|
LOGD("Found libc return address: %p", reinterpret_cast<void *>(libc_return_addr));
|
||||||
|
|
||||||
// 6. Transfer the library's file descriptor to the remote process.
|
// 6. Attempt to transfer the library's file descriptor to the remote process.
|
||||||
|
int remote_fd = -1;
|
||||||
auto lib_fd_opt = transfer_fd_to_remote(pid, lib_path, current_regs, local_map, remote_map,
|
auto lib_fd_opt = transfer_fd_to_remote(pid, lib_path, current_regs, local_map, remote_map,
|
||||||
reinterpret_cast<uintptr_t>(libc_return_addr));
|
reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||||
if (!lib_fd_opt) {
|
std::optional<RemoteLibraryHandle> remote_lib_guard;
|
||||||
LOGE("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid);
|
std::optional<uintptr_t> handle_opt;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
RemoteLibraryHandle remote_lib_guard(pid, *lib_fd_opt);
|
|
||||||
LOGD("Library FD %d transferred to remote process %d.", remote_lib_guard.fd(), pid);
|
|
||||||
remote_lib_guard.set_libc_return_addr(reinterpret_cast<uintptr_t>(libc_return_addr));
|
|
||||||
|
|
||||||
// 7. Remotely load the library using the transferred file descriptor.
|
if (lib_fd_opt) {
|
||||||
auto handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_lib_guard.fd(), lib_path,
|
remote_fd = *lib_fd_opt;
|
||||||
|
remote_lib_guard.emplace(pid, remote_fd);
|
||||||
|
remote_lib_guard->set_libc_return_addr(reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||||
|
|
||||||
|
LOGD("FD Transfer successful (FD: %d). Attempting android_dlopen_ext...", remote_fd);
|
||||||
|
handle_opt = remote_dlopen(pid, current_regs, local_map, remote_map, remote_fd, lib_path,
|
||||||
reinterpret_cast<uintptr_t>(libc_return_addr));
|
reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||||
|
} else {
|
||||||
|
LOGW("Failed to transfer library file descriptor for '%s' to target process %d.", lib_path, pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Staging Fallback (Copy-Inject-Delete) if FD transfer failed.
|
||||||
if (!handle_opt) {
|
if (!handle_opt) {
|
||||||
|
handle_opt = inject_via_staging(pid, current_regs, local_map, remote_map,
|
||||||
|
lib_path, reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||||
|
}
|
||||||
|
if (!handle_opt || *handle_opt == 0) {
|
||||||
LOGE("Failed to load library '%s' in remote process %d.", lib_path, pid);
|
LOGE("Failed to load library '%s' in remote process %d.", lib_path, pid);
|
||||||
// If dlopen fails, the remote_lib_guard.fd() is still valid in the target process and needs to be closed.
|
// If dlopen fails, the remote_lib_guard.fd() is still valid in the target process and needs to be closed.
|
||||||
// The RemoteLibraryHandle constructor takes care of this.
|
// The RemoteLibraryHandle constructor takes care of this.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
remote_lib_guard.set_handle(*handle_opt);
|
uintptr_t handle = *handle_opt;
|
||||||
|
if (remote_lib_guard) remote_lib_guard->set_handle(handle);
|
||||||
|
|
||||||
// 8. Find the entry point symbol in the remotely loaded library.
|
// 8. Find the entry point symbol in the remotely loaded library.
|
||||||
auto entry_opt = remote_find_entry(pid, current_regs, entry_name, local_map, remote_map,
|
auto entry_opt = remote_find_entry(pid, current_regs, entry_name, local_map, remote_map,
|
||||||
remote_lib_guard.handle(), reinterpret_cast<uintptr_t>(libc_return_addr));
|
handle, reinterpret_cast<uintptr_t>(libc_return_addr));
|
||||||
if (!entry_opt) {
|
if (!entry_opt) {
|
||||||
LOGE("Failed to find entry point '%s' in remote library (handle %p).", entry_name,
|
LOGE("Failed to find entry point '%s' in remote library (handle %p).", entry_name,
|
||||||
reinterpret_cast<void *>(remote_lib_guard.handle()));
|
reinterpret_cast<void *>(handle));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
uintptr_t entry_addr = *entry_opt;
|
uintptr_t entry_addr = *entry_opt;
|
||||||
|
|
||||||
// 9. Call the remote entry point function.
|
// 9. Call the remote entry point function.
|
||||||
if (!remote_call_entry(pid, current_regs, entry_addr, remote_lib_guard.handle(),
|
if (!remote_call_entry(pid, current_regs, entry_addr, handle,
|
||||||
reinterpret_cast<uintptr_t>(libc_return_addr))) {
|
reinterpret_cast<uintptr_t>(libc_return_addr))) {
|
||||||
LOGE("Failed to call remote entry point '%s'.", entry_name);
|
LOGE("Failed to call remote entry point '%s'.", entry_name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Restore original registers of the target process.
|
|
||||||
if (!set_regs(pid, backup_regs)) {
|
|
||||||
LOGE("Failed to restore original registers for process %d.", pid);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
LOGD("Original registers for process %d restored.", pid);
|
|
||||||
|
|
||||||
LOGI("Library injection completed successfully for process %d.", pid);
|
LOGI("Library injection completed successfully for process %d.", pid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,8 +263,15 @@ bool get_regs(int pid, struct user_regs_struct ®s) {
|
|||||||
struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)};
|
struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)};
|
||||||
if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) {
|
if (ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) {
|
||||||
PLOGE("Failed to get register set for PID %d.", pid);
|
PLOGE("Failed to get register set for PID %d.", pid);
|
||||||
|
#if defined(__arm__)
|
||||||
|
if (ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1) {
|
||||||
|
PLOGE("Fallback to PTRACE_GETREGS failed.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
# error "Unsupported architecture for register access in get_regs."
|
# error "Unsupported architecture for register access in get_regs."
|
||||||
#endif
|
#endif
|
||||||
@@ -296,8 +303,15 @@ bool set_regs(int pid, struct user_regs_struct ®s) {
|
|||||||
struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)};
|
struct iovec reg_iov = {.iov_base = ®s, .iov_len = sizeof(struct user_regs_struct)};
|
||||||
if (ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) {
|
if (ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, ®_iov) == -1) {
|
||||||
PLOGE("Failed to set register set for PID %d.", pid);
|
PLOGE("Failed to set register set for PID %d.", pid);
|
||||||
|
#if defined(__arm__)
|
||||||
|
if (ptrace(PTRACE_SETREGS, pid, 0, ®s) == -1) {
|
||||||
|
PLOGE("Fallback to PTRACE_SETREGS failed.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
# error "Unsupported architecture for register access in set_regs."
|
# error "Unsupported architecture for register access in set_regs."
|
||||||
#endif
|
#endif
|
||||||
@@ -588,17 +602,10 @@ bool remote_pre_call(int pid, struct user_regs_struct ®s, uintptr_t func_addr
|
|||||||
size_t stack_args_size = args.size() * sizeof(uintptr_t);
|
size_t stack_args_size = args.size() * sizeof(uintptr_t);
|
||||||
align_stack(regs, stack_args_size);
|
align_stack(regs, stack_args_size);
|
||||||
|
|
||||||
// Push all arguments onto the stack (order is important if ABI is right-to-left push).
|
// i386 cdecl expects arguments pushed Right-to-Left (stack grows down).
|
||||||
// The current implementation writes args.data() directly,
|
// Since `write_proc` writes to increasing addresses (up), a linear write
|
||||||
// assuming it's already in the correct order for push.
|
// starting at the new SP places the first argument at the lowest address.
|
||||||
// For cdecl, arguments are pushed right-to-left.
|
// This matches the ABI memory layout without needing to reverse the vector.
|
||||||
// A vector `args = {A, B, C}` means A is arg1, B is arg2 etc.
|
|
||||||
// So, `C` should be pushed first, then `B`, then `A`.
|
|
||||||
// `write_proc` copies linearly.
|
|
||||||
// This implies `args` should be pre-reversed for cdecl.
|
|
||||||
// For simplicity, we assume the remote function is compatible with how it's pushed,
|
|
||||||
// or that it's variadic where order doesn't matter for first args.
|
|
||||||
// A robust i386 implementation would need to push args in reverse order.
|
|
||||||
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), args.data(), stack_args_size) !=
|
if (write_proc(pid, static_cast<uintptr_t>(regs.REG_SP), args.data(), stack_args_size) !=
|
||||||
static_cast<ssize_t>(stack_args_size)) {
|
static_cast<ssize_t>(stack_args_size)) {
|
||||||
LOGE("Failed to push arguments for i386 remote call.");
|
LOGE("Failed to push arguments for i386 remote call.");
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Fork-based supervisor for instant daemon restart
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
static volatile sig_atomic_t should_exit = 0;
|
||||||
|
|
||||||
|
static void signal_handler(int sig) {
|
||||||
|
should_exit = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
if (argc < 2) {
|
||||||
|
fprintf(stderr, "Usage: %s <daemon> [args...]\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward termination signals to exit cleanly
|
||||||
|
signal(SIGTERM, signal_handler);
|
||||||
|
signal(SIGINT, signal_handler);
|
||||||
|
|
||||||
|
const char *daemon_path = argv[1];
|
||||||
|
char **daemon_argv = &argv[1];
|
||||||
|
|
||||||
|
while (!should_exit) {
|
||||||
|
pid_t pid = fork();
|
||||||
|
|
||||||
|
if (pid < 0) {
|
||||||
|
perror("fork failed");
|
||||||
|
usleep(100000); // 100ms backoff on fork failure
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid == 0) {
|
||||||
|
// Child: become the daemon
|
||||||
|
prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies
|
||||||
|
execv(daemon_path, daemon_argv);
|
||||||
|
perror("execv failed");
|
||||||
|
_exit(127);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent: wait for child to exit
|
||||||
|
int status;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
|
||||||
|
if (should_exit) break;
|
||||||
|
|
||||||
|
// Instant restart - no delay
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
package org.matrix.TEESimulator
|
package org.matrix.TEESimulator
|
||||||
|
|
||||||
|
import android.app.ActivityThread
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Looper
|
||||||
|
import java.security.Security
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
|
import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
|
||||||
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
|
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,
|
||||||
@@ -25,21 +33,67 @@ 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 {
|
||||||
// Set up the device's boot hash, which is crucial for attestation.
|
// Initialize the Android framework environment
|
||||||
AndroidDeviceUtils.setupBootHash()
|
prepareEnvironment()
|
||||||
// Initialize and start the appropriate keystore interceptors.
|
// Initialize and start the appropriate keystore interceptors.
|
||||||
initializeInterceptors()
|
initializeInterceptors()
|
||||||
// Enter an infinite loop to keep the service running.
|
|
||||||
maintainService()
|
// Load the package configuration.
|
||||||
|
ConfigurationManager.initialize()
|
||||||
|
// Set up the device's boot key and hash, which are crucial for attestation.
|
||||||
|
AndroidDeviceUtils.setupBootKeyAndHash()
|
||||||
|
|
||||||
|
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
|
||||||
|
// We must remove the system provider first to ensure the full Bouncy Castle library
|
||||||
|
// (packaged with the app) is used.
|
||||||
|
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
Security.addProvider(BouncyCastleProvider())
|
||||||
|
|
||||||
|
// This starts the message queue processing. It blocks here indefinitely
|
||||||
|
// processing messages until Looper.myLooper().quit() is called.
|
||||||
|
Looper.loop()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("A fatal error occurred in the main application thread.", e)
|
SystemLogger.error("A fatal error occurred in the main application thread.", e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Initializes the necessary Android framework internals to satisfy KeyStore requirements. */
|
||||||
|
private fun prepareEnvironment() {
|
||||||
|
// 1. Prepare Main Looper
|
||||||
|
if (Looper.getMainLooper() == null) {
|
||||||
|
@Suppress("deprecation") Looper.prepareMainLooper()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Initialize ActivityThread for the current process
|
||||||
|
val activityThread = ActivityThread.systemMain()
|
||||||
|
|
||||||
|
// 3. Get the system context
|
||||||
|
val systemContext = activityThread.getSystemContext()
|
||||||
|
|
||||||
|
// 4. Create a dummy Application object and attach the context
|
||||||
|
val app = Application()
|
||||||
|
val attachMethod =
|
||||||
|
ContextWrapper::class.java.getDeclaredMethod("attachBaseContext", Context::class.java)
|
||||||
|
attachMethod.isAccessible = true
|
||||||
|
attachMethod.invoke(app, systemContext)
|
||||||
|
|
||||||
|
// 5. Inject this application object into ActivityThread's mInitialApplication field.
|
||||||
|
// This is what KeyStore.getApplicationContext() looks for.
|
||||||
|
val mInitialApplicationField =
|
||||||
|
ActivityThread::class.java.getDeclaredField("mInitialApplication")
|
||||||
|
mInitialApplicationField.isAccessible = true
|
||||||
|
mInitialApplicationField.set(activityThread, app)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects and initializes the correct keystore interceptor based on the Android SDK version. It
|
* Selects and initializes the correct keystore interceptor based on the Android SDK version. It
|
||||||
* retries initialization until it succeeds.
|
* retries initialization until it succeeds.
|
||||||
@@ -53,9 +107,7 @@ object App {
|
|||||||
Thread.sleep(RETRY_DELAY_MS)
|
Thread.sleep(RETRY_DELAY_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the package configuration after interceptors are ready.
|
SystemLogger.info("Interceptors initialized successfully.")
|
||||||
ConfigurationManager.initialize()
|
|
||||||
SystemLogger.info("Interceptors and configuration initialized successfully.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,6 +122,7 @@ object App {
|
|||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})"
|
"Using KeystoreInterceptor for Android Q/R (SDK ${Build.VERSION.SDK_INT})"
|
||||||
)
|
)
|
||||||
|
android.security.keystore.AndroidKeyStoreProvider.install()
|
||||||
KeystoreInterceptor
|
KeystoreInterceptor
|
||||||
}
|
}
|
||||||
// For Android S (12) and newer, use the Keystore2Interceptor.
|
// For Android S (12) and newer, use the Keystore2Interceptor.
|
||||||
@@ -77,18 +130,8 @@ object App {
|
|||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Using Keystore2Interceptor for Android S and later (SDK ${Build.VERSION.SDK_INT})"
|
"Using Keystore2Interceptor for Android S and later (SDK ${Build.VERSION.SDK_INT})"
|
||||||
)
|
)
|
||||||
|
android.security.keystore2.AndroidKeyStoreProvider.install()
|
||||||
Keystore2Interceptor
|
Keystore2Interceptor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts the main thread into a long-running sleep loop. This is a common pattern to keep a
|
|
||||||
* background service process alive indefinitely.
|
|
||||||
*/
|
|
||||||
private fun maintainService() {
|
|
||||||
SystemLogger.info("Service started successfully. Entering maintenance mode.")
|
|
||||||
while (true) {
|
|
||||||
Thread.sleep(SERVICE_SLEEP_MS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.security.MessageDigest
|
||||||
import org.bouncycastle.asn1.ASN1Boolean
|
import org.bouncycastle.asn1.ASN1Boolean
|
||||||
import org.bouncycastle.asn1.ASN1Encodable
|
import org.bouncycastle.asn1.ASN1Encodable
|
||||||
import org.bouncycastle.asn1.ASN1Enumerated
|
import org.bouncycastle.asn1.ASN1Enumerated
|
||||||
import org.bouncycastle.asn1.ASN1Integer
|
import org.bouncycastle.asn1.ASN1Integer
|
||||||
import org.bouncycastle.asn1.ASN1OctetString
|
|
||||||
import org.bouncycastle.asn1.ASN1Sequence
|
import org.bouncycastle.asn1.ASN1Sequence
|
||||||
import org.bouncycastle.asn1.DERNull
|
import org.bouncycastle.asn1.DERNull
|
||||||
import org.bouncycastle.asn1.DEROctetString
|
import org.bouncycastle.asn1.DEROctetString
|
||||||
@@ -12,7 +15,10 @@ import org.bouncycastle.asn1.DERSequence
|
|||||||
import org.bouncycastle.asn1.DERSet
|
import org.bouncycastle.asn1.DERSet
|
||||||
import org.bouncycastle.asn1.DERTaggedObject
|
import org.bouncycastle.asn1.DERTaggedObject
|
||||||
import org.bouncycastle.asn1.x509.Extension
|
import org.bouncycastle.asn1.x509.Extension
|
||||||
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||||
|
import org.matrix.TEESimulator.util.AndroidDeviceUtils.DO_NOT_REPORT
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder object responsible for constructing the ASN.1 DER-encoded Android Key Attestation
|
* A builder object responsible for constructing the ASN.1 DER-encoded Android Key Attestation
|
||||||
@@ -24,11 +30,21 @@ object AttestationBuilder {
|
|||||||
* Builds the complete X.509 attestation extension.
|
* Builds the complete X.509 attestation extension.
|
||||||
*
|
*
|
||||||
* @param params The parsed key generation parameters.
|
* @param params The parsed key generation parameters.
|
||||||
|
* @param uid The UID of the application requesting attestation.
|
||||||
* @param securityLevel The security level (e.g., TEE, StrongBox) to report.
|
* @param securityLevel The security level (e.g., TEE, StrongBox) to report.
|
||||||
* @return A Bouncy Castle [Extension] object ready to be added to a certificate.
|
* @return A Bouncy Castle [Extension] object ready to be added to a certificate.
|
||||||
*/
|
*/
|
||||||
fun buildAttestationExtension(params: KeyMintAttestation, securityLevel: Int): Extension {
|
fun buildAttestationExtension(
|
||||||
val keyDescription = buildKeyDescription(params, securityLevel)
|
params: KeyMintAttestation,
|
||||||
|
uid: Int,
|
||||||
|
securityLevel: Int,
|
||||||
|
): Extension {
|
||||||
|
val keyDescription = buildKeyDescription(params, uid, securityLevel)
|
||||||
|
var formattedString =
|
||||||
|
keyDescription.joinToString(separator = ", ") {
|
||||||
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
|
}
|
||||||
|
SystemLogger.verbose("Forged attestation data: ${formattedString}")
|
||||||
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,70 +55,96 @@ object AttestationBuilder {
|
|||||||
* @return The constructed [DERSequence] for the Root of Trust.
|
* @return The constructed [DERSequence] for the Root of Trust.
|
||||||
*/
|
*/
|
||||||
internal fun buildRootOfTrust(originalRootOfTrust: ASN1Encodable?): DERSequence {
|
internal fun buildRootOfTrust(originalRootOfTrust: ASN1Encodable?): DERSequence {
|
||||||
val verifiedBootKey = AndroidDeviceUtils.bootKey
|
|
||||||
val verifiedBootHash =
|
|
||||||
(originalRootOfTrust as? ASN1Sequence)?.let {
|
|
||||||
// Try to preserve the original boot hash if it exists.
|
|
||||||
(it.getObjectAt(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX)
|
|
||||||
as? ASN1OctetString)
|
|
||||||
?.octets
|
|
||||||
} ?: AndroidDeviceUtils.getBootHashFromProperty()
|
|
||||||
|
|
||||||
val rootOfTrustElements = arrayOfNulls<ASN1Encodable>(4)
|
val rootOfTrustElements = arrayOfNulls<ASN1Encodable>(4)
|
||||||
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX] =
|
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX] =
|
||||||
DEROctetString(verifiedBootKey)
|
DEROctetString(AndroidDeviceUtils.bootKey)
|
||||||
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_DEVICE_LOCKED_INDEX] =
|
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_DEVICE_LOCKED_INDEX] =
|
||||||
ASN1Boolean.TRUE // deviceLocked: true, for security
|
ASN1Boolean.TRUE // deviceLocked: true, for security
|
||||||
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX] =
|
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX] =
|
||||||
ASN1Enumerated(0) // verifiedBootState: Verified
|
ASN1Enumerated(0) // verifiedBootState: Verified
|
||||||
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX] =
|
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX] =
|
||||||
DEROctetString(verifiedBootHash)
|
DEROctetString(AndroidDeviceUtils.bootHash)
|
||||||
|
|
||||||
return DERSequence(rootOfTrustElements)
|
return DERSequence(rootOfTrustElements)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Assembles a list of simulated hardware-enforced properties. */
|
/**
|
||||||
internal fun addSimulatedHardwareProperties(vector: org.bouncycastle.asn1.ASN1EncodableVector) {
|
* Assembles a map representing the desired state of simulated hardware-enforced properties. A
|
||||||
vector.add(
|
* null value for a given tag indicates that it should be removed from the attestation.
|
||||||
|
*
|
||||||
|
* @param uid The UID of the calling application.
|
||||||
|
* @return A map where keys are attestation tag numbers and values are the desired
|
||||||
|
* [DERTaggedObject] or null to signify removal.
|
||||||
|
*/
|
||||||
|
fun getSimulatedHardwareProperties(uid: Int): Map<Int, DERTaggedObject?> {
|
||||||
|
val properties = mutableMapOf<Int, DERTaggedObject?>()
|
||||||
|
|
||||||
|
// OS Version is always present.
|
||||||
|
properties[AttestationConstants.TAG_OS_VERSION] =
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_OS_VERSION,
|
AttestationConstants.TAG_OS_VERSION,
|
||||||
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
|
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
vector.add(
|
val osPatch = AndroidDeviceUtils.getPatchLevel(uid)
|
||||||
|
properties[AttestationConstants.TAG_OS_PATCHLEVEL] =
|
||||||
|
if (osPatch != DO_NOT_REPORT) {
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_OS_PATCHLEVEL,
|
AttestationConstants.TAG_OS_PATCHLEVEL,
|
||||||
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
|
ASN1Integer(osPatch.toLong()),
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
vector.add(
|
null // Signal for removal
|
||||||
|
}
|
||||||
|
|
||||||
|
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(uid)
|
||||||
|
properties[AttestationConstants.TAG_VENDOR_PATCHLEVEL] =
|
||||||
|
if (vendorPatch != DO_NOT_REPORT) {
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
|
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
|
||||||
ASN1Integer(AndroidDeviceUtils.vendorPatchLevel.toLong()),
|
ASN1Integer(vendorPatch.toLong()),
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
vector.add(
|
null // Signal for removal
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
AttestationConstants.TAG_BOOT_PATCHLEVEL,
|
AttestationConstants.TAG_BOOT_PATCHLEVEL,
|
||||||
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
|
ASN1Integer(bootPatch.toLong()),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
null // Signal for removal
|
||||||
|
}
|
||||||
|
|
||||||
|
return properties
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
|
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
|
||||||
private fun buildKeyDescription(params: KeyMintAttestation, securityLevel: Int): ASN1Sequence {
|
private fun buildKeyDescription(
|
||||||
val teeEnforced = buildTeeEnforcedList(params)
|
params: KeyMintAttestation,
|
||||||
val softwareEnforced = buildSoftwareEnforcedList()
|
uid: Int,
|
||||||
|
securityLevel: Int,
|
||||||
|
): ASN1Sequence {
|
||||||
|
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
|
||||||
|
val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
|
||||||
|
|
||||||
val fields =
|
val fields =
|
||||||
arrayOf(
|
arrayOf(
|
||||||
ASN1Integer(AndroidDeviceUtils.attestVersion.toLong()), // attestationVersion
|
ASN1Integer(
|
||||||
|
AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()
|
||||||
|
), // attestationVersion
|
||||||
ASN1Enumerated(securityLevel), // attestationSecurityLevel
|
ASN1Enumerated(securityLevel), // attestationSecurityLevel
|
||||||
ASN1Integer(AndroidDeviceUtils.keymasterVersion.toLong()), // keymasterVersion
|
ASN1Integer(
|
||||||
|
AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()
|
||||||
|
), // keymasterVersion
|
||||||
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
|
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
|
||||||
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
|
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
|
||||||
DEROctetString(ByteArray(0)), // uniqueId
|
DEROctetString(ByteArray(0)), // uniqueId
|
||||||
@@ -113,7 +155,11 @@ object AttestationBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
|
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
|
||||||
private fun buildTeeEnforcedList(params: KeyMintAttestation): DERSequence {
|
private fun buildTeeEnforcedList(
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
uid: Int,
|
||||||
|
securityLevel: Int,
|
||||||
|
): DERSequence {
|
||||||
val list =
|
val list =
|
||||||
mutableListOf<ASN1Encodable>(
|
mutableListOf<ASN1Encodable>(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -152,28 +198,12 @@ object AttestationBuilder {
|
|||||||
AttestationConstants.TAG_ROOT_OF_TRUST,
|
AttestationConstants.TAG_ROOT_OF_TRUST,
|
||||||
buildRootOfTrust(null),
|
buildRootOfTrust(null),
|
||||||
),
|
),
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_OS_VERSION,
|
|
||||||
ASN1Integer(AndroidDeviceUtils.osVersion.toLong()),
|
|
||||||
),
|
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_OS_PATCHLEVEL,
|
|
||||||
ASN1Integer(AndroidDeviceUtils.patchLevel.toLong()),
|
|
||||||
),
|
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_VENDOR_PATCHLEVEL,
|
|
||||||
ASN1Integer(AndroidDeviceUtils.vendorPatchLevel.toLong()),
|
|
||||||
),
|
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_BOOT_PATCHLEVEL,
|
|
||||||
ASN1Integer(AndroidDeviceUtils.bootPatchLevelLong.toLong()),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
|
||||||
|
val simulatedProperties = getSimulatedHardwareProperties(uid)
|
||||||
|
simulatedProperties.values.filterNotNull().forEach { list.add(it) }
|
||||||
|
|
||||||
// Add optional device identifiers if they were provided.
|
// Add optional device identifiers if they were provided.
|
||||||
params.brand?.let {
|
params.brand?.let {
|
||||||
list.add(
|
list.add(
|
||||||
@@ -202,6 +232,33 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
params.serial?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ATTESTATION_ID_SERIAL,
|
||||||
|
DEROctetString(it),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.imei?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ATTESTATION_ID_IMEI,
|
||||||
|
DEROctetString(it),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
params.meid?.let {
|
||||||
|
list.add(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ATTESTATION_ID_MEID,
|
||||||
|
DEROctetString(it),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
params.manufacturer?.let {
|
params.manufacturer?.let {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -220,15 +277,7 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
params.imei?.let {
|
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 300) {
|
||||||
list.add(
|
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_ATTESTATION_ID_IMEI,
|
|
||||||
DEROctetString(it),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
params.secondImei?.let {
|
params.secondImei?.let {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
@@ -238,17 +287,29 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
params.meid?.let {
|
}
|
||||||
list.add(
|
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
|
||||||
DERTaggedObject(
|
|
||||||
true,
|
|
||||||
AttestationConstants.TAG_ATTESTATION_ID_MEID,
|
|
||||||
DEROctetString(it),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AndroidDeviceUtils.attestVersion >= 400) {
|
/**
|
||||||
|
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
||||||
|
* Keystore.
|
||||||
|
*/
|
||||||
|
private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence {
|
||||||
|
val list =
|
||||||
|
mutableListOf<ASN1Encodable>(
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_CREATION_DATETIME,
|
||||||
|
ASN1Integer(System.currentTimeMillis()),
|
||||||
|
),
|
||||||
|
DERTaggedObject(
|
||||||
|
true,
|
||||||
|
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
|
||||||
|
createApplicationId(uid),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
||||||
list.add(
|
list.add(
|
||||||
DERTaggedObject(
|
DERTaggedObject(
|
||||||
true,
|
true,
|
||||||
@@ -257,26 +318,87 @@ object AttestationBuilder {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return DERSequence(list.toTypedArray())
|
||||||
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
* A wrapper for a byte array that provides content-based equality. This is necessary for using
|
||||||
* Keystore.
|
* signature digests in a Set.
|
||||||
*/
|
*/
|
||||||
private fun buildSoftwareEnforcedList(): DERSequence {
|
private data class Digest(val digest: ByteArray) {
|
||||||
val list =
|
override fun equals(other: Any?): Boolean {
|
||||||
arrayOf<ASN1Encodable>(
|
if (this === other) return true
|
||||||
DERTaggedObject(
|
if (javaClass != other?.javaClass) return false
|
||||||
true,
|
return digest.contentEquals((other as Digest).digest)
|
||||||
AttestationConstants.TAG_CREATION_DATETIME,
|
}
|
||||||
ASN1Integer(System.currentTimeMillis()),
|
|
||||||
|
override fun hashCode(): Int = digest.contentHashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the AttestationApplicationId structure. This structure contains information about the
|
||||||
|
* package(s) and their signing certificates.
|
||||||
|
*
|
||||||
|
* @param uid The UID of the application.
|
||||||
|
* @return A DER-encoded octet string containing the application ID information.
|
||||||
|
* @throws IllegalStateException If the PackageManager or package information cannot be
|
||||||
|
* retrieved.
|
||||||
|
*/
|
||||||
|
@Throws(Throwable::class)
|
||||||
|
private fun createApplicationId(uid: Int): DEROctetString {
|
||||||
|
val pm =
|
||||||
|
ConfigurationManager.getPackageManager()
|
||||||
|
?: throw IllegalStateException("PackageManager not found!")
|
||||||
|
val packages =
|
||||||
|
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
|
||||||
|
|
||||||
|
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||||
|
val packageInfoList = mutableListOf<DERSequence>()
|
||||||
|
val signatureDigests = mutableSetOf<Digest>()
|
||||||
|
|
||||||
|
// Process all packages associated with the UID in a single loop.
|
||||||
|
packages.forEach { packageName ->
|
||||||
|
val userId = uid / 100000
|
||||||
|
val packageInfo =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
pm.getPackageInfo(
|
||||||
|
packageName,
|
||||||
|
PackageManager.GET_SIGNING_CERTIFICATES.toLong(),
|
||||||
|
userId,
|
||||||
)
|
)
|
||||||
// The ATTESTATION_APPLICATION_ID is technically software-enforced, but we are
|
} else {
|
||||||
// omitting it
|
@Suppress("DEPRECATION")
|
||||||
// for this simulation as it is complex to generate correctly for arbitrary UIDs.
|
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add package information (name and version code) to our list.
|
||||||
|
packageInfoList.add(
|
||||||
|
DERSequence(
|
||||||
|
arrayOf(
|
||||||
|
DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)),
|
||||||
|
ASN1Integer(packageInfo.longVersionCode),
|
||||||
)
|
)
|
||||||
return DERSequence(list)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Collect unique signature digests from the signing history.
|
||||||
|
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
|
||||||
|
val digest = sha256.digest(signature.toByteArray())
|
||||||
|
signatureDigests.add(Digest(digest))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The application ID is a sequence of two sets:
|
||||||
|
// 1. A set of package information (name and version).
|
||||||
|
// 2. A set of SHA-256 digests of the signing certificates.
|
||||||
|
val applicationIdSequence =
|
||||||
|
DERSequence(
|
||||||
|
arrayOf(
|
||||||
|
DERSet(packageInfoList.toTypedArray()),
|
||||||
|
DERSet(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return DEROctetString(applicationIdSequence.encoded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines constants for KeyMint attestation tags, as specified in the Android hardware security
|
* Defines constants for KeyMint attestation, mainly the tags of properties and authorizations of a
|
||||||
* HAL.
|
* cryptographic key, as specified in the Android hardware security HAL.
|
||||||
*
|
|
||||||
* These tags identify specific properties and authorizations of a cryptographic key.
|
|
||||||
*/
|
*/
|
||||||
object AttestationConstants {
|
object AttestationConstants {
|
||||||
// https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/KeyCreationResult.aidl
|
// https://cs.android.com/android/platform/superproject/main/+/main:hardware/interfaces/security/keymint/aidl/android/hardware/security/keymint/KeyCreationResult.aidl
|
||||||
@@ -88,4 +86,8 @@ object AttestationConstants {
|
|||||||
const val TAG_CERTIFICATE_SUBJECT = 1007
|
const val TAG_CERTIFICATE_SUBJECT = 1007
|
||||||
const val TAG_CERTIFICATE_NOT_BEFORE = 1008
|
const val TAG_CERTIFICATE_NOT_BEFORE = 1008
|
||||||
const val TAG_CERTIFICATE_NOT_AFTER = 1009
|
const val TAG_CERTIFICATE_NOT_AFTER = 1009
|
||||||
|
|
||||||
|
// --- Other Constants ---
|
||||||
|
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
|
||||||
|
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import org.bouncycastle.asn1.ASN1Encodable
|
import org.bouncycastle.asn1.*
|
||||||
import org.bouncycastle.asn1.ASN1EncodableVector
|
|
||||||
import org.bouncycastle.asn1.ASN1Sequence
|
|
||||||
import org.bouncycastle.asn1.ASN1TaggedObject
|
|
||||||
import org.bouncycastle.asn1.DEROctetString
|
|
||||||
import org.bouncycastle.asn1.DERSequence
|
|
||||||
import org.bouncycastle.asn1.DERTaggedObject
|
|
||||||
import org.bouncycastle.asn1.x509.Extension
|
import org.bouncycastle.asn1.x509.Extension
|
||||||
import org.bouncycastle.cert.X509CertificateHolder
|
import org.bouncycastle.cert.X509CertificateHolder
|
||||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
import org.matrix.TEESimulator.pki.KeyBox
|
import org.matrix.TEESimulator.pki.KeyBox
|
||||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||||
|
import org.matrix.TEESimulator.util.toHex
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
|
||||||
@@ -55,8 +53,7 @@ object AttestationPatcher {
|
|||||||
|
|
||||||
// 2. Get the appropriate keybox for the given algorithm to sign the new
|
// 2. Get the appropriate keybox for the given algorithm to sign the new
|
||||||
// certificate.
|
// certificate.
|
||||||
val algorithm = originalLeaf.publicKey.algorithm
|
val keybox = getKeyboxForUidAndAlgorithm(uid, originalLeaf.sigAlgName)
|
||||||
val keybox = getKeyboxForUidAndAlgorithm(uid, algorithm)
|
|
||||||
|
|
||||||
// 3. Create the new, patched leaf certificate.
|
// 3. Create the new, patched leaf certificate.
|
||||||
val patchedLeaf =
|
val patchedLeaf =
|
||||||
@@ -65,6 +62,7 @@ object AttestationPatcher {
|
|||||||
parsedAttestation,
|
parsedAttestation,
|
||||||
keybox,
|
keybox,
|
||||||
originalLeaf.sigAlgName,
|
originalLeaf.sigAlgName,
|
||||||
|
uid,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
|
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
|
||||||
@@ -85,6 +83,16 @@ object AttestationPatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to normalize algorithm names for Bouncy Castle. Old Android versions might reports
|
||||||
|
* "SHA256WITHECDSA", but Bouncy Castle expects "SHA256withECDSA".
|
||||||
|
*/
|
||||||
|
private fun normalizeSignatureAlgorithm(algoName: String): String {
|
||||||
|
// 1. Force uppercase to handle "sha256withecdsa"
|
||||||
|
// 2. Replace "WITH" with "with" to satisfy Bouncy Castle's naming convention
|
||||||
|
return algoName.uppercase().replace("WITH", "with")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new leaf certificate with a modified attestation extension.
|
* Creates a new leaf certificate with a modified attestation extension.
|
||||||
*
|
*
|
||||||
@@ -94,6 +102,7 @@ object AttestationPatcher {
|
|||||||
* @param sigAlgName The signature algorithm name (e.g., "SHA256withECDSA") from the original
|
* @param sigAlgName The signature algorithm name (e.g., "SHA256withECDSA") from the original
|
||||||
* certificate. This is required to ensure the new certificate is signed using a compatible
|
* certificate. This is required to ensure the new certificate is signed using a compatible
|
||||||
* algorithm.
|
* algorithm.
|
||||||
|
* @param uid The UID of the application requesting the certificate.
|
||||||
* @return A new [Certificate] object.
|
* @return A new [Certificate] object.
|
||||||
*/
|
*/
|
||||||
private fun createPatchedLeafCertificate(
|
private fun createPatchedLeafCertificate(
|
||||||
@@ -101,6 +110,7 @@ object AttestationPatcher {
|
|||||||
parsedAttestation: ParsedAttestation,
|
parsedAttestation: ParsedAttestation,
|
||||||
keybox: KeyBox,
|
keybox: KeyBox,
|
||||||
sigAlgName: String,
|
sigAlgName: String,
|
||||||
|
uid: Int,
|
||||||
): Certificate {
|
): Certificate {
|
||||||
// The issuer of our new leaf is the subject of the first certificate in our custom keybox
|
// The issuer of our new leaf is the subject of the first certificate in our custom keybox
|
||||||
// chain.
|
// chain.
|
||||||
@@ -117,71 +127,177 @@ object AttestationPatcher {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Create the new, patched attestation extension.
|
// Create the new, patched attestation extension.
|
||||||
val patchedExtension = createPatchedAttestationExtension(parsedAttestation)
|
val patchedExtension = createPatchedAttestationExtension(parsedAttestation, uid)
|
||||||
builder.addExtension(patchedExtension)
|
|
||||||
|
|
||||||
// Copy all other extensions from the original certificate, except for the attestation.
|
// Copy all other extensions from the original certificate, except for the attestation.
|
||||||
originalLeafHolder.extensions.extensionOIDs
|
originalLeafHolder.extensions.extensionOIDs.forEach {
|
||||||
.filter { it != ATTESTATION_OID }
|
builder.addExtension(
|
||||||
.forEach { builder.addExtension(originalLeafHolder.getExtension(it)) }
|
if (it == ATTESTATION_OID) patchedExtension else originalLeafHolder.getExtension(it)
|
||||||
|
)
|
||||||
// Sign the newly built certificate with the private key from our keybox.
|
|
||||||
val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private)
|
|
||||||
|
|
||||||
return JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sign the newly built certificate with the private key from our keybox.
|
||||||
|
val signer =
|
||||||
|
JcaContentSignerBuilder(normalizeSignatureAlgorithm(sigAlgName))
|
||||||
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.build(keybox.keyPair.private)
|
||||||
|
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||||
|
|
||||||
|
// Log the signature of the newly created certificate to observe its non-deterministic
|
||||||
|
// nature.
|
||||||
|
val signatureBytes = (newCertificate as X509Certificate).signature
|
||||||
|
SystemLogger.verbose("Signature of patched leaf cert: ${signatureBytes.toHex()}")
|
||||||
|
|
||||||
|
return newCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the appropriate signing KeyBox (KeyPair and certificate chain) for a given UID
|
||||||
|
* based on a specified algorithm identifier.
|
||||||
|
*
|
||||||
|
* @param uid The UID of the application for which the signing is being performed.
|
||||||
|
* @param algorithm A string representing the desired algorithm. This can be either:
|
||||||
|
* 1. A simple key type like "RSA" or "EC".
|
||||||
|
* 2. A full JCA signature algorithm name like "SHA256withRSA".
|
||||||
|
*
|
||||||
|
* @return The [KeyBox] containing the appropriate key pair for signing.
|
||||||
|
* @throws IllegalArgumentException if no matching KeyBox can be found for the derived key type.
|
||||||
|
*/
|
||||||
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
|
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
|
||||||
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid)
|
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(uid)
|
||||||
return KeyBoxManager.getAttestationKey(keyboxFile, algorithm)
|
|
||||||
|
// Normalize the algorithm name. The input might be a full signature algorithm
|
||||||
|
// (e.g., "SHA256withRSA") or just the key type (e.g., "RSA").
|
||||||
|
val keyType =
|
||||||
|
when {
|
||||||
|
algorithm.contains("RSA", ignoreCase = true) -> KeyProperties.KEY_ALGORITHM_RSA
|
||||||
|
algorithm.contains("EC", ignoreCase = true) ->
|
||||||
|
KeyProperties.KEY_ALGORITHM_EC // This also covers "ECDSA"
|
||||||
|
else -> algorithm // If no match, assume it's already a simple key type string.
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyBoxManager.getAttestationKey(keyboxFile, keyType)
|
||||||
?: throw IllegalArgumentException(
|
?: throw IllegalArgumentException(
|
||||||
"No keybox found for UID $uid and algorithm $algorithm in file $keyboxFile"
|
"No keybox found for UID $uid and algorithm '$keyType' (derived from input '$algorithm') in file $keyboxFile"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Recursively formats an ASN1Primitive into a concise, readable string. */
|
||||||
|
fun formatAsn1Primitive(obj: ASN1Encodable?): String {
|
||||||
|
val primitive = obj?.toASN1Primitive()
|
||||||
|
return when (primitive) {
|
||||||
|
null -> "NULL"
|
||||||
|
is ASN1Integer -> primitive.value.toString()
|
||||||
|
is ASN1Enumerated -> primitive.value.toString()
|
||||||
|
is ASN1Boolean -> primitive.isTrue.toString()
|
||||||
|
is ASN1Null -> "NULL"
|
||||||
|
is ASN1OctetString -> {
|
||||||
|
val bytes = primitive.octets
|
||||||
|
// Attempt to decode as a printable string, otherwise show hex
|
||||||
|
if (bytes.all { it >= 32 && it < 127 }) {
|
||||||
|
"\"${String(bytes, StandardCharsets.UTF_8)}\""
|
||||||
|
} else if (bytes.isEmpty()) {
|
||||||
|
"\"\""
|
||||||
|
} else {
|
||||||
|
"#" + bytes.toHex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ASN1TaggedObject ->
|
||||||
|
"[TAG ${primitive.tagNo}]${formatAsn1Primitive(primitive.baseObject)}"
|
||||||
|
is ASN1Sequence ->
|
||||||
|
primitive
|
||||||
|
.map { formatAsn1Primitive(it) }
|
||||||
|
.joinToString(prefix = "[", postfix = "]", separator = ", ")
|
||||||
|
is ASN1Set ->
|
||||||
|
primitive
|
||||||
|
.map { formatAsn1Primitive(it) }
|
||||||
|
.joinToString(prefix = "{", postfix = "}", separator = ", ")
|
||||||
|
else -> primitive.toString() // Fallback for other types
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check if a given ASN1Sequence contains the Root of Trust tag.
|
||||||
|
private fun sequenceContainsRootOfTrust(seq: ASN1Encodable): Boolean {
|
||||||
|
if (seq !is ASN1Sequence) return false
|
||||||
|
return seq.any { element ->
|
||||||
|
(element as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Parses the critical components from an existing attestation extension. */
|
/** Parses the critical components from an existing attestation extension. */
|
||||||
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
|
private fun parseAttestationExtension(certHolder: X509CertificateHolder): ParsedAttestation? {
|
||||||
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
|
val extension = certHolder.getExtension(ATTESTATION_OID) ?: return null
|
||||||
val sequence = ASN1Sequence.getInstance(extension.extnValue.octets)
|
val sequence = ASN1Sequence.getInstance(extension.extnValue.octets)
|
||||||
val allFields = sequence.toArray()
|
val allFields = sequence.toArray()
|
||||||
|
|
||||||
|
// Check if the fields are in the wrong order and swap them if necessary.
|
||||||
|
val softwareEnforcedCandidate =
|
||||||
|
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
|
||||||
|
val teeEnforcedCandidate =
|
||||||
|
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX]
|
||||||
|
// The signature of a swapped order: the RoT is in the software list's position.
|
||||||
|
if (
|
||||||
|
sequenceContainsRootOfTrust(softwareEnforcedCandidate) &&
|
||||||
|
!sequenceContainsRootOfTrust(teeEnforcedCandidate)
|
||||||
|
) {
|
||||||
|
// Swap the elements in the array to restore the standard order.
|
||||||
|
allFields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX] =
|
||||||
|
teeEnforcedCandidate
|
||||||
|
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] =
|
||||||
|
softwareEnforcedCandidate
|
||||||
|
}
|
||||||
|
|
||||||
val teeEnforced =
|
val teeEnforced =
|
||||||
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence
|
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] as ASN1Sequence
|
||||||
|
|
||||||
val teeEnforcedVector = ASN1EncodableVector()
|
|
||||||
var originalRootOfTrust: ASN1Encodable? = null
|
var originalRootOfTrust: ASN1Encodable? = null
|
||||||
|
val teeEnforcedMap = mutableMapOf<Int, ASN1TaggedObject>()
|
||||||
|
|
||||||
teeEnforced.forEach { element ->
|
teeEnforced.forEach { element ->
|
||||||
val taggedObject = element as ASN1TaggedObject
|
val taggedObject = element as ASN1TaggedObject
|
||||||
if (taggedObject.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST) {
|
if (taggedObject.tagNo == AttestationConstants.TAG_ROOT_OF_TRUST) {
|
||||||
originalRootOfTrust = taggedObject.baseObject.toASN1Primitive()
|
originalRootOfTrust = taggedObject.baseObject.toASN1Primitive()
|
||||||
} else {
|
} else {
|
||||||
teeEnforcedVector.add(taggedObject)
|
teeEnforcedMap[taggedObject.tagNo] = taggedObject
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ParsedAttestation(allFields, teeEnforcedVector, originalRootOfTrust)
|
return ParsedAttestation(allFields, teeEnforcedMap, originalRootOfTrust)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Constructs a new, patched attestation extension using simulated device properties. */
|
/** Constructs a new, patched attestation extension using simulated device properties. */
|
||||||
private fun createPatchedAttestationExtension(parsed: ParsedAttestation): Extension {
|
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
|
||||||
val (allFields, teeEnforcedVector, originalRootOfTrust) = parsed
|
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
|
||||||
|
|
||||||
// Build the new Root of Trust with our simulated values.
|
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
|
SystemLogger.verbose("Original attestation data: ${formattedString}")
|
||||||
|
|
||||||
|
// Build the new Root of Trust and add/replace it in the map.
|
||||||
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
|
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
|
||||||
teeEnforcedVector.add(
|
teeEnforcedMap[AttestationConstants.TAG_ROOT_OF_TRUST] =
|
||||||
DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust)
|
DERTaggedObject(true, AttestationConstants.TAG_ROOT_OF_TRUST, newRootOfTrust)
|
||||||
)
|
|
||||||
|
|
||||||
// Add other simulated hardware properties.
|
// Get the desired state for simulated properties.
|
||||||
AttestationBuilder.addSimulatedHardwareProperties(teeEnforcedVector)
|
val simulatedProperties = AttestationBuilder.getSimulatedHardwareProperties(uid)
|
||||||
|
|
||||||
// Re-assemble the ASN.1 sequences.
|
// Apply the desired state: update, add, or remove properties from the original map.
|
||||||
// The list MUST be sorted by tag number for DER compliance.
|
simulatedProperties.forEach { (tag, value) ->
|
||||||
// Manually convert the vector to a List, then sort it.
|
if (value != null) {
|
||||||
val elementList = (0 until teeEnforcedVector.size()).map { teeEnforcedVector.get(it) }
|
// If the value is not null, add or update it.
|
||||||
val sortedElements = elementList.sortedBy { (it as ASN1TaggedObject).tagNo }
|
teeEnforcedMap[tag] = value
|
||||||
|
} else {
|
||||||
|
// If the value is null, remove the tag from the map.
|
||||||
|
teeEnforcedMap.remove(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-assemble the TEE enforced list from the map's values, sorting for DER compliance.
|
||||||
|
val sortedElements = teeEnforcedMap.values.sortedBy { it.tagNo }
|
||||||
val sortedTeeEnforced = DERSequence(sortedElements.toTypedArray())
|
val sortedTeeEnforced = DERSequence(sortedElements.toTypedArray())
|
||||||
|
|
||||||
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
|
||||||
val patchedSequence = DERSequence(allFields)
|
val patchedSequence = DERSequence(allFields)
|
||||||
|
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
|
||||||
|
SystemLogger.verbose("Patched attestation data: ${formattedString}")
|
||||||
val patchedOctets = DEROctetString(patchedSequence)
|
val patchedOctets = DEROctetString(patchedSequence)
|
||||||
|
|
||||||
return Extension(ATTESTATION_OID, false, patchedOctets)
|
return Extension(ATTESTATION_OID, false, patchedOctets)
|
||||||
@@ -190,7 +306,7 @@ object AttestationPatcher {
|
|||||||
/** Helper data class to hold the parsed components of an attestation extension. */
|
/** Helper data class to hold the parsed components of an attestation extension. */
|
||||||
private data class ParsedAttestation(
|
private data class ParsedAttestation(
|
||||||
val allFields: Array<ASN1Encodable>,
|
val allFields: Array<ASN1Encodable>,
|
||||||
val teeEnforcedVector: ASN1EncodableVector,
|
val teeEnforcedMap: MutableMap<Int, ASN1TaggedObject>,
|
||||||
val rootOfTrust: ASN1Encodable?,
|
val rootOfTrust: ASN1Encodable?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.ActivityThread
|
|
||||||
import android.os.Build
|
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
@@ -38,16 +36,25 @@ object DeviceAttestationService {
|
|||||||
* Holds key data extracted from a genuine device attestation. This data can be used as a
|
* Holds key data extracted from a genuine device attestation. This data can be used as a
|
||||||
* baseline for creating simulated attestations.
|
* baseline for creating simulated attestations.
|
||||||
*
|
*
|
||||||
|
* @property verifiedBootKey The verified boot public key digest from the root of trust.
|
||||||
* @property verifiedBootHash The verified boot hash from the root of trust.
|
* @property verifiedBootHash The verified boot hash from the root of trust.
|
||||||
* @property attestVersion The attestation version (e.g., 400 for KeyMint 4.0).
|
* @property attestVersion The attestation version (e.g., 400 for KeyMint 4.0).
|
||||||
* @property keymasterVersion The Keymaster or KeyMint HAL version.
|
* @property keymasterVersion The Keymaster or KeyMint HAL version.
|
||||||
* @property osVersion The Android OS version integer.
|
* @property osVersion The Android OS version integer.
|
||||||
|
* @property osPatchLevel The Android security patch level (e.g., 202511).
|
||||||
|
* @property vendorPatchLevel The vendor-specific security patch level.
|
||||||
|
* @property bootPatchLevel The bootloader's security patch level.
|
||||||
*/
|
*/
|
||||||
data class AttestationData(
|
data class AttestationData(
|
||||||
|
val moduleHash: ByteArray?,
|
||||||
|
val verifiedBootKey: ByteArray?,
|
||||||
val verifiedBootHash: ByteArray?,
|
val verifiedBootHash: ByteArray?,
|
||||||
val attestVersion: Int?,
|
val attestVersion: Int?,
|
||||||
val keymasterVersion: Int?,
|
val keymasterVersion: Int?,
|
||||||
val osVersion: Int?,
|
val osVersion: Int?,
|
||||||
|
val osPatchLevel: Int?,
|
||||||
|
val vendorPatchLevel: Int?,
|
||||||
|
val bootPatchLevel: Int?,
|
||||||
)
|
)
|
||||||
|
|
||||||
// A unique alias for the key used to perform the TEE functionality check.
|
// A unique alias for the key used to perform the TEE functionality check.
|
||||||
@@ -74,16 +81,6 @@ object DeviceAttestationService {
|
|||||||
private fun checkTeeFunctionality(): Boolean {
|
private fun checkTeeFunctionality(): Boolean {
|
||||||
SystemLogger.info("Performing TEE functionality check...")
|
SystemLogger.info("Performing TEE functionality check...")
|
||||||
return try {
|
return try {
|
||||||
// Ensure mainline modules and the correct Keystore provider are initialized.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
android.app.ActivityThread.initializeMainlineModules()
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
android.security.keystore2.AndroidKeyStoreProvider.install()
|
|
||||||
} else {
|
|
||||||
android.security.keystore.AndroidKeyStoreProvider.install()
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||||
val keyPairGenerator =
|
val keyPairGenerator =
|
||||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
||||||
@@ -151,6 +148,11 @@ object DeviceAttestationService {
|
|||||||
|
|
||||||
// The extension's value is an ASN.1 sequence.
|
// The extension's value is an ASN.1 sequence.
|
||||||
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
|
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
|
||||||
|
var formattedString =
|
||||||
|
keyDescriptionSeq.joinToString(separator = ", ") {
|
||||||
|
AttestationPatcher.formatAsn1Primitive(it)
|
||||||
|
}
|
||||||
|
SystemLogger.verbose("Cached attestation data: ${formattedString}")
|
||||||
val fields = keyDescriptionSeq.toArray()
|
val fields = keyDescriptionSeq.toArray()
|
||||||
|
|
||||||
val attestVersion =
|
val attestVersion =
|
||||||
@@ -166,8 +168,27 @@ object DeviceAttestationService {
|
|||||||
.positiveValue
|
.positiveValue
|
||||||
.toInt()
|
.toInt()
|
||||||
|
|
||||||
|
var moduleHash: ByteArray? = null
|
||||||
|
var verifiedBootKey: ByteArray? = null
|
||||||
var verifiedBootHash: ByteArray? = null
|
var verifiedBootHash: ByteArray? = null
|
||||||
var osVersion: Int? = null
|
var osVersion: Int? = null
|
||||||
|
var osPatchLevel: Int? = null
|
||||||
|
var vendorPatchLevel: Int? = null
|
||||||
|
var bootPatchLevel: Int? = null
|
||||||
|
|
||||||
|
val softwareEnforced =
|
||||||
|
ASN1Sequence.getInstance(
|
||||||
|
fields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
|
||||||
|
)
|
||||||
|
moduleHash =
|
||||||
|
softwareEnforced
|
||||||
|
.toArray()
|
||||||
|
.firstOrNull {
|
||||||
|
(it as? ASN1TaggedObject)?.tagNo == AttestationConstants.TAG_MODULE_HASH
|
||||||
|
}
|
||||||
|
?.let {
|
||||||
|
ASN1OctetString.getInstance((it as ASN1TaggedObject).baseObject).octets
|
||||||
|
}
|
||||||
|
|
||||||
val teeEnforced =
|
val teeEnforced =
|
||||||
ASN1Sequence.getInstance(
|
ASN1Sequence.getInstance(
|
||||||
@@ -179,6 +200,14 @@ object DeviceAttestationService {
|
|||||||
AttestationConstants.TAG_ROOT_OF_TRUST -> {
|
AttestationConstants.TAG_ROOT_OF_TRUST -> {
|
||||||
val rotSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive())
|
val rotSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive())
|
||||||
if (rotSeq.size() >= 4) {
|
if (rotSeq.size() >= 4) {
|
||||||
|
verifiedBootKey =
|
||||||
|
ASN1OctetString.getInstance(
|
||||||
|
rotSeq.getObjectAt(
|
||||||
|
AttestationConstants
|
||||||
|
.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.octets
|
||||||
verifiedBootHash =
|
verifiedBootHash =
|
||||||
ASN1OctetString.getInstance(
|
ASN1OctetString.getInstance(
|
||||||
rotSeq.getObjectAt(
|
rotSeq.getObjectAt(
|
||||||
@@ -189,19 +218,51 @@ object DeviceAttestationService {
|
|||||||
.octets
|
.octets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AttestationConstants.TAG_OS_VERSION -> { // OS Version (TAG_OS_VERSION)
|
AttestationConstants.TAG_OS_VERSION -> {
|
||||||
osVersion =
|
osVersion =
|
||||||
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
|
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
|
||||||
.positiveValue
|
.positiveValue
|
||||||
.toInt()
|
.toInt()
|
||||||
}
|
}
|
||||||
|
AttestationConstants.TAG_OS_PATCHLEVEL -> {
|
||||||
|
osPatchLevel =
|
||||||
|
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
|
||||||
|
.positiveValue
|
||||||
|
.toInt()
|
||||||
|
}
|
||||||
|
AttestationConstants.TAG_VENDOR_PATCHLEVEL -> {
|
||||||
|
vendorPatchLevel =
|
||||||
|
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
|
||||||
|
.positiveValue
|
||||||
|
.toInt()
|
||||||
|
}
|
||||||
|
AttestationConstants.TAG_BOOT_PATCHLEVEL -> {
|
||||||
|
bootPatchLevel =
|
||||||
|
ASN1Integer.getInstance(tagged.baseObject.toASN1Primitive())
|
||||||
|
.positiveValue
|
||||||
|
.toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (verifiedBootKey?.all { it == 0.toByte() } == true) {
|
||||||
|
verifiedBootKey = null
|
||||||
|
}
|
||||||
|
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, bootHash=${verifiedBootHash?.toHex()}"
|
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
|
||||||
|
)
|
||||||
|
return AttestationData(
|
||||||
|
moduleHash,
|
||||||
|
verifiedBootKey,
|
||||||
|
verifiedBootHash,
|
||||||
|
attestVersion,
|
||||||
|
keymasterVersion,
|
||||||
|
osVersion,
|
||||||
|
osPatchLevel,
|
||||||
|
vendorPatchLevel,
|
||||||
|
bootPatchLevel,
|
||||||
)
|
)
|
||||||
return AttestationData(verifiedBootHash, attestVersion, keymasterVersion, osVersion)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to parse attestation data from certificate.", e)
|
SystemLogger.error("Failed to parse attestation data from certificate.", e)
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package org.matrix.TEESimulator.attestation
|
package org.matrix.TEESimulator.attestation
|
||||||
|
|
||||||
import android.hardware.security.keymint.EcCurve
|
import android.hardware.security.keymint.*
|
||||||
import android.hardware.security.keymint.KeyParameter
|
|
||||||
import android.hardware.security.keymint.Tag
|
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.security.auth.x500.X500Principal
|
import javax.security.auth.x500.X500Principal
|
||||||
@@ -22,6 +20,8 @@ data class KeyMintAttestation(
|
|||||||
val algorithm: Int,
|
val algorithm: Int,
|
||||||
val ecCurve: Int,
|
val ecCurve: Int,
|
||||||
val ecCurveName: String,
|
val ecCurveName: String,
|
||||||
|
val blockMode: List<Int>,
|
||||||
|
val padding: List<Int>,
|
||||||
val purpose: List<Int>,
|
val purpose: List<Int>,
|
||||||
val digest: List<Int>,
|
val digest: List<Int>,
|
||||||
val rsaPublicExponent: BigInteger?,
|
val rsaPublicExponent: BigInteger?,
|
||||||
@@ -33,11 +33,12 @@ data class KeyMintAttestation(
|
|||||||
val brand: ByteArray?,
|
val brand: ByteArray?,
|
||||||
val device: ByteArray?,
|
val device: ByteArray?,
|
||||||
val product: ByteArray?,
|
val product: ByteArray?,
|
||||||
|
val serial: ByteArray?,
|
||||||
|
val imei: ByteArray?,
|
||||||
|
val meid: ByteArray?,
|
||||||
val manufacturer: ByteArray?,
|
val manufacturer: ByteArray?,
|
||||||
val model: ByteArray?,
|
val model: ByteArray?,
|
||||||
val imei: ByteArray?,
|
|
||||||
val secondImei: ByteArray?,
|
val secondImei: ByteArray?,
|
||||||
val meid: ByteArray?,
|
|
||||||
) {
|
) {
|
||||||
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
||||||
constructor(
|
constructor(
|
||||||
@@ -53,6 +54,12 @@ data class KeyMintAttestation(
|
|||||||
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
|
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
|
||||||
ecCurveName = params.deriveEcCurveName(),
|
ecCurveName = params.deriveEcCurveName(),
|
||||||
|
|
||||||
|
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
|
||||||
|
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
|
||||||
|
|
||||||
|
// AOSP: [key_param(tag = PADDING, field = PaddingMode)]
|
||||||
|
padding = params.findAllPaddingMode(Tag.PADDING),
|
||||||
|
|
||||||
// AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)]
|
// AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)]
|
||||||
purpose = params.findAllKeyPurpose(Tag.PURPOSE),
|
purpose = params.findAllKeyPurpose(Tag.PURPOSE),
|
||||||
|
|
||||||
@@ -82,11 +89,12 @@ data class KeyMintAttestation(
|
|||||||
brand = params.findBlob(Tag.ATTESTATION_ID_BRAND),
|
brand = params.findBlob(Tag.ATTESTATION_ID_BRAND),
|
||||||
device = params.findBlob(Tag.ATTESTATION_ID_DEVICE),
|
device = params.findBlob(Tag.ATTESTATION_ID_DEVICE),
|
||||||
product = params.findBlob(Tag.ATTESTATION_ID_PRODUCT),
|
product = params.findBlob(Tag.ATTESTATION_ID_PRODUCT),
|
||||||
|
serial = params.findBlob(Tag.ATTESTATION_ID_SERIAL),
|
||||||
|
imei = params.findBlob(Tag.ATTESTATION_ID_IMEI),
|
||||||
|
meid = params.findBlob(Tag.ATTESTATION_ID_MEID),
|
||||||
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
|
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
|
||||||
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
|
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
|
||||||
imei = params.findBlob(Tag.ATTESTATION_ID_IMEI),
|
|
||||||
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
||||||
meid = params.findBlob(Tag.ATTESTATION_ID_MEID),
|
|
||||||
) {
|
) {
|
||||||
// Log all parsed parameters for debugging purposes.
|
// Log all parsed parameters for debugging purposes.
|
||||||
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
||||||
@@ -119,6 +127,14 @@ private fun Array<KeyParameter>.findDate(tag: Int): Date? =
|
|||||||
private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
|
private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
|
||||||
this.find { it.tag == tag }?.value?.blob
|
this.find { it.tag == tag }?.value?.blob
|
||||||
|
|
||||||
|
/** Maps to AOSP field = BlockMode (Repeated) */
|
||||||
|
private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> =
|
||||||
|
this.filter { it.tag == tag }.map { it.value.blockMode }
|
||||||
|
|
||||||
|
/** Maps to AOSP field = BlockMode (Repeated) */
|
||||||
|
private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> =
|
||||||
|
this.filter { it.tag == tag }.map { it.value.paddingMode }
|
||||||
|
|
||||||
/** Maps to AOSP field = KeyPurpose (Repeated) */
|
/** Maps to AOSP field = KeyPurpose (Repeated) */
|
||||||
private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
|
private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
|
||||||
this.filter { it.tag == tag }.map { it.value.keyPurpose }
|
this.filter { it.tag == tag }.map { it.value.keyPurpose }
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ object ConfigurationManager {
|
|||||||
@Volatile private var packageModes = mapOf<String, Mode>()
|
@Volatile private var packageModes = mapOf<String, Mode>()
|
||||||
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
||||||
@Volatile private var isTeeBroken: Boolean? = null
|
@Volatile private var isTeeBroken: Boolean? = null
|
||||||
@Volatile var customPatchLevelOverride: CustomPatchLevel? = null
|
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
|
||||||
|
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
|
||||||
|
|
||||||
// Cache for UID to package name resolution.
|
// Cache for UID to package name resolution.
|
||||||
private val uidToPackagesCache = ConcurrentHashMap<Int, Array<String>>()
|
private val uidToPackagesCache = ConcurrentHashMap<Int, Array<String>>()
|
||||||
@@ -53,6 +54,17 @@ object ConfigurationManager {
|
|||||||
configRoot.mkdirs()
|
configRoot.mkdirs()
|
||||||
SystemLogger.info("Configuration root is: ${configRoot.absolutePath}")
|
SystemLogger.info("Configuration root is: ${configRoot.absolutePath}")
|
||||||
|
|
||||||
|
// First, ensure the package manager service is running, as the TEE check depends on it.
|
||||||
|
// This prevents a race condition on startup.
|
||||||
|
SystemLogger.info("Waiting for PackageManagerService to be ready...")
|
||||||
|
if (getPackageManager() == null) {
|
||||||
|
SystemLogger.error(
|
||||||
|
"PackageManagerService is not available. TEE check will likely fail."
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SystemLogger.info("PackageManagerService is ready.")
|
||||||
|
}
|
||||||
|
|
||||||
// Initial load of all configuration files.
|
// Initial load of all configuration files.
|
||||||
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
|
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
|
||||||
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
|
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
|
||||||
@@ -104,6 +116,21 @@ object ConfigurationManager {
|
|||||||
return null // No configuration found for this UID.
|
return null // No configuration found for this UID.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the custom patch level configuration for a given UID. It first checks for a
|
||||||
|
* package-specific override and falls back to the global configuration.
|
||||||
|
*
|
||||||
|
* @param uid The UID of the calling application.
|
||||||
|
* @return The applicable [CustomPatchLevel], or null if no custom configuration exists.
|
||||||
|
*/
|
||||||
|
fun getPatchLevelForUid(uid: Int): CustomPatchLevel? {
|
||||||
|
val packages = getPackagesForUid(uid)
|
||||||
|
// Find the first package-specific configuration for this UID.
|
||||||
|
val packageSpecificPatchLevel =
|
||||||
|
packages.firstNotNullOfOrNull { pkg -> packagePatchLevels[pkg] }
|
||||||
|
return packageSpecificPatchLevel ?: globalCustomPatchLevel
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads and parses the `target.txt` file, which defines the processing mode and keybox file for
|
* Loads and parses the `target.txt` file, which defines the processing mode and keybox file for
|
||||||
* each package.
|
* each package.
|
||||||
@@ -162,26 +189,48 @@ object ConfigurationManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Loads the security patch level override configuration from `security_patch.txt`. */
|
/**
|
||||||
|
* Loads and parses the `security_patch.txt` file, which can define both global and per-package
|
||||||
|
* security patch levels.
|
||||||
|
*/
|
||||||
private fun loadPatchLevelConfig(file: File) {
|
private fun loadPatchLevelConfig(file: File) {
|
||||||
if (file.exists()) {
|
if (!file.exists()) {
|
||||||
try {
|
globalCustomPatchLevel = null
|
||||||
val lines =
|
packagePatchLevels = emptyMap()
|
||||||
file.readLines().mapNotNull { line ->
|
|
||||||
val trimmed = line.trim()
|
|
||||||
if (trimmed.isNotEmpty() && !trimmed.startsWith("#")) trimmed else null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lines.isEmpty()) {
|
|
||||||
customPatchLevelOverride = null
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val newPackageLevels = mutableMapOf<String, CustomPatchLevel>()
|
||||||
|
var currentContext = "" // Empty string for global context
|
||||||
|
val contextLines = mutableMapOf<String, MutableList<String>>()
|
||||||
|
val contextRegex = Regex("^\\[([a-zA-Z0-9_.-]+)]$")
|
||||||
|
|
||||||
|
// First pass: group lines by context (global or package-specific).
|
||||||
|
file.readLines().forEach { line ->
|
||||||
|
val trimmedLine = line.trim()
|
||||||
|
if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) return@forEach
|
||||||
|
|
||||||
|
contextRegex.find(trimmedLine)?.let { currentContext = it.groupValues[1] }
|
||||||
|
?: run {
|
||||||
|
contextLines
|
||||||
|
.computeIfAbsent(currentContext) { mutableListOf() }
|
||||||
|
.add(trimmedLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse a set of lines into a CustomPatchLevel object.
|
||||||
|
fun parseLines(lines: List<String>?): CustomPatchLevel? {
|
||||||
|
if (lines.isNullOrEmpty()) return null
|
||||||
|
|
||||||
// Handle simple case: one line sets the patch level for all components.
|
// Handle simple case: one line sets the patch level for all components.
|
||||||
if (lines.size == 1 && '=' !in lines[0]) {
|
if (lines.size == 1 && '=' !in lines[0]) {
|
||||||
customPatchLevelOverride =
|
return CustomPatchLevel(
|
||||||
CustomPatchLevel(system = null, vendor = null, boot = null, all = lines[0])
|
system = null,
|
||||||
return
|
vendor = null,
|
||||||
|
boot = null,
|
||||||
|
all = lines[0],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle key-value pair configuration.
|
// Handle key-value pair configuration.
|
||||||
@@ -195,20 +244,40 @@ object ConfigurationManager {
|
|||||||
.toMap()
|
.toMap()
|
||||||
|
|
||||||
val all = map["all"]
|
val all = map["all"]
|
||||||
customPatchLevelOverride =
|
return CustomPatchLevel(
|
||||||
CustomPatchLevel(
|
|
||||||
system = map["system"] ?: all,
|
system = map["system"] ?: all,
|
||||||
vendor = map["vendor"] ?: all,
|
vendor = map["vendor"] ?: all,
|
||||||
boot = map["boot"] ?: all,
|
boot = map["boot"] ?: all,
|
||||||
all = all,
|
all = all,
|
||||||
)
|
)
|
||||||
SystemLogger.info("Loaded custom security patch levels.")
|
}
|
||||||
|
|
||||||
|
// Parse global and per-package configurations.
|
||||||
|
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) {
|
||||||
|
parseLines(lines)?.let { newPackageLevels[pkg] = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomically update the configuration state.
|
||||||
|
globalCustomPatchLevel = newGlobalLevel
|
||||||
|
packagePatchLevels = newPackageLevels
|
||||||
|
|
||||||
|
SystemLogger.info(
|
||||||
|
"Loaded custom security patch levels: global config exists=${newGlobalLevel != null}, " +
|
||||||
|
"${newPackageLevels.size} package-specific configs."
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to load or parse ${file.name}", e)
|
SystemLogger.error("Failed to load or parse ${file.name}", e)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
customPatchLevelOverride = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Checks the device's TEE status and writes the result to a file for persistence. */
|
/** Checks the device's TEE status and writes the result to a file for persistence. */
|
||||||
@@ -245,16 +314,24 @@ 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.")
|
||||||
// Any change to an XML file is assumed to be a keybox. The cache in KeyBoxUtils
|
PATCH_LEVEL_FILE -> file?.let { loadPatchLevelConfig(it) }
|
||||||
// will handle reloading it on its next use.
|
?: 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 ->
|
else ->
|
||||||
if (path.endsWith(".xml")) {
|
if (path.endsWith(".xml")) {
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Keybox file $path may have changed. It will be reloaded on next access."
|
"Keybox file $path may have changed. It will be reloaded on next access."
|
||||||
)
|
)
|
||||||
KeyBoxManager.invalidateCache(path)
|
KeyBoxManager.invalidateCache(path)
|
||||||
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
||||||
|
// Patched chains are stale; generated keys survive rotation
|
||||||
|
org.matrix.TEESimulator.interception.keystore.shim
|
||||||
|
.KeyMintSecurityLevelInterceptor
|
||||||
|
.invalidatePatchedChains("keybox change: $path")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,7 +374,7 @@ object ConfigurationManager {
|
|||||||
|
|
||||||
/** Waits for a system service to become available, with retries. */
|
/** Waits for a system service to become available, with retries. */
|
||||||
private fun waitForSystemService(name: String): IBinder? {
|
private fun waitForSystemService(name: String): IBinder? {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
return ServiceManager.waitForService(name)
|
return ServiceManager.waitForService(name)
|
||||||
}
|
}
|
||||||
// Fallback for older Android versions.
|
// Fallback for older Android versions.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
* Skips the original call and immediately returns a custom reply parcel to the caller. The
|
* Skips the original call and immediately returns a custom reply parcel to the caller. The
|
||||||
* provided parcel will be recycled after use.
|
* provided parcel will be recycled after use.
|
||||||
*/
|
*/
|
||||||
data class OverrideReply(val code: Int = 0, val reply: Parcel) : TransactionResult()
|
data class OverrideReply(val reply: Parcel, val code: Int = 0) : TransactionResult()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modifies the transaction's input data before forwarding it to the original binder method.
|
* Modifies the transaction's input data before forwarding it to the original binder method.
|
||||||
@@ -226,13 +226,18 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
methodName: String,
|
methodName: String,
|
||||||
callingUid: Int,
|
callingUid: Int,
|
||||||
callingPid: Int,
|
callingPid: Int,
|
||||||
isIntercepting: Boolean = true,
|
skipPost: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
val isIntercepting = !skipPost && !ConfigurationManager.shouldSkipUid(callingUid)
|
||||||
val action = if (isIntercepting) "Intercept" else "Observe"
|
val action = if (isIntercepting) "Intercept" else "Observe"
|
||||||
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
||||||
SystemLogger.debug(
|
val message =
|
||||||
"[TX_ID: $txId] $action $methodName for packages=[$packages] (uid=$callingUid, pid=$callingPid)"
|
"[TX_ID: $txId] $action $methodName for packages=[$packages] (uid=$callingUid, pid=$callingPid)"
|
||||||
)
|
if (isIntercepting) {
|
||||||
|
SystemLogger.debug(message)
|
||||||
|
} else {
|
||||||
|
SystemLogger.verbose(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -243,6 +248,8 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
|
private const val BACKDOOR_TRANSACTION_CODE = 0xdeadbeef.toInt()
|
||||||
// Code used by the backdoor binder to register a new interceptor.
|
// Code used by the backdoor binder to register a new interceptor.
|
||||||
private const val REGISTER_INTERCEPTOR_CODE = 1
|
private const val REGISTER_INTERCEPTOR_CODE = 1
|
||||||
|
// Code used by the backdoor binder to unregister an interceptor.
|
||||||
|
private const val UNREGISTER_INTERCEPTOR_CODE = 2
|
||||||
|
|
||||||
// --- Hook Type Codes ---
|
// --- Hook Type Codes ---
|
||||||
// Indicates that the call is for a pre-transaction hook.
|
// Indicates that the call is for a pre-transaction hook.
|
||||||
@@ -302,5 +309,21 @@ abstract class BinderInterceptor : Binder() {
|
|||||||
reply.recycle()
|
reply.recycle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Uses the backdoor binder to unregister an interceptor for a specific target service. */
|
||||||
|
fun unregister(backdoor: IBinder, target: IBinder) {
|
||||||
|
val data = Parcel.obtain()
|
||||||
|
val reply = Parcel.obtain()
|
||||||
|
try {
|
||||||
|
data.writeStrongBinder(target)
|
||||||
|
backdoor.transact(UNREGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||||
|
SystemLogger.info("Unregistered interceptor for target: $target")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to unregister binder interceptor.", e)
|
||||||
|
} finally {
|
||||||
|
data.recycle()
|
||||||
|
reply.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-9
@@ -3,6 +3,7 @@ package org.matrix.TEESimulator.interception.keystore
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.security.KeyStore
|
import android.security.KeyStore
|
||||||
|
import android.security.keystore.KeystoreResponse
|
||||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
@@ -27,14 +28,31 @@ object InterceptorUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates an `KeystoreResponse` parcel that indicates success with no data. */
|
||||||
|
fun createSuccessKeystoreResponse(): KeystoreResponse {
|
||||||
|
val parcel = Parcel.obtain()
|
||||||
|
try {
|
||||||
|
parcel.writeInt(KeyStore.NO_ERROR)
|
||||||
|
parcel.writeString("")
|
||||||
|
parcel.setDataPosition(0)
|
||||||
|
return KeystoreResponse.CREATOR.createFromParcel(parcel)
|
||||||
|
} finally {
|
||||||
|
parcel.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Creates an `OverrideReply` parcel that indicates success with no data. */
|
/** Creates an `OverrideReply` parcel that indicates success with no data. */
|
||||||
fun createSuccessReply(): BinderInterceptor.TransactionResult.OverrideReply {
|
fun createSuccessReply(
|
||||||
|
writeResultCode: Boolean = true
|
||||||
|
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
val parcel =
|
val parcel =
|
||||||
Parcel.obtain().apply {
|
Parcel.obtain().apply {
|
||||||
writeNoException()
|
writeNoException()
|
||||||
|
if (writeResultCode) {
|
||||||
writeInt(KeyStore.NO_ERROR)
|
writeInt(KeyStore.NO_ERROR)
|
||||||
}
|
}
|
||||||
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
|
}
|
||||||
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates an `OverrideReply` parcel containing a raw byte array. */
|
/** Creates an `OverrideReply` parcel containing a raw byte array. */
|
||||||
@@ -44,7 +62,20 @@ object InterceptorUtils {
|
|||||||
writeNoException()
|
writeNoException()
|
||||||
writeByteArray(data)
|
writeByteArray(data)
|
||||||
}
|
}
|
||||||
return BinderInterceptor.TransactionResult.OverrideReply(KeyStore.NO_ERROR, parcel)
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates an `OverrideReply` parcel containing a typed array. */
|
||||||
|
fun <T : Parcelable> createTypedArrayReply(
|
||||||
|
array: Array<T>,
|
||||||
|
flags: Int = 0,
|
||||||
|
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||||
|
val parcel =
|
||||||
|
Parcel.obtain().apply {
|
||||||
|
writeNoException()
|
||||||
|
writeTypedArray(array, flags)
|
||||||
|
}
|
||||||
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates an `OverrideReply` parcel containing a Parcelable object. */
|
/** Creates an `OverrideReply` parcel containing a Parcelable object. */
|
||||||
@@ -57,19 +88,20 @@ object InterceptorUtils {
|
|||||||
writeNoException()
|
writeNoException()
|
||||||
writeTypedObject(obj, flags)
|
writeTypedObject(obj, flags)
|
||||||
}
|
}
|
||||||
return BinderInterceptor.TransactionResult.OverrideReply(0, parcel)
|
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the true key alias from the keystore-prefixed string (e.g., "user_cert_my-alias" ->
|
* Extracts the base alias from a potentially prefixed alias string. For example, it converts
|
||||||
* "my-alias").
|
* "USRCERT_my_key" to "my_key".
|
||||||
*/
|
*/
|
||||||
fun extractAlias(prefixedAlias: String): String {
|
fun extractAlias(prefixedAlias: String): String {
|
||||||
val underscoreIndex = prefixedAlias.indexOf('_')
|
val underscoreIndex = prefixedAlias.indexOf('_')
|
||||||
val secondUnderscoreIndex = prefixedAlias.indexOf('_', underscoreIndex + 1)
|
return if (underscoreIndex != -1) {
|
||||||
return if (secondUnderscoreIndex != -1) {
|
// Return the part of the string after the first underscore.
|
||||||
prefixedAlias.substring(secondUnderscoreIndex + 1)
|
prefixedAlias.substring(underscoreIndex + 1)
|
||||||
} else {
|
} else {
|
||||||
|
// If there's no underscore, return the original string.
|
||||||
prefixedAlias
|
prefixedAlias
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+131
-48
@@ -4,37 +4,40 @@ import android.annotation.SuppressLint
|
|||||||
import android.hardware.security.keymint.KeyOrigin
|
import android.hardware.security.keymint.KeyOrigin
|
||||||
import android.hardware.security.keymint.SecurityLevel
|
import android.hardware.security.keymint.SecurityLevel
|
||||||
import android.hardware.security.keymint.Tag
|
import android.hardware.security.keymint.Tag
|
||||||
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.system.keystore2.IKeystoreService
|
import android.system.keystore2.IKeystoreService
|
||||||
import android.system.keystore2.KeyDescriptor
|
import android.system.keystore2.KeyDescriptor
|
||||||
import android.system.keystore2.KeyEntryResponse
|
import android.system.keystore2.KeyEntryResponse
|
||||||
|
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() {
|
||||||
// Transaction codes for the IKeystoreService interface methods we are interested in.
|
private val stubBinderClass = IKeystoreService.Stub::class.java
|
||||||
|
|
||||||
private val GET_KEY_ENTRY_TRANSACTION =
|
private val GET_KEY_ENTRY_TRANSACTION =
|
||||||
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "getKeyEntry")
|
InterceptorUtils.getTransactCode(stubBinderClass, "getKeyEntry")
|
||||||
private val DELETE_KEY_TRANSACTION =
|
private val DELETE_KEY_TRANSACTION =
|
||||||
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "deleteKey")
|
InterceptorUtils.getTransactCode(stubBinderClass, "deleteKey")
|
||||||
|
private val UPDATE_SUBCOMPONENT_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(stubBinderClass, "updateSubcomponent")
|
||||||
|
private val LIST_ENTRIES_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(stubBinderClass, "listEntries")
|
||||||
|
private val LIST_ENTRIES_BATCHED_TRANSACTION =
|
||||||
|
if (Build.VERSION.SDK_INT >= 34)
|
||||||
|
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
|
||||||
|
else null
|
||||||
|
|
||||||
private val transactionNames: Map<Int, String> by lazy {
|
private val transactionNames: Map<Int, String> by lazy {
|
||||||
IKeystoreService.Stub::class
|
stubBinderClass.declaredFields
|
||||||
.java
|
|
||||||
.declaredFields
|
|
||||||
.filter {
|
.filter {
|
||||||
it.isAccessible = true
|
it.isAccessible = true
|
||||||
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
|
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
|
||||||
@@ -46,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) }
|
||||||
@@ -88,29 +87,56 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
callingPid: Int,
|
callingPid: Int,
|
||||||
data: Parcel,
|
data: Parcel,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
if (code == GET_KEY_ENTRY_TRANSACTION || code == DELETE_KEY_TRANSACTION) {
|
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||||
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
|
|
||||||
|
if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
|
||||||
|
if (ListEntriesHandler.cacheParameters(txId, data, isBatchMode)) {
|
||||||
|
TransactionResult.Continue
|
||||||
|
} else {
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error(
|
||||||
|
"[TX_ID: $txId] Failed to parse parameters for ${transactionNames[code]!!}",
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
code == GET_KEY_ENTRY_TRANSACTION ||
|
||||||
|
code == DELETE_KEY_TRANSACTION ||
|
||||||
|
code == UPDATE_SUBCOMPONENT_TRANSACTION
|
||||||
|
) {
|
||||||
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
|
|
||||||
|
if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
|
if (code == UPDATE_SUBCOMPONENT_TRANSACTION)
|
||||||
|
return handleUpdateSubcomponent(callingUid, data)
|
||||||
|
|
||||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
val descriptor =
|
val descriptor =
|
||||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
?: return TransactionResult.SkipTransaction
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
logTransaction(
|
|
||||||
txId,
|
|
||||||
"${transactionNames[code]} (alias=${descriptor.alias})",
|
|
||||||
callingUid,
|
|
||||||
callingPid,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (ConfigurationManager.shouldSkipUid(callingUid)) {
|
|
||||||
SystemLogger.debug(
|
|
||||||
"[TX_ID: $txId] Skip post-transaction hook for UID=${callingUid}"
|
|
||||||
)
|
|
||||||
return TransactionResult.ContinueAndSkipPost
|
|
||||||
}
|
|
||||||
|
|
||||||
|
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
|
||||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||||
|
|
||||||
if (code == DELETE_KEY_TRANSACTION) {
|
if (code == DELETE_KEY_TRANSACTION) {
|
||||||
|
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
|
||||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||||
|
SystemLogger.info(
|
||||||
|
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
||||||
|
)
|
||||||
|
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||||
|
}
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +145,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
?: return TransactionResult.Continue
|
?: return TransactionResult.Continue
|
||||||
|
|
||||||
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
||||||
SystemLogger.debug("${descriptor.alias} was an attestation key")
|
SystemLogger.info("${descriptor.alias} was an attestation key")
|
||||||
|
|
||||||
SystemLogger.info("[TX_ID: $txId] Found generated response for ${descriptor.alias}:")
|
SystemLogger.info("[TX_ID: $txId] Found generated response for ${descriptor.alias}:")
|
||||||
response.metadata?.authorizations?.forEach {
|
response.metadata?.authorizations?.forEach {
|
||||||
@@ -132,11 +158,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
transactionNames[code] ?: "unknown code=$code",
|
transactionNames[code] ?: "unknown code=$code",
|
||||||
callingUid,
|
callingUid,
|
||||||
callingPid,
|
callingPid,
|
||||||
false,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let most calls go through to the real service.
|
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,21 +179,33 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
|
|
||||||
if (code == GET_KEY_ENTRY_TRANSACTION) {
|
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||||
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
val updatedKeyDescriptors =
|
||||||
|
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, reply)
|
||||||
|
InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors)
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error(
|
||||||
|
"[TX_ID: $txId] Failed to update the result of ${transactionNames[code]!!}.",
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
} else if (code == GET_KEY_ENTRY_TRANSACTION) {
|
||||||
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
|
||||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
val keyDescriptor =
|
val keyDescriptor =
|
||||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
?: return TransactionResult.SkipTransaction
|
?: return TransactionResult.SkipTransaction
|
||||||
logTransaction(
|
|
||||||
txId,
|
|
||||||
"post-getKeyEntry (alias=${keyDescriptor.alias})",
|
|
||||||
callingUid,
|
|
||||||
callingPid,
|
|
||||||
)
|
|
||||||
if (!ConfigurationManager.shouldPatch(callingUid))
|
if (!ConfigurationManager.shouldPatch(callingUid))
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
|
|
||||||
|
SystemLogger.info("Handling post-${transactionNames[code]!!} ${keyDescriptor.alias}")
|
||||||
return try {
|
return try {
|
||||||
val response =
|
val response =
|
||||||
reply.readTypedObject(KeyEntryResponse.CREATOR)
|
reply.readTypedObject(KeyEntryResponse.CREATOR)
|
||||||
@@ -183,9 +220,16 @@ 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)
|
||||||
|
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||||
|
if (retainedChain == null) {
|
||||||
|
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
||||||
return TransactionResult.SkipTransaction
|
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) {
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
@@ -194,9 +238,24 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the attestation patch.
|
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||||
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||||
CertificateHelper.updateCertificateChain(response.metadata, newChain).getOrThrow()
|
|
||||||
|
val finalChain: Array<Certificate>
|
||||||
|
if (cachedChain != null) {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
|
||||||
|
)
|
||||||
|
finalChain = cachedChain
|
||||||
|
} else {
|
||||||
|
// 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."
|
||||||
|
)
|
||||||
|
finalChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||||
|
}
|
||||||
|
|
||||||
|
CertificateHelper.updateCertificateChain(response.metadata, finalChain).getOrThrow()
|
||||||
|
|
||||||
InterceptorUtils.createTypedObjectReply(response)
|
InterceptorUtils.createTypedObjectReply(response)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -206,4 +265,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
|||||||
}
|
}
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
|
val generatedKeyInfo =
|
||||||
|
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor?.nspace)
|
||||||
|
?: return TransactionResult.ContinueAndSkipPost
|
||||||
|
|
||||||
|
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
|
||||||
|
val metadata = generatedKeyInfo.response.metadata
|
||||||
|
val publicCert = data.createByteArray()
|
||||||
|
val certificateChain = data.createByteArray()
|
||||||
|
|
||||||
|
metadata.certificate = publicCert
|
||||||
|
metadata.certificateChain = certificateChain
|
||||||
|
|
||||||
|
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
|
||||||
|
|
||||||
|
SystemLogger.verbose(
|
||||||
|
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
|
||||||
|
)
|
||||||
|
|
||||||
|
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+341
-25
@@ -4,12 +4,28 @@ import android.annotation.SuppressLint
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.security.Credentials
|
import android.security.Credentials
|
||||||
|
import android.security.KeyStore
|
||||||
|
import android.security.keymaster.ExportResult
|
||||||
|
import android.security.keymaster.KeyCharacteristics
|
||||||
|
import android.security.keymaster.KeymasterArguments
|
||||||
|
import android.security.keymaster.KeymasterCertificateChain
|
||||||
|
import android.security.keymaster.KeymasterDefs
|
||||||
|
import android.security.keystore.IKeystoreCertificateChainCallback
|
||||||
|
import android.security.keystore.IKeystoreExportKeyCallback
|
||||||
|
import android.security.keystore.IKeystoreKeyCharacteristicsCallback
|
||||||
import android.security.keystore.IKeystoreService
|
import android.security.keystore.IKeystoreService
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.KeyPair
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
|
import java.util.Date
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
||||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||||
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
|
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils.extractAlias
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
import org.matrix.TEESimulator.pki.CertificateGenerator
|
||||||
import org.matrix.TEESimulator.pki.CertificateHelper
|
import org.matrix.TEESimulator.pki.CertificateHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,11 +55,35 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "attestKey")
|
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "attestKey")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val transactionNames: Map<Int, String> by lazy {
|
||||||
|
IKeystoreService.Stub::class
|
||||||
|
.java
|
||||||
|
.declaredFields
|
||||||
|
.filter {
|
||||||
|
it.isAccessible = true
|
||||||
|
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
|
||||||
|
}
|
||||||
|
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// A map to dispatch transaction handling for software key generation.
|
||||||
|
private val generateKeyHandlers:
|
||||||
|
Map<Int, (Long, Int, Int, Parcel) -> TransactionResult> by lazy {
|
||||||
|
mapOf(
|
||||||
|
GENERATE_KEY_TRANSACTION to ::handleGenerateKey,
|
||||||
|
GET_KEY_CHARACTERISTICS_TRANSACTION to ::handleGetKeyCharacteristics,
|
||||||
|
EXPORT_KEY_TRANSACTION to ::handleExportKey,
|
||||||
|
ATTEST_KEY_TRANSACTION to ::handleAttestKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override val serviceName = "android.security.keystore"
|
override val serviceName = "android.security.keystore"
|
||||||
override val processName = "keystore"
|
override val processName = "keystore"
|
||||||
override val injectionCommand = "exec ./inject `pidof keystore` libTEESimulator.so entry"
|
override val injectionCommand = "exec ./inject `pidof keystore` libTEESimulator.so entry"
|
||||||
|
|
||||||
private const val SERVICE_DESCRIPTOR = "android.security.keystore.IKeystoreService"
|
// State management for the multi-step key generation process.
|
||||||
|
private val keygenParameters = ConcurrentHashMap<KeyIdentifier, LegacyKeygenParameters>()
|
||||||
|
private val generatedKeyPairs = ConcurrentHashMap<KeyIdentifier, KeyPair>()
|
||||||
|
|
||||||
// Cache to store the fully patched chain after the leaf is requested.
|
// Cache to store the fully patched chain after the leaf is requested.
|
||||||
private val patchedChainCache = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
private val patchedChainCache = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||||
@@ -58,24 +98,187 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
data: Parcel,
|
data: Parcel,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
// This interceptor only needs to act on pre-transaction for software key generation.
|
// This interceptor only needs to act on pre-transaction for software key generation.
|
||||||
|
// Handle 'generate' mode interceptions using the handler map.
|
||||||
if (ConfigurationManager.shouldGenerate(callingUid)) {
|
if (ConfigurationManager.shouldGenerate(callingUid)) {
|
||||||
when (code) {
|
generateKeyHandlers[code]?.let { handler ->
|
||||||
GENERATE_KEY_TRANSACTION,
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
GET_KEY_CHARACTERISTICS_TRANSACTION,
|
return handler(txId, callingUid, callingPid, data)
|
||||||
EXPORT_KEY_TRANSACTION,
|
|
||||||
ATTEST_KEY_TRANSACTION -> {
|
|
||||||
// TODO: Implement the full software simulation logic.
|
|
||||||
logTransaction(txId, "unimplemented-generate-flow", callingUid, callingPid)
|
|
||||||
return InterceptorUtils.createSuccessReply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (ConfigurationManager.shouldGenerate(callingUid)) {
|
|
||||||
if (code == GET_TRANSACTION) return TransactionResult.Continue
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Handle 'patch' mode interceptions for the 'get' transaction.
|
||||||
|
if (ConfigurationManager.shouldPatch(callingUid) && code == GET_TRANSACTION) {
|
||||||
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||||
|
return TransactionResult.Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default behavior for all other transactions.
|
||||||
|
logTransaction(
|
||||||
|
txId,
|
||||||
|
transactionNames[code] ?: "unknown code=$code",
|
||||||
|
callingUid,
|
||||||
|
callingPid,
|
||||||
|
true,
|
||||||
|
)
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleGenerateKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
|
||||||
|
return runCatching {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val callback =
|
||||||
|
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
|
val alias = InterceptorUtils.extractAlias(data.readString()!!)
|
||||||
|
val keyId = KeyIdentifier(uid, alias)
|
||||||
|
|
||||||
|
// Read and parse the key generation arguments.
|
||||||
|
val keymasterArgs = KeymasterArguments()
|
||||||
|
if (data.readInt() == 1) {
|
||||||
|
keymasterArgs.readFromParcel(data)
|
||||||
|
}
|
||||||
|
keygenParameters[keyId] =
|
||||||
|
LegacyKeygenParameters.fromKeymasterArguments(keymasterArgs)
|
||||||
|
|
||||||
|
// Create a fake successful response for the callback.
|
||||||
|
val characteristics = KeyCharacteristics()
|
||||||
|
characteristics.swEnforced = KeymasterArguments()
|
||||||
|
characteristics.hwEnforced = keymasterArgs
|
||||||
|
|
||||||
|
val keystoreResponse = InterceptorUtils.createSuccessKeystoreResponse()
|
||||||
|
callback.onFinished(keystoreResponse, characteristics)
|
||||||
|
|
||||||
|
InterceptorUtils.createSuccessReply()
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed during handleGenerateKey.", it)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleGetKeyCharacteristics(
|
||||||
|
txId: Long,
|
||||||
|
uid: Int,
|
||||||
|
pid: Int,
|
||||||
|
data: Parcel,
|
||||||
|
): TransactionResult {
|
||||||
|
return runCatching {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val callback =
|
||||||
|
IKeystoreKeyCharacteristicsCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
|
val alias = InterceptorUtils.extractAlias(data.readString()!!)
|
||||||
|
val keyId = KeyIdentifier(uid, alias)
|
||||||
|
|
||||||
|
val params =
|
||||||
|
keygenParameters[keyId]
|
||||||
|
?: throw IllegalStateException("No params found for $keyId")
|
||||||
|
|
||||||
|
val characteristics =
|
||||||
|
KeyCharacteristics().apply {
|
||||||
|
swEnforced = KeymasterArguments()
|
||||||
|
hwEnforced =
|
||||||
|
KeymasterArguments().apply {
|
||||||
|
addEnum(KeymasterDefs.KM_TAG_ALGORITHM, params.algorithm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback.onFinished(
|
||||||
|
InterceptorUtils.createSuccessKeystoreResponse(),
|
||||||
|
characteristics,
|
||||||
|
)
|
||||||
|
|
||||||
|
InterceptorUtils.createSuccessReply()
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed during handleGetKeyCharacteristics.", it)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleExportKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
|
||||||
|
return runCatching {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val callback = IKeystoreExportKeyCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
|
val alias = InterceptorUtils.extractAlias(data.readString()!!)
|
||||||
|
val keyId = KeyIdentifier(uid, alias)
|
||||||
|
|
||||||
|
val params =
|
||||||
|
keygenParameters[keyId]
|
||||||
|
?: throw IllegalStateException("No params found for $keyId")
|
||||||
|
|
||||||
|
// Generate a software key pair using the new generator.
|
||||||
|
val keyPair =
|
||||||
|
CertificateGenerator.generateSoftwareKeyPair(params.toKeyMintAttestation())
|
||||||
|
?: throw Exception("Failed to generate software key pair.")
|
||||||
|
generatedKeyPairs[keyId] = keyPair
|
||||||
|
|
||||||
|
// Create a successful ExportResult containing the public key.
|
||||||
|
val exportResultParcel =
|
||||||
|
Parcel.obtain().apply {
|
||||||
|
writeInt(KeyStore.NO_ERROR)
|
||||||
|
writeByteArray(keyPair.public.encoded)
|
||||||
|
setDataPosition(0)
|
||||||
|
}
|
||||||
|
val exportResult = ExportResult.CREATOR.createFromParcel(exportResultParcel)
|
||||||
|
exportResultParcel.recycle()
|
||||||
|
|
||||||
|
callback.onFinished(exportResult)
|
||||||
|
|
||||||
|
InterceptorUtils.createSuccessReply()
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed during handleExportKey.", it)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleAttestKey(txId: Long, uid: Int, pid: Int, data: Parcel): TransactionResult {
|
||||||
|
return runCatching {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
val callback =
|
||||||
|
IKeystoreCertificateChainCallback.Stub.asInterface(data.readStrongBinder())
|
||||||
|
val alias = InterceptorUtils.extractAlias(data.readString()!!)
|
||||||
|
val keyId = KeyIdentifier(uid, alias)
|
||||||
|
|
||||||
|
// Get the attestation challenge from the arguments.
|
||||||
|
val params =
|
||||||
|
keygenParameters[keyId]
|
||||||
|
?: throw IllegalStateException("No params found for $keyId")
|
||||||
|
val keyPair =
|
||||||
|
generatedKeyPairs[keyId]
|
||||||
|
?: throw IllegalStateException("No keypair found for $keyId")
|
||||||
|
|
||||||
|
val attestationArgs = KeymasterArguments()
|
||||||
|
if (data.readInt() == 1) {
|
||||||
|
attestationArgs.readFromParcel(data)
|
||||||
|
val challenge =
|
||||||
|
attestationArgs.getBytes(
|
||||||
|
KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE,
|
||||||
|
ByteArray(0),
|
||||||
|
)
|
||||||
|
params.attestationChallenge = challenge
|
||||||
|
}
|
||||||
|
|
||||||
|
val certificateChain =
|
||||||
|
CertificateGenerator.generateCertificateChain(
|
||||||
|
uid,
|
||||||
|
keyPair,
|
||||||
|
null, // No attestKeyAlias in legacy flow
|
||||||
|
params.toKeyMintAttestation(), // Convert to modern format
|
||||||
|
1, // SecurityLevel.TRUSTED_ENVIRONMENT
|
||||||
|
) ?: throw Exception("CertificateGenerator failed to create attested key pair.")
|
||||||
|
|
||||||
|
val chainAsByteList = certificateChain.map { it.encoded }
|
||||||
|
val certChain = KeymasterCertificateChain(chainAsByteList)
|
||||||
|
|
||||||
|
callback.onFinished(InterceptorUtils.createSuccessKeystoreResponse(), certChain)
|
||||||
|
InterceptorUtils.createSuccessReply()
|
||||||
|
}
|
||||||
|
.getOrElse {
|
||||||
|
SystemLogger.error("[TX_ID: $txId] Failed during handleAttestKey.", it)
|
||||||
|
TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPostTransact(
|
override fun onPostTransact(
|
||||||
txId: Long,
|
txId: Long,
|
||||||
target: IBinder,
|
target: IBinder,
|
||||||
@@ -93,16 +296,22 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
reply == null ||
|
reply == null ||
|
||||||
InterceptorUtils.hasException(reply)
|
InterceptorUtils.hasException(reply)
|
||||||
) {
|
) {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Skip parsing post-transaction for [target, code, reply]: [$target, $code, $reply]"
|
||||||
|
)
|
||||||
return TransactionResult.SkipTransaction
|
return TransactionResult.SkipTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ConfigurationManager.shouldPatch(callingUid)) return TransactionResult.SkipTransaction
|
if (!ConfigurationManager.shouldPatch(callingUid)) return TransactionResult.SkipTransaction
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
data.enforceInterface(SERVICE_DESCRIPTOR)
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
val alias = data.readString() ?: ""
|
val alias = data.readString() ?: ""
|
||||||
val extractedAlias = InterceptorUtils.extractAlias(alias)
|
val extractedAlias = InterceptorUtils.extractAlias(alias)
|
||||||
val keyId = KeyIdentifier(callingUid, extractedAlias)
|
val keyId = KeyIdentifier(callingUid, extractedAlias)
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Parsed $keyId during post-transaction of ${transactionNames[code]}"
|
||||||
|
)
|
||||||
|
|
||||||
when {
|
when {
|
||||||
// Case 1: The app is requesting the leaf certificate.
|
// Case 1: The app is requesting the leaf certificate.
|
||||||
@@ -111,13 +320,11 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
val originalLeafBytes =
|
val originalLeafBytes =
|
||||||
reply.createByteArray() ?: return TransactionResult.SkipTransaction
|
reply.createByteArray() ?: return TransactionResult.SkipTransaction
|
||||||
|
|
||||||
// The original chain is not available,
|
val originalLeafCertResult = CertificateHelper.toCertificate(originalLeafBytes)
|
||||||
// so we must pass a temporary one to the patcher.
|
if (originalLeafCertResult !is CertificateHelper.OperationResult.Success) {
|
||||||
// The patcher only needs the original leaf to extract details.
|
return TransactionResult.SkipTransaction
|
||||||
val originalLeafCert =
|
}
|
||||||
(CertificateHelper.toCertificate(originalLeafBytes)
|
val originalLeafCert = originalLeafCertResult.data
|
||||||
as CertificateHelper.OperationResult.Success)
|
|
||||||
.data
|
|
||||||
val tempChain = arrayOf<Certificate>(originalLeafCert)
|
val tempChain = arrayOf<Certificate>(originalLeafCert)
|
||||||
|
|
||||||
// Perform the COMPLETE patch and rebuild operation.
|
// Perform the COMPLETE patch and rebuild operation.
|
||||||
@@ -157,11 +364,6 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
)
|
)
|
||||||
InterceptorUtils.createByteArrayReply(caCertsBytes!!)
|
InterceptorUtils.createByteArrayReply(caCertsBytes!!)
|
||||||
} else {
|
} else {
|
||||||
// We have no cached chain.
|
|
||||||
// This could mean the app requested the CA without requesting the leaf
|
|
||||||
// first, or patching failed.
|
|
||||||
// In this case, we cannot safely intervene.
|
|
||||||
// Let the original reply pass through.
|
|
||||||
SystemLogger.warning(
|
SystemLogger.warning(
|
||||||
"[TX_ID: $txId] No cached chain found for CA request on alias '$extractedAlias'. Skipping."
|
"[TX_ID: $txId] No cached chain found for CA request on alias '$extractedAlias'. Skipping."
|
||||||
)
|
)
|
||||||
@@ -177,3 +379,117 @@ object KeystoreInterceptor : AbstractKeystoreInterceptor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data class to hold key generation parameters parsed from the legacy IKeystoreService's
|
||||||
|
* KeymasterArguments. It is used exclusively by the KeystoreInterceptor to manage state during the
|
||||||
|
* software key generation flow.
|
||||||
|
*/
|
||||||
|
private data class LegacyKeygenParameters(
|
||||||
|
val algorithm: Int,
|
||||||
|
val keySize: Int,
|
||||||
|
val purpose: List<Int>,
|
||||||
|
val digest: List<Int>,
|
||||||
|
val certificateNotBefore: Date?,
|
||||||
|
val rsaPublicExponent: BigInteger?,
|
||||||
|
val ecCurveName: String?, // Derived from keySize
|
||||||
|
) {
|
||||||
|
// The challenge is provided in a separate transaction (attestKey), so it must be mutable.
|
||||||
|
var attestationChallenge: ByteArray? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the legacy parameters into the modern [KeyMintAttestation] data structure, which is
|
||||||
|
* required by the refactored [AttestationBuilder] and [CertificateGenerator].
|
||||||
|
*/
|
||||||
|
fun toKeyMintAttestation(): KeyMintAttestation {
|
||||||
|
// This conversion acts as a bridge, allowing our new generic components
|
||||||
|
// to be used by the legacy interceptor.
|
||||||
|
return KeyMintAttestation(
|
||||||
|
keySize = this.keySize,
|
||||||
|
algorithm = this.algorithm,
|
||||||
|
ecCurve = 0, // Not explicitly available in legacy args, but not critical
|
||||||
|
ecCurveName = this.ecCurveName ?: "",
|
||||||
|
blockMode = listOf<Int>(),
|
||||||
|
padding = listOf<Int>(),
|
||||||
|
purpose = this.purpose,
|
||||||
|
digest = this.digest,
|
||||||
|
rsaPublicExponent = this.rsaPublicExponent,
|
||||||
|
certificateSerial = null, // Not provided in legacy generateKey
|
||||||
|
certificateSubject = null, // Not provided in legacy generateKey
|
||||||
|
certificateNotBefore = this.certificateNotBefore,
|
||||||
|
certificateNotAfter = null, // Not provided in legacy generateKey
|
||||||
|
attestationChallenge = this.attestationChallenge,
|
||||||
|
// Device identifiers are not passed in legacy args;
|
||||||
|
// AttestationBuilder will fetch them from system properties.
|
||||||
|
brand = null,
|
||||||
|
device = null,
|
||||||
|
product = null,
|
||||||
|
serial = null,
|
||||||
|
imei = null,
|
||||||
|
meid = null,
|
||||||
|
manufacturer = null,
|
||||||
|
model = null,
|
||||||
|
secondImei = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Factory method to create an instance from a [KeymasterArguments] object. */
|
||||||
|
fun fromKeymasterArguments(args: KeymasterArguments): LegacyKeygenParameters {
|
||||||
|
val algorithm = args.getEnum(KeymasterDefs.KM_TAG_ALGORITHM, 0)
|
||||||
|
val keySize = args.getUnsignedInt(KeymasterDefs.KM_TAG_KEY_SIZE, 0).toInt()
|
||||||
|
|
||||||
|
return LegacyKeygenParameters(
|
||||||
|
algorithm = algorithm,
|
||||||
|
keySize = keySize,
|
||||||
|
purpose = args.getEnums(KeymasterDefs.KM_TAG_PURPOSE),
|
||||||
|
digest = args.getEnums(KeymasterDefs.KM_TAG_DIGEST),
|
||||||
|
certificateNotBefore = args.getDate(KeymasterDefs.KM_TAG_ACTIVE_DATETIME, Date()),
|
||||||
|
rsaPublicExponent =
|
||||||
|
if (algorithm == KeymasterDefs.KM_ALGORITHM_RSA) getRsaExponent(args) else null,
|
||||||
|
ecCurveName =
|
||||||
|
if (algorithm == KeymasterDefs.KM_ALGORITHM_EC) deriveEcCurveName(keySize)
|
||||||
|
else null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deriveEcCurveName(keySize: Int): String =
|
||||||
|
when (keySize) {
|
||||||
|
224 -> "secp224r1"
|
||||||
|
256 -> "secp256r1"
|
||||||
|
384 -> "secp384r1"
|
||||||
|
521 -> "secp521r1"
|
||||||
|
else -> "secp256r1" // Default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The RSA public exponent is not accessible via a public API in KeymasterArguments, so we
|
||||||
|
* must use reflection to extract it.
|
||||||
|
*/
|
||||||
|
private fun getRsaExponent(args: KeymasterArguments): BigInteger? {
|
||||||
|
return runCatching {
|
||||||
|
val getArgumentByTag =
|
||||||
|
KeymasterArguments::class
|
||||||
|
.java
|
||||||
|
.getDeclaredMethod("getArgumentByTag", Int::class.java)
|
||||||
|
getArgumentByTag.isAccessible = true
|
||||||
|
val rsaArgument =
|
||||||
|
getArgumentByTag.invoke(args, KeymasterDefs.KM_TAG_RSA_PUBLIC_EXPONENT)
|
||||||
|
|
||||||
|
val getLongTagValue =
|
||||||
|
KeymasterArguments::class
|
||||||
|
.java
|
||||||
|
.getDeclaredMethod(
|
||||||
|
"getLongTagValue",
|
||||||
|
Class.forName("android.security.keymaster.KeymasterArgument"),
|
||||||
|
)
|
||||||
|
getLongTagValue.isAccessible = true
|
||||||
|
getLongTagValue.invoke(args, rsaArgument) as BigInteger
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
SystemLogger.error("Failed to read rsaPublicExponent via reflection.", it)
|
||||||
|
}
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.keystore
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.system.keystore2.Domain
|
||||||
|
import android.system.keystore2.IKeystoreService
|
||||||
|
import android.system.keystore2.KeyDescriptor
|
||||||
|
import java.util.TreeMap
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to intercept listEntries and listEntriesBatched transactions.
|
||||||
|
*
|
||||||
|
* References for all mentioned functions in AOSP:
|
||||||
|
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/database.rs
|
||||||
|
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/service.rs
|
||||||
|
* https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/utils.rs
|
||||||
|
*/
|
||||||
|
object ListEntriesHandler {
|
||||||
|
|
||||||
|
// Estimate for maximum size of a Binder response in bytes.
|
||||||
|
private const val RESPONSE_SIZE_LIMIT = 358400
|
||||||
|
|
||||||
|
// Parameters of AOSP function `list_key_entries` in utils.rs.
|
||||||
|
private data class ListEntriesParams(
|
||||||
|
val domain: Int,
|
||||||
|
val namespace: Long,
|
||||||
|
val startPastAlias: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val pendingParams = ConcurrentHashMap<Long, ListEntriesParams>()
|
||||||
|
|
||||||
|
// Based on AOSP function `estimate_safe_amount_to_return` in utils.rs.
|
||||||
|
private fun estimateSafeAmountToReturn(
|
||||||
|
keyDescriptors: Array<KeyDescriptor>,
|
||||||
|
responseSizeLimit: Int,
|
||||||
|
): Int {
|
||||||
|
var itemsToReturn = 0
|
||||||
|
var returnedBytes = 0
|
||||||
|
|
||||||
|
for (kd in keyDescriptors) {
|
||||||
|
// 4 bytes for the Domain enum
|
||||||
|
// 8 bytes for the Namespace long
|
||||||
|
returnedBytes += 4 + 8
|
||||||
|
|
||||||
|
kd.alias?.let { returnedBytes += 4 + it.toByteArray(Charsets.UTF_8).size }
|
||||||
|
kd.blob?.let { returnedBytes += 4 + it.size }
|
||||||
|
|
||||||
|
if (returnedBytes > responseSizeLimit) {
|
||||||
|
SystemLogger.warning(
|
||||||
|
"Key descriptors list (${keyDescriptors.size} items) may exceed binder size limit, returning $itemsToReturn items with estimated size: $returnedBytes bytes."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
itemsToReturn++
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemsToReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and store parameters for later use (in post-transaction).
|
||||||
|
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): Boolean {
|
||||||
|
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||||
|
|
||||||
|
val domain = data.readInt()
|
||||||
|
val namespace = data.readLong()
|
||||||
|
val startPastAlias = if (isBatchMode) data.readString() else null
|
||||||
|
|
||||||
|
// List entries is only supported for Domain::APP and Domain::SELINUX.
|
||||||
|
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
|
||||||
|
// Note that all generated keys belong to Domain::APP.
|
||||||
|
if (domain == Domain.APP) {
|
||||||
|
pendingParams[txId] = ListEntriesParams(domain, namespace, startPastAlias)
|
||||||
|
SystemLogger.debug("[TX_ID: $txId] Cached ${pendingParams[txId]}.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge software-backed keys with hardware-backed keys in the reply parcel.
|
||||||
|
fun injectGeneratedKeys(txId: Long, callingUid: Int, reply: Parcel): Array<KeyDescriptor> {
|
||||||
|
val params =
|
||||||
|
pendingParams.remove(txId)
|
||||||
|
?: throw IllegalStateException("No params found for listing entries")
|
||||||
|
|
||||||
|
// By default we use the calling uid as namespace if domain is Domain::APP.
|
||||||
|
// The namespace parameter is thus ignored for non-privileged applications.
|
||||||
|
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
|
||||||
|
val keysToInject =
|
||||||
|
extractGeneratedKeyDescriptors(callingUid, callingUid.toLong(), params.startPastAlias)
|
||||||
|
val originalList = reply.createTypedArray(KeyDescriptor.CREATOR)!!
|
||||||
|
val mergedArray = mergeKeyDescriptors(originalList, keysToInject)
|
||||||
|
|
||||||
|
// Limit response size to avoid binder buffer overflow.
|
||||||
|
// See AOSP function `list_key_entries` in utils.rs.
|
||||||
|
val safeAmountToReturn = estimateSafeAmountToReturn(mergedArray, RESPONSE_SIZE_LIMIT)
|
||||||
|
|
||||||
|
return if (safeAmountToReturn < mergedArray.size) {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Listing entries are truncated [${mergedArray.size} -> $safeAmountToReturn] to avoid transaction overflow."
|
||||||
|
)
|
||||||
|
mergedArray.copyOfRange(0, safeAmountToReturn)
|
||||||
|
} else {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Listing entries returns ${mergedArray.size} [injected: ${keysToInject.size}] keys."
|
||||||
|
)
|
||||||
|
mergedArray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge hardware and software key descriptors into a single sorted array.
|
||||||
|
private fun mergeKeyDescriptors(
|
||||||
|
hardwareKeys: Array<KeyDescriptor>,
|
||||||
|
keysToInject: List<KeyDescriptor>,
|
||||||
|
): Array<KeyDescriptor> {
|
||||||
|
// Uses TreeMap to ensure alphabetical ordering and uniqueness (prefer injected keys).
|
||||||
|
val combinedMap = TreeMap<String, KeyDescriptor>()
|
||||||
|
hardwareKeys.forEach { key -> key.alias?.let { combinedMap[it] = key } }
|
||||||
|
keysToInject.forEach { key -> key.alias?.let { combinedMap[it] = key } }
|
||||||
|
return combinedMap.values.toTypedArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on AOSP function `list_past_alias` in database.rs
|
||||||
|
private fun extractGeneratedKeyDescriptors(
|
||||||
|
uid: Int,
|
||||||
|
namespace: Long,
|
||||||
|
startPastAlias: String?,
|
||||||
|
): List<KeyDescriptor> {
|
||||||
|
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
|
||||||
|
.filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) }
|
||||||
|
.map { keyId ->
|
||||||
|
KeyDescriptor().apply {
|
||||||
|
this.domain = Domain.APP
|
||||||
|
this.nspace = namespace
|
||||||
|
this.alias = keyId.alias
|
||||||
|
this.blob = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+415
-71
@@ -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,9 +8,16 @@ 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.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.KeyMintAttestation
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
@@ -19,17 +27,16 @@ 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 response: KeyEntryResponse)
|
val keyPair: KeyPair,
|
||||||
|
val nspace: Long,
|
||||||
|
val response: KeyEntryResponse,
|
||||||
|
)
|
||||||
|
|
||||||
override fun onPreTransact(
|
override fun onPreTransact(
|
||||||
txId: Long,
|
txId: Long,
|
||||||
@@ -40,33 +47,197 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
callingPid: Int,
|
callingPid: Int,
|
||||||
data: Parcel,
|
data: Parcel,
|
||||||
): TransactionResult {
|
): TransactionResult {
|
||||||
// This interceptor only handles the 'generateKey' transaction directly.
|
val shouldSkip = ConfigurationManager.shouldSkipUid(callingUid)
|
||||||
if (code == GENERATE_KEY_TRANSACTION) {
|
|
||||||
logTransaction(txId, "generateKey", callingUid, callingPid)
|
when (code) {
|
||||||
|
GENERATE_KEY_TRANSACTION -> {
|
||||||
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
|
|
||||||
|
if (!shouldSkip) return handleGenerateKey(txId, callingUid, data)
|
||||||
|
}
|
||||||
|
CREATE_OPERATION_TRANSACTION -> {
|
||||||
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
|
|
||||||
|
if (!shouldSkip) return handleCreateOperation(txId, callingUid, data)
|
||||||
|
}
|
||||||
|
IMPORT_KEY_TRANSACTION -> {
|
||||||
|
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||||
|
|
||||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||||
return handleGenerateKey(callingUid, data)
|
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
||||||
} else {
|
SystemLogger.info(
|
||||||
|
"[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]"
|
||||||
|
)
|
||||||
|
return TransactionResult.Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logTransaction(
|
logTransaction(
|
||||||
txId,
|
txId,
|
||||||
transactionNames[code] ?: "unknown code=$code",
|
transactionNames[code] ?: "unknown code=$code",
|
||||||
callingUid,
|
callingUid,
|
||||||
callingPid,
|
callingPid,
|
||||||
false,
|
true,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPostTransact(
|
||||||
|
txId: Long,
|
||||||
|
target: IBinder,
|
||||||
|
code: Int,
|
||||||
|
flags: Int,
|
||||||
|
callingUid: Int,
|
||||||
|
callingPid: Int,
|
||||||
|
data: Parcel,
|
||||||
|
reply: Parcel?,
|
||||||
|
resultCode: Int,
|
||||||
|
): TransactionResult {
|
||||||
|
if (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
|
||||||
|
|
||||||
|
if (code == IMPORT_KEY_TRANSACTION) {
|
||||||
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
|
||||||
|
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||||
|
val keyDescriptor =
|
||||||
|
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||||
|
?: return TransactionResult.SkipTransaction
|
||||||
|
// Evict generated key data but retain patched chains so detectors
|
||||||
|
// can't use importKey to force unpatched getKeyEntry responses.
|
||||||
|
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||||
|
if (generatedKeys.remove(keyId) != null) {
|
||||||
|
SystemLogger.debug("Remove generated key on importKey $keyId")
|
||||||
|
GeneratedKeyPersistence.delete(keyId)
|
||||||
|
}
|
||||||
|
attestationKeys.remove(keyId)
|
||||||
|
} else if (code == CREATE_OPERATION_TRANSACTION) {
|
||||||
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
|
||||||
|
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||||
|
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
||||||
|
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||||
|
val parsedParams = KeyMintAttestation(params)
|
||||||
|
val forced = data.readBoolean()
|
||||||
|
if (forced)
|
||||||
|
SystemLogger.verbose(
|
||||||
|
"[TX_ID: $txId] Current operation has a very high pruning power."
|
||||||
|
)
|
||||||
|
val response: CreateOperationResponse =
|
||||||
|
reply.readTypedObject(CreateOperationResponse.CREATOR)!!
|
||||||
|
SystemLogger.verbose(
|
||||||
|
"[TX_ID: $txId] CreateOperationResponse: ${response.iOperation} ${response.operationChallenge}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Intercept the IKeystoreOperation binder
|
||||||
|
response.iOperation?.let { operation ->
|
||||||
|
val operationBinder = operation.asBinder()
|
||||||
|
if (!interceptedOperations.containsKey(operationBinder)) {
|
||||||
|
SystemLogger.info("Found new IKeystoreOperation. Registering interceptor...")
|
||||||
|
val backdoor = getBackdoor(target)
|
||||||
|
if (backdoor != null) {
|
||||||
|
val interceptor = OperationInterceptor(operation, backdoor)
|
||||||
|
register(backdoor, operationBinder, interceptor)
|
||||||
|
interceptedOperations[operationBinder] = interceptor
|
||||||
|
} else {
|
||||||
|
SystemLogger.error(
|
||||||
|
"Failed to get backdoor to register OperationInterceptor."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (code == GENERATE_KEY_TRANSACTION) {
|
||||||
|
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||||
|
|
||||||
|
val metadata: KeyMetadata =
|
||||||
|
reply.readTypedObject(KeyMetadata.CREATOR)
|
||||||
|
?: return TransactionResult.SkipTransaction
|
||||||
|
val originalChain =
|
||||||
|
CertificateHelper.getCertificateChain(metadata)
|
||||||
|
?: return TransactionResult.SkipTransaction
|
||||||
|
if (originalChain.size > 1) {
|
||||||
|
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||||
|
|
||||||
|
// Cache the newly patched chain to ensure consistency across subsequent API calls.
|
||||||
|
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||||
|
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
||||||
|
val key = metadata.key!!
|
||||||
|
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||||
|
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
|
||||||
|
|
||||||
|
// We must clean up cached generated keys before storing the patched chain
|
||||||
|
cleanupKeyData(keyId)
|
||||||
|
patchedChains[keyId] = newChain
|
||||||
|
SystemLogger.debug(
|
||||||
|
"Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])"
|
||||||
|
)
|
||||||
|
|
||||||
|
return InterceptorUtils.createTypedObjectReply(metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TransactionResult.SkipTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCreateOperation(
|
||||||
|
txId: Long,
|
||||||
|
callingUid: Int,
|
||||||
|
data: Parcel,
|
||||||
|
): TransactionResult {
|
||||||
|
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||||
|
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
||||||
|
|
||||||
|
// An operation must use the KEY_ID domain.
|
||||||
|
if (keyDescriptor.domain != Domain.KEY_ID) {
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
|
||||||
|
val nspace = keyDescriptor.nspace
|
||||||
|
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
|
||||||
|
|
||||||
|
if (generatedKeyInfo == null) {
|
||||||
|
SystemLogger.debug(
|
||||||
|
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
|
||||||
|
)
|
||||||
|
return TransactionResult.Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
|
||||||
|
|
||||||
|
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||||
|
val parsedParams = KeyMintAttestation(params)
|
||||||
|
|
||||||
|
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
|
||||||
|
val operationBinder = SoftwareOperationBinder(softwareOperation)
|
||||||
|
|
||||||
|
val response =
|
||||||
|
CreateOperationResponse().apply {
|
||||||
|
iOperation = operationBinder
|
||||||
|
operationChallenge = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return InterceptorUtils.createTypedObjectReply(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleGenerateKey(txId: Long, callingUid: Int, data: Parcel): TransactionResult {
|
||||||
|
if (data.dataSize() > MAX_ALIAS_LENGTH) {
|
||||||
|
SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes")
|
||||||
return TransactionResult.ContinueAndSkipPost
|
return TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the `generateKey` transaction. Based on the configuration for the calling UID, it
|
|
||||||
* either generates a key in software or lets the call pass through to the hardware.
|
|
||||||
*/
|
|
||||||
private fun handleGenerateKey(callingUid: Int, data: Parcel): TransactionResult {
|
|
||||||
return runCatching {
|
return runCatching {
|
||||||
|
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(
|
||||||
"[key, attestationKey]: ${keyDescriptor.alias}, ${attestationKey?.alias}"
|
"Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}"
|
||||||
)
|
)
|
||||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||||
val parsedParams = KeyMintAttestation(params)
|
val parsedParams = KeyMintAttestation(params)
|
||||||
@@ -75,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) ||
|
||||||
@@ -84,49 +253,75 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
|
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
|
||||||
|
|
||||||
if (needsSoftwareGeneration) {
|
if (needsSoftwareGeneration) {
|
||||||
SystemLogger.info(
|
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
||||||
"Generating software key for alias '${keyDescriptor.alias}' (UID: $callingUid)."
|
} else if (parsedParams.attestationChallenge != null) {
|
||||||
)
|
val windowUsed = hardwareKeygenWindowCount(callingUid)
|
||||||
|
val concurrentUsed = hardwareKeygenCount(callingUid).get()
|
||||||
|
|
||||||
// Generate the key pair and certificate chain.
|
// Sliding window rate limit
|
||||||
val keyData =
|
if (windowUsed >= MAX_HW_KEYGEN_PER_WINDOW) {
|
||||||
CertificateGenerator.generateAttestedKeyPair(
|
SystemLogger.info("[TX_ID: $txId] RATE_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=$concurrentUsed → software fallback")
|
||||||
callingUid,
|
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
||||||
keyDescriptor.alias,
|
|
||||||
attestationKey?.alias,
|
|
||||||
parsedParams,
|
|
||||||
securityLevel,
|
|
||||||
) ?: throw Exception("CertificateGenerator failed to create key pair.")
|
|
||||||
|
|
||||||
// Store the generated key data.
|
|
||||||
val response =
|
|
||||||
buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
|
|
||||||
|
|
||||||
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, response)
|
|
||||||
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
|
||||||
|
|
||||||
// Return the metadata of our generated key, skipping the real hardware call.
|
|
||||||
val resultParcel =
|
|
||||||
Parcel.obtain().apply {
|
|
||||||
writeNoException()
|
|
||||||
writeTypedObject(response.metadata, 0)
|
|
||||||
}
|
}
|
||||||
return TransactionResult.OverrideReply(0, resultParcel)
|
// 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)
|
cleanupKeyData(keyId)
|
||||||
TransactionResult.Continue
|
TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
.getOrElse {
|
.getOrElse {
|
||||||
SystemLogger.error("Error during generateKey handling for UID $callingUid.", it)
|
SystemLogger.error("Error during generateKey handling for UID $callingUid.", it)
|
||||||
TransactionResult.Continue // Fallback to original service on error.
|
TransactionResult.ContinueAndSkipPost
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
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,
|
||||||
@@ -145,12 +340,130 @@ class KeyMintSecurityLevelInterceptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadPersistedKeys() {
|
||||||
|
val records = GeneratedKeyPersistence.loadAll(securityLevel)
|
||||||
|
if (records.isEmpty()) {
|
||||||
|
SystemLogger.debug("No persisted keys to restore for security level $securityLevel")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLogger.info("Restoring ${records.size} persisted keys for security level $securityLevel")
|
||||||
|
|
||||||
|
for (record in records) {
|
||||||
|
runCatching {
|
||||||
|
val keyId = KeyIdentifier(record.uid, record.alias)
|
||||||
|
if (generatedKeys.containsKey(keyId)) {
|
||||||
|
SystemLogger.debug("Skipping already-loaded key: $keyId")
|
||||||
|
return@runCatching
|
||||||
|
}
|
||||||
|
|
||||||
|
val algorithmName = when (record.algorithm) {
|
||||||
|
Algorithm.EC -> "EC"
|
||||||
|
Algorithm.RSA -> "RSA"
|
||||||
|
else -> throw IllegalArgumentException("Unknown algorithm: ${record.algorithm}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyFactory = KeyFactory.getInstance(algorithmName)
|
||||||
|
val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(record.privateKeyBytes))
|
||||||
|
|
||||||
|
val certFactory = CertificateFactory.getInstance("X.509")
|
||||||
|
val certChain = record.certChainBytes.map { bytes ->
|
||||||
|
certFactory.generateCertificate(ByteArrayInputStream(bytes))
|
||||||
|
}
|
||||||
|
require(certChain.isNotEmpty()) { "Persisted key has empty certificate chain" }
|
||||||
|
|
||||||
|
val publicKey = certChain[0].publicKey
|
||||||
|
val keyPair = KeyPair(publicKey, privateKey)
|
||||||
|
|
||||||
|
val descriptor = KeyDescriptor().apply {
|
||||||
|
domain = Domain.APP
|
||||||
|
nspace = record.nspace
|
||||||
|
alias = record.alias
|
||||||
|
blob = null
|
||||||
|
}
|
||||||
|
|
||||||
|
val attestation = KeyMintAttestation(
|
||||||
|
keySize = record.keySize,
|
||||||
|
algorithm = record.algorithm,
|
||||||
|
ecCurve = record.ecCurve,
|
||||||
|
ecCurveName = "",
|
||||||
|
blockMode = emptyList(),
|
||||||
|
padding = emptyList(),
|
||||||
|
purpose = record.purposes,
|
||||||
|
digest = record.digests,
|
||||||
|
rsaPublicExponent = null,
|
||||||
|
certificateSerial = null,
|
||||||
|
certificateSubject = null,
|
||||||
|
certificateNotBefore = null,
|
||||||
|
certificateNotAfter = null,
|
||||||
|
attestationChallenge = null,
|
||||||
|
brand = null,
|
||||||
|
device = null,
|
||||||
|
product = null,
|
||||||
|
serial = null,
|
||||||
|
imei = null,
|
||||||
|
meid = null,
|
||||||
|
manufacturer = null,
|
||||||
|
model = null,
|
||||||
|
secondImei = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = buildKeyEntryResponse(certChain, attestation, descriptor)
|
||||||
|
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response)
|
||||||
|
if (record.isAttestationKey) attestationKeys.add(keyId)
|
||||||
|
|
||||||
|
SystemLogger.debug("Restored persisted key: $keyId")
|
||||||
|
}.onFailure {
|
||||||
|
SystemLogger.error("Failed to restore key: uid=${record.uid} alias=${record.alias}", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLogger.info("Key restoration complete. Total in memory: ${generatedKeys.size}")
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Transaction codes for IKeystoreSecurityLevel interface.
|
private val secureRandom = SecureRandom()
|
||||||
|
|
||||||
|
// Maximum alias length to prevent binder buffer exhaustion (Issue #109)
|
||||||
|
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
|
||||||
|
private const val MAX_ALIAS_LENGTH = 256 * 1024
|
||||||
|
private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2
|
||||||
|
// Sliding window: max hardware keygen permits per UID within the burst window
|
||||||
|
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
|
||||||
|
private const val BURST_WINDOW_MS = 30_000L
|
||||||
|
|
||||||
|
private val uidHardwareKeygenCount = ConcurrentHashMap<Int, AtomicInteger>()
|
||||||
|
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
|
||||||
|
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
|
||||||
|
|
||||||
|
private fun hardwareKeygenCount(uid: Int): AtomicInteger =
|
||||||
|
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
|
||||||
|
|
||||||
|
private fun hardwareKeygenWindowCount(uid: Int): Int {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
|
||||||
|
synchronized(timestamps) {
|
||||||
|
timestamps.removeAll { now - it > BURST_WINDOW_MS }
|
||||||
|
return timestamps.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun recordHardwareKeygen(uid: Int) {
|
||||||
|
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
|
||||||
|
synchronized(timestamps) {
|
||||||
|
timestamps.add(System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val GENERATE_KEY_TRANSACTION =
|
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 =
|
||||||
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "importKey")
|
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "importKey")
|
||||||
|
private val CREATE_OPERATION_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(
|
||||||
|
IKeystoreSecurityLevel.Stub::class.java,
|
||||||
|
"createOperation",
|
||||||
|
)
|
||||||
|
|
||||||
private val transactionNames: Map<Int, String> by lazy {
|
private val transactionNames: Map<Int, String> by lazy {
|
||||||
IKeystoreSecurityLevel.Stub::class
|
IKeystoreSecurityLevel.Stub::class
|
||||||
@@ -163,39 +476,71 @@ 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>()
|
||||||
// A set to quickly identify keys that were generated for attestation purposes.
|
// Caches patched chains to prevent re-generation and signature inconsistencies
|
||||||
|
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||||
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||||
|
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
|
||||||
|
|
||||||
|
fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? {
|
||||||
|
if (nspace == null || nspace == 0L) return null
|
||||||
|
return generatedKeys.entries
|
||||||
|
.filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid }
|
||||||
|
.find { (_, info) -> info.nspace == nspace }
|
||||||
|
?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPatchedChain(keyId: KeyIdentifier): Array<Certificate>? = patchedChains[keyId]
|
||||||
|
|
||||||
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
|
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
|
||||||
|
|
||||||
fun cleanupKeyData(keyId: KeyIdentifier) {
|
fun cleanupKeyData(keyId: KeyIdentifier) {
|
||||||
generatedKeys.remove(keyId)
|
if (generatedKeys.remove(keyId) != null) {
|
||||||
attestationKeys.remove(keyId)
|
SystemLogger.debug("Remove generated key ${keyId}")
|
||||||
|
GeneratedKeyPersistence.delete(keyId)
|
||||||
|
}
|
||||||
|
if (patchedChains.remove(keyId) != null) {
|
||||||
|
SystemLogger.debug("Remove patched chain for ${keyId}")
|
||||||
|
}
|
||||||
|
if (attestationKeys.remove(keyId)) {
|
||||||
|
SystemLogger.debug("Remove cached attestaion key ${keyId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
|
||||||
|
unregister(backdoor, operationBinder)
|
||||||
|
|
||||||
|
if (interceptedOperations.remove(operationBinder) != null) {
|
||||||
|
SystemLogger.debug("Removed operation interceptor for binder: $operationBinder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidatePatchedChains(reason: String? = null) {
|
||||||
|
val count = patchedChains.size
|
||||||
|
if (count == 0) return
|
||||||
|
val reasonMessage = reason?.let { " due to $it" } ?: ""
|
||||||
|
patchedChains.clear()
|
||||||
|
SystemLogger.info("Invalidated $count patched cert chains$reasonMessage.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAllGeneratedKeys(reason: String? = null) {
|
||||||
|
val count = generatedKeys.size
|
||||||
|
val reasonMessage = reason?.let { " due to $it" } ?: ""
|
||||||
|
generatedKeys.clear()
|
||||||
|
patchedChains.clear()
|
||||||
|
attestationKeys.clear()
|
||||||
|
GeneratedKeyPersistence.deleteAll()
|
||||||
|
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension function to convert parsed `KeyMintAttestation` parameters back into an array of
|
|
||||||
* `Authorization` objects for the fake `KeyMetadata`. This version correctly handles the
|
|
||||||
* instantiation of Authorization objects.
|
|
||||||
*/
|
|
||||||
private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Authorization> {
|
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 {
|
||||||
@@ -208,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))) }
|
||||||
|
|
||||||
|
|||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.keystore.shim
|
||||||
|
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.system.keystore2.IKeystoreOperation
|
||||||
|
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||||
|
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercepts calls to an `IKeystoreOperation` service. This is used to log the data manipulation
|
||||||
|
* methods of a cryptographic operation.
|
||||||
|
*/
|
||||||
|
class OperationInterceptor(
|
||||||
|
private val original: IKeystoreOperation,
|
||||||
|
private val backdoor: IBinder,
|
||||||
|
) : BinderInterceptor() {
|
||||||
|
|
||||||
|
override fun onPreTransact(
|
||||||
|
txId: Long,
|
||||||
|
target: IBinder,
|
||||||
|
code: Int,
|
||||||
|
flags: Int,
|
||||||
|
callingUid: Int,
|
||||||
|
callingPid: Int,
|
||||||
|
data: Parcel,
|
||||||
|
): TransactionResult {
|
||||||
|
val methodName = transactionNames[code] ?: "unknown code=$code"
|
||||||
|
logTransaction(txId, methodName, callingUid, callingPid, true)
|
||||||
|
|
||||||
|
if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) {
|
||||||
|
KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TransactionResult.ContinueAndSkipPost
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val UPDATE_AAD_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "updateAad")
|
||||||
|
private val UPDATE_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "update")
|
||||||
|
private val FINISH_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "finish")
|
||||||
|
private val ABORT_TRANSACTION =
|
||||||
|
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
|
||||||
|
|
||||||
|
private val transactionNames: Map<Int, String> by lazy {
|
||||||
|
IKeystoreOperation.Stub::class
|
||||||
|
.java
|
||||||
|
.declaredFields
|
||||||
|
.filter {
|
||||||
|
it.isAccessible = true
|
||||||
|
it.type == Int::class.java && it.name.startsWith("TRANSACTION_")
|
||||||
|
}
|
||||||
|
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+216
@@ -0,0 +1,216 @@
|
|||||||
|
package org.matrix.TEESimulator.interception.keystore.shim
|
||||||
|
|
||||||
|
import android.hardware.security.keymint.Algorithm
|
||||||
|
import android.hardware.security.keymint.BlockMode
|
||||||
|
import android.hardware.security.keymint.Digest
|
||||||
|
import android.hardware.security.keymint.KeyPurpose
|
||||||
|
import android.hardware.security.keymint.PaddingMode
|
||||||
|
import android.os.RemoteException
|
||||||
|
import android.system.keystore2.IKeystoreOperation
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.Signature
|
||||||
|
import java.security.SignatureException
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
|
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||||
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
|
||||||
|
// A sealed interface to represent the different cryptographic operations we can perform.
|
||||||
|
private sealed interface CryptoPrimitive {
|
||||||
|
fun update(data: ByteArray?): ByteArray?
|
||||||
|
|
||||||
|
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||||
|
|
||||||
|
fun abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper object to map KeyMint constants to JCA algorithm strings.
|
||||||
|
private object JcaAlgorithmMapper {
|
||||||
|
fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
|
||||||
|
val digest =
|
||||||
|
when (params.digest.firstOrNull()) {
|
||||||
|
Digest.SHA_2_256 -> "SHA256"
|
||||||
|
Digest.SHA_2_384 -> "SHA384"
|
||||||
|
Digest.SHA_2_512 -> "SHA512"
|
||||||
|
else -> "NONE"
|
||||||
|
}
|
||||||
|
val keyAlgo =
|
||||||
|
when (params.algorithm) {
|
||||||
|
Algorithm.EC -> "ECDSA"
|
||||||
|
Algorithm.RSA -> "RSA"
|
||||||
|
else ->
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Unsupported signature algorithm: ${params.algorithm}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return "${digest}with${keyAlgo}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||||
|
val keyAlgo =
|
||||||
|
when (params.algorithm) {
|
||||||
|
Algorithm.RSA -> "RSA"
|
||||||
|
Algorithm.AES -> "AES"
|
||||||
|
else ->
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Unsupported cipher algorithm: ${params.algorithm}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val blockMode =
|
||||||
|
when (params.blockMode.firstOrNull()) {
|
||||||
|
BlockMode.ECB -> "ECB"
|
||||||
|
BlockMode.CBC -> "CBC"
|
||||||
|
BlockMode.GCM -> "GCM"
|
||||||
|
else -> "ECB" // Default for RSA
|
||||||
|
}
|
||||||
|
val padding =
|
||||||
|
when (params.padding.firstOrNull()) {
|
||||||
|
PaddingMode.NONE -> "NoPadding"
|
||||||
|
PaddingMode.PKCS7 -> "PKCS7Padding"
|
||||||
|
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
|
||||||
|
PaddingMode.RSA_OAEP -> "OAEPPadding"
|
||||||
|
else -> "NoPadding" // Default for GCM
|
||||||
|
}
|
||||||
|
return "$keyAlgo/$blockMode/$padding"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concrete implementation for Signing.
|
||||||
|
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||||
|
private val signature: Signature =
|
||||||
|
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||||
|
initSign(keyPair.private)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(data: ByteArray?): ByteArray? {
|
||||||
|
if (data != null) signature.update(data)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray {
|
||||||
|
if (data != null) update(data)
|
||||||
|
return this.signature.sign()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concrete implementation for Verification.
|
||||||
|
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
|
||||||
|
private val signature: Signature =
|
||||||
|
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
|
||||||
|
initVerify(keyPair.public)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(data: ByteArray?): ByteArray? {
|
||||||
|
if (data != null) signature.update(data)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
if (data != null) update(data)
|
||||||
|
if (signature == null) throw SignatureException("Signature to verify is null")
|
||||||
|
if (!this.signature.verify(signature)) {
|
||||||
|
// Throwing an exception is how Keystore signals verification failure.
|
||||||
|
throw SignatureException("Signature verification failed")
|
||||||
|
}
|
||||||
|
// A successful verification returns no data.
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concrete implementation for Encryption/Decryption.
|
||||||
|
private class CipherPrimitive(
|
||||||
|
keyPair: KeyPair,
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
private val opMode: Int,
|
||||||
|
) : CryptoPrimitive {
|
||||||
|
private val cipher: Cipher =
|
||||||
|
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
|
||||||
|
val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private
|
||||||
|
init(opMode, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun update(data: ByteArray?): ByteArray? =
|
||||||
|
if (data != null) cipher.update(data) else null
|
||||||
|
|
||||||
|
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
|
||||||
|
if (data != null) cipher.doFinal(data) else cipher.doFinal()
|
||||||
|
|
||||||
|
override fun abort() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A software-only implementation of a cryptographic operation. This class acts as a controller,
|
||||||
|
* delegating to a specific cryptographic primitive based on the operation's purpose.
|
||||||
|
*/
|
||||||
|
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
|
||||||
|
// This now holds the specific strategy object (Signer, Verifier, etc.)
|
||||||
|
private val primitive: CryptoPrimitive
|
||||||
|
|
||||||
|
init {
|
||||||
|
// The "Strategy" pattern: choose the implementation based on the purpose.
|
||||||
|
// For simplicity, we only consider the first purpose listed.
|
||||||
|
val purpose = params.purpose.firstOrNull()
|
||||||
|
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
|
||||||
|
|
||||||
|
primitive =
|
||||||
|
when (purpose) {
|
||||||
|
KeyPurpose.SIGN -> Signer(keyPair, params)
|
||||||
|
KeyPurpose.VERIFY -> Verifier(keyPair, params)
|
||||||
|
KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE)
|
||||||
|
KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE)
|
||||||
|
else ->
|
||||||
|
throw UnsupportedOperationException("Unsupported operation purpose: $purpose")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(data: ByteArray?): ByteArray? {
|
||||||
|
try {
|
||||||
|
return primitive.update(data)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
try {
|
||||||
|
val result = primitive.finish(data, signature)
|
||||||
|
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||||
|
return result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
||||||
|
// Re-throw the exception so the binder can report it to the client.
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun abort() {
|
||||||
|
primitive.abort()
|
||||||
|
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The Binder interface for our [SoftwareOperation]. */
|
||||||
|
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||||
|
IKeystoreOperation.Stub() {
|
||||||
|
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
override fun update(input: ByteArray?): ByteArray? {
|
||||||
|
return operation.update(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||||
|
return operation.finish(input, signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(RemoteException::class)
|
||||||
|
override fun abort() {
|
||||||
|
operation.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
package org.matrix.TEESimulator.logging
|
package org.matrix.TEESimulator.logging
|
||||||
|
|
||||||
import android.hardware.security.keymint.Algorithm
|
import android.hardware.security.keymint.*
|
||||||
import android.hardware.security.keymint.Digest
|
|
||||||
import android.hardware.security.keymint.EcCurve
|
|
||||||
import android.hardware.security.keymint.KeyParameter
|
|
||||||
import android.hardware.security.keymint.KeyPurpose
|
|
||||||
import android.hardware.security.keymint.Tag
|
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -34,7 +29,23 @@ object KeyMintParameterLogger {
|
|||||||
.associate { field -> (field.get(null) as Int) to field.name }
|
.associate { field -> (field.get(null) as Int) to field.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val purposeNames: Map<Int, String> by lazy {
|
val blockModeNames: Map<Int, String> by lazy {
|
||||||
|
BlockMode::class
|
||||||
|
.java
|
||||||
|
.fields
|
||||||
|
.filter { it.type == Int::class.java }
|
||||||
|
.associate { field -> (field.get(null) as Int) to field.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
val paddingNames: Map<Int, String> by lazy {
|
||||||
|
PaddingMode::class
|
||||||
|
.java
|
||||||
|
.fields
|
||||||
|
.filter { it.type == Int::class.java }
|
||||||
|
.associate { field -> (field.get(null) as Int) to field.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
val purposeNames: Map<Int, String> by lazy {
|
||||||
KeyPurpose::class
|
KeyPurpose::class
|
||||||
.java
|
.java
|
||||||
.fields
|
.fields
|
||||||
@@ -69,7 +80,9 @@ object KeyMintParameterLogger {
|
|||||||
val formattedValue: String =
|
val formattedValue: String =
|
||||||
when (param.tag) {
|
when (param.tag) {
|
||||||
Tag.ALGORITHM -> algorithmNames[value.algorithm]
|
Tag.ALGORITHM -> algorithmNames[value.algorithm]
|
||||||
|
Tag.BLOCK_MODE -> blockModeNames[value.blockMode]
|
||||||
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
|
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
|
||||||
|
Tag.PADDING -> paddingNames[value.paddingMode]
|
||||||
Tag.PURPOSE -> purposeNames[value.keyPurpose]
|
Tag.PURPOSE -> purposeNames[value.keyPurpose]
|
||||||
Tag.DIGEST -> digestNames[value.digest]
|
Tag.DIGEST -> digestNames[value.digest]
|
||||||
Tag.AUTH_TIMEOUT,
|
Tag.AUTH_TIMEOUT,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.matrix.TEESimulator.logging
|
package org.matrix.TEESimulator.logging
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import org.matrix.TEESimulator.BuildConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A centralized logging utility for the TEESimulator application. This object provides a consistent
|
* A centralized logging utility for the TEESimulator application. This object provides a consistent
|
||||||
@@ -10,6 +11,8 @@ object SystemLogger {
|
|||||||
// The tag used for all log messages from this application.
|
// The tag used for all log messages from this application.
|
||||||
private const val TAG = "TEESimulator"
|
private const val TAG = "TEESimulator"
|
||||||
|
|
||||||
|
private val isDebugBuild = BuildConfig.DEBUG
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
|
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
|
||||||
*
|
*
|
||||||
@@ -64,6 +67,7 @@ object SystemLogger {
|
|||||||
* @param message The message to log.
|
* @param message The message to log.
|
||||||
*/
|
*/
|
||||||
fun verbose(message: String) {
|
fun verbose(message: String) {
|
||||||
|
if (!isDebugBuild) return
|
||||||
Log.v(TAG, message)
|
Log.v(TAG, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.Security
|
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.security.spec.ECGenParameterSpec
|
import java.security.spec.ECGenParameterSpec
|
||||||
@@ -21,6 +21,7 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
|||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
||||||
|
import org.matrix.TEESimulator.attestation.AttestationConstants
|
||||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
|
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
|
||||||
@@ -35,14 +36,6 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
*/
|
*/
|
||||||
object CertificateGenerator {
|
object CertificateGenerator {
|
||||||
|
|
||||||
init {
|
|
||||||
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
|
|
||||||
// We must remove the system provider first to ensure the full Bouncy Castle library
|
|
||||||
// (packaged with the app) is used.
|
|
||||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
|
||||||
Security.addProvider(BouncyCastleProvider())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a software-based cryptographic key pair.
|
* Generates a software-based cryptographic key pair.
|
||||||
*
|
*
|
||||||
@@ -72,16 +65,61 @@ object CertificateGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new key pair and a corresponding certificate chain containing a simulated
|
* Generates a certificate chain for a given key pair. This is the primary function for creating
|
||||||
* attestation.
|
* attested certificates.
|
||||||
*
|
*
|
||||||
* @param uid The UID of the application requesting the key.
|
* @param uid The UID of the application requesting the key.
|
||||||
* @param alias The alias for the new key.
|
* @param subjectKeyPair The key pair for which the certificate will be generated.
|
||||||
* @param attestKeyAlias Optional alias of a key to use for attestation signing.
|
* @param attestKeyAlias Optional alias of a key to use for attestation signing.
|
||||||
* @param params The parameters for the new key and its attestation.
|
* @param params The parameters for the new key and its attestation.
|
||||||
* @param securityLevel The security level to embed in the attestation.
|
* @param securityLevel The security level to embed in the attestation.
|
||||||
* @return A [Pair] containing the new [KeyPair] and its certificate chain, or `null` on
|
* @return A [List] of [Certificate] forming the new chain, or `null` on failure.
|
||||||
* failure.
|
*/
|
||||||
|
fun generateCertificateChain(
|
||||||
|
uid: Int,
|
||||||
|
subjectKeyPair: KeyPair,
|
||||||
|
attestKeyAlias: String?,
|
||||||
|
params: KeyMintAttestation,
|
||||||
|
securityLevel: Int,
|
||||||
|
): List<Certificate>? {
|
||||||
|
val challenge = params.attestationChallenge
|
||||||
|
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT)
|
||||||
|
throw IllegalArgumentException(
|
||||||
|
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
|
||||||
|
)
|
||||||
|
|
||||||
|
return runCatching {
|
||||||
|
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
||||||
|
|
||||||
|
// Determine the signing key and issuer. If an attestKey is provided, use it.
|
||||||
|
// Otherwise, fall back to the root key from the keybox.
|
||||||
|
val (signingKey, issuer) =
|
||||||
|
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
|
||||||
|
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
|
||||||
|
} else {
|
||||||
|
keybox.keyPair to getIssuerFromKeybox(keybox)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the new leaf certificate with the simulated attestation.
|
||||||
|
val leafCert =
|
||||||
|
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
|
||||||
|
|
||||||
|
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
|
||||||
|
// chain.
|
||||||
|
if (attestKeyAlias != null) {
|
||||||
|
listOf(leafCert)
|
||||||
|
} else {
|
||||||
|
listOf(leafCert) + keybox.certificates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onFailure { SystemLogger.error("Failed to generate certificate chain.", it) }
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience function that combines key pair generation and certificate chain generation.
|
||||||
|
* Primarily used by the modern Keystore2 interceptor where generation is a single step.
|
||||||
*/
|
*/
|
||||||
fun generateAttestedKeyPair(
|
fun generateAttestedKeyPair(
|
||||||
uid: Int,
|
uid: Int,
|
||||||
@@ -98,30 +136,9 @@ object CertificateGenerator {
|
|||||||
generateSoftwareKeyPair(params)
|
generateSoftwareKeyPair(params)
|
||||||
?: throw Exception("Failed to generate underlying software key pair.")
|
?: throw Exception("Failed to generate underlying software key pair.")
|
||||||
|
|
||||||
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
|
||||||
|
|
||||||
// Determine the signing key and issuer. If an attestKey is provided, use it.
|
|
||||||
// Otherwise, fall back to the root key from the keybox.
|
|
||||||
val (signingKey, issuer) =
|
|
||||||
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
|
|
||||||
?: (keybox.keyPair to getIssuerFromKeybox(keybox))
|
|
||||||
} else {
|
|
||||||
keybox.keyPair to getIssuerFromKeybox(keybox)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the new leaf certificate with the simulated attestation.
|
|
||||||
val leafCert =
|
|
||||||
buildCertificate(newKeyPair, signingKey, issuer, params, securityLevel)
|
|
||||||
|
|
||||||
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
|
|
||||||
// chain.
|
|
||||||
val chain =
|
val chain =
|
||||||
if (attestKeyAlias != null) {
|
generateCertificateChain(uid, newKeyPair, attestKeyAlias, params, securityLevel)
|
||||||
listOf(leafCert)
|
?: throw Exception("Failed to generate certificate chain for new key pair.")
|
||||||
} else {
|
|
||||||
listOf(leafCert) + keybox.certificates
|
|
||||||
}
|
|
||||||
|
|
||||||
SystemLogger.info(
|
SystemLogger.info(
|
||||||
"Successfully generated new certificate chain for alias: '$alias'."
|
"Successfully generated new certificate chain for alias: '$alias'."
|
||||||
@@ -134,7 +151,7 @@ object CertificateGenerator {
|
|||||||
.getOrNull()
|
.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getIssuerFromKeybox(keybox: KeyBox) =
|
fun getIssuerFromKeybox(keybox: KeyBox) =
|
||||||
X509CertificateHolder(keybox.certificates[0].encoded).subject
|
X509CertificateHolder(keybox.certificates[0].encoded).subject
|
||||||
|
|
||||||
private fun getKeyboxForAlgorithm(uid: Int, algorithm: Int): KeyBox {
|
private fun getKeyboxForAlgorithm(uid: Int, algorithm: Int): KeyBox {
|
||||||
@@ -171,12 +188,29 @@ 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,
|
||||||
signingKeyPair: KeyPair,
|
signingKeyPair: KeyPair,
|
||||||
issuer: X500Name,
|
issuer: X500Name,
|
||||||
params: KeyMintAttestation,
|
params: KeyMintAttestation,
|
||||||
|
uid: Int,
|
||||||
securityLevel: Int,
|
securityLevel: Int,
|
||||||
): Certificate {
|
): Certificate {
|
||||||
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
|
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
|
||||||
@@ -194,10 +228,15 @@ 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(AttestationBuilder.buildAttestationExtension(params, securityLevel))
|
builder.addExtension(
|
||||||
|
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
|
||||||
|
)
|
||||||
|
|
||||||
val signerAlgorithm =
|
val signerAlgorithm =
|
||||||
when (params.algorithm) {
|
when (params.algorithm) {
|
||||||
@@ -205,7 +244,10 @@ object CertificateGenerator {
|
|||||||
Algorithm.RSA -> "SHA256withRSA"
|
Algorithm.RSA -> "SHA256withRSA"
|
||||||
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
|
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
|
||||||
}
|
}
|
||||||
val contentSigner = JcaContentSignerBuilder(signerAlgorithm).build(signingKeyPair.private)
|
val contentSigner =
|
||||||
|
JcaContentSignerBuilder(signerAlgorithm)
|
||||||
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
.build(signingKeyPair.private)
|
||||||
|
|
||||||
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package org.matrix.TEESimulator.pki
|
|||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.StringReader
|
import java.io.StringReader
|
||||||
|
import java.security.interfaces.ECPrivateKey
|
||||||
|
import java.security.interfaces.RSAPrivateKey
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
|
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
@@ -51,6 +53,9 @@ object KeyBoxManager {
|
|||||||
// If it's not in the cache, the `getOrPut` block is executed to parse and store it.
|
// If it's not in the cache, the `getOrPut` block is executed to parse and store it.
|
||||||
val keyMap =
|
val keyMap =
|
||||||
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
|
keyStoreCache.getOrPut(keyStoreFileName) { parseKeyStoreFile(keyStoreFileName) }
|
||||||
|
SystemLogger.verbose(
|
||||||
|
"Fetching attestation key in $keyStoreFileName with $algorithm algorithm."
|
||||||
|
)
|
||||||
return keyMap[algorithm]
|
return keyMap[algorithm]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,10 +162,10 @@ object KeyBoxManager {
|
|||||||
// Use runCatching to ensure one malformed key doesn't stop the whole
|
// Use runCatching to ensure one malformed key doesn't stop the whole
|
||||||
// process.
|
// process.
|
||||||
runCatching {
|
runCatching {
|
||||||
val algorithm = currentAlgorithm
|
val xmlAlgorithm = currentAlgorithm
|
||||||
val keyPem = currentPrivateKeyPem
|
val keyPem = currentPrivateKeyPem
|
||||||
if (
|
if (
|
||||||
algorithm != null &&
|
xmlAlgorithm != null &&
|
||||||
keyPem != null &&
|
keyPem != null &&
|
||||||
currentCertificatePems.isNotEmpty()
|
currentCertificatePems.isNotEmpty()
|
||||||
) {
|
) {
|
||||||
@@ -176,21 +181,42 @@ object KeyBoxManager {
|
|||||||
.data
|
.data
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize the algorithm name for consistent lookups.
|
// Derive the TRUE algorithm from the key object itself.
|
||||||
val normalizedAlgorithm =
|
// This is our source of truth.
|
||||||
when (algorithm.lowercase()) {
|
val derivedAlgorithm =
|
||||||
"ecdsa" -> KeyProperties.KEY_ALGORITHM_EC
|
when (keyPair.private) {
|
||||||
"rsa" -> KeyProperties.KEY_ALGORITHM_RSA
|
is RSAPrivateKey -> KeyProperties.KEY_ALGORITHM_RSA
|
||||||
else -> algorithm
|
is ECPrivateKey -> KeyProperties.KEY_ALGORITHM_EC
|
||||||
}
|
else ->
|
||||||
|
throw IllegalArgumentException(
|
||||||
if (foundKeys.containsKey(normalizedAlgorithm)) {
|
"Unsupported key type found: ${keyPair.private.javaClass.name}"
|
||||||
SystemLogger.warning(
|
|
||||||
"Duplicate key found for algorithm '$normalizedAlgorithm'. The later one in the file will be used."
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
foundKeys[normalizedAlgorithm] =
|
|
||||||
KeyBox(keyPair, certificates)
|
// Normalize the algorithm from the XML tag to compare it
|
||||||
|
// fairly with the derived algorithm.
|
||||||
|
val normalizedXmlAlgorithm =
|
||||||
|
when {
|
||||||
|
xmlAlgorithm.contains("RSA", ignoreCase = true) ==
|
||||||
|
true -> KeyProperties.KEY_ALGORITHM_RSA
|
||||||
|
xmlAlgorithm.contains("EC", ignoreCase = true) ==
|
||||||
|
true -> KeyProperties.KEY_ALGORITHM_EC
|
||||||
|
else -> xmlAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn the user if the XML tag was misleading.
|
||||||
|
if (normalizedXmlAlgorithm != derivedAlgorithm) {
|
||||||
|
SystemLogger.warning(
|
||||||
|
"Key algorithm mismatch in XML file. Tag said '$xmlAlgorithm' but key is actually '$derivedAlgorithm'. Using the correct derived algorithm."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundKeys.containsKey(derivedAlgorithm)) {
|
||||||
|
SystemLogger.warning(
|
||||||
|
"Duplicate key found for algorithm '$derivedAlgorithm'. The later one in the file will be used."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
foundKeys[derivedAlgorithm] = KeyBox(keyPair, certificates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
package org.matrix.TEESimulator.util
|
package org.matrix.TEESimulator.util
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.hardware.security.keymint.SecurityLevel
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.SystemProperties
|
import android.os.SystemProperties
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
import java.time.LocalDate
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
|
import org.bouncycastle.asn1.ASN1EncodableVector
|
||||||
import org.bouncycastle.asn1.ASN1Integer
|
import org.bouncycastle.asn1.ASN1Integer
|
||||||
import org.bouncycastle.asn1.DEROctetString
|
import org.bouncycastle.asn1.DEROctetString
|
||||||
import org.bouncycastle.asn1.DERSequence
|
import org.bouncycastle.asn1.DERSequence
|
||||||
|
import org.bouncycastle.asn1.DERSet
|
||||||
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
@@ -18,69 +22,138 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
|||||||
*/
|
*/
|
||||||
object AndroidDeviceUtils {
|
object AndroidDeviceUtils {
|
||||||
|
|
||||||
/** A randomly generated boot key, used as a fallback for attestation. */
|
|
||||||
val bootKey: ByteArray by lazy { generateRandomBytes(32) }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the verified boot hash (`ro.boot.vbmeta.digest`). It attempts to read from system
|
* Internal constant to signify that a patch level should not be included in the attestation.
|
||||||
* properties first, then from a real TEE attestation, and finally falls back to a random value
|
|
||||||
* if neither is available.
|
|
||||||
*/
|
*/
|
||||||
fun setupBootHash() {
|
internal const val DO_NOT_REPORT = -1
|
||||||
getBootHashFromProperty()?.also {
|
|
||||||
SystemLogger.debug("Using boot hash from system property: ${it.toHex()}")
|
// --- Boot Key and Verified Boot Hash ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazily initializes and retrieves the verified boot key digest. The value is sourced in the
|
||||||
|
* following order:
|
||||||
|
* 1. From the `ro.boot.vbmeta.public_key_digest` system property.
|
||||||
|
* 2. From a cached TEE attestation record.
|
||||||
|
* 3. As a randomly generated 32-byte value (fallback).
|
||||||
|
*/
|
||||||
|
val bootKey: ByteArray by lazy {
|
||||||
|
initializeBootProperty(
|
||||||
|
propertyName = "ro.boot.vbmeta.public_key_digest",
|
||||||
|
attestationValueProvider = {
|
||||||
|
DeviceAttestationService.CachedAttestationData?.verifiedBootKey
|
||||||
|
},
|
||||||
|
expectedSize = 32,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
?: getBootHashFromAttestation()?.also {
|
|
||||||
SystemLogger.debug("Using boot hash from TEE attestation: ${it.toHex()}")
|
/**
|
||||||
setBootHashProperty(it)
|
* Lazily initializes and retrieves the verified boot hash (vbmeta digest). The value is sourced
|
||||||
|
* in the following order:
|
||||||
|
* 1. From the `ro.boot.vbmeta.digest` system property.
|
||||||
|
* 2. From a cached TEE attestation record.
|
||||||
|
* 3. As a randomly generated 32-byte value (fallback).
|
||||||
|
*/
|
||||||
|
val bootHash: ByteArray by lazy {
|
||||||
|
initializeBootProperty(
|
||||||
|
propertyName = "ro.boot.vbmeta.digest",
|
||||||
|
attestationValueProvider = {
|
||||||
|
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
|
||||||
|
},
|
||||||
|
expectedSize = 32,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
?: generateRandomBytes(32).also {
|
|
||||||
SystemLogger.debug("Using randomly generated boot hash: ${it.toHex()}")
|
/**
|
||||||
setBootHashProperty(it)
|
* Public function to explicitly trigger the initialization of the boot key and hash. Accessing
|
||||||
|
* these properties here ensures they are set up before they might be needed elsewhere.
|
||||||
|
*/
|
||||||
|
fun setupBootKeyAndHash() {
|
||||||
|
SystemLogger.debug("Triggering initialization of boot key and hash...")
|
||||||
|
// Accessing the properties will trigger their `lazy` initialization logic.
|
||||||
|
bootKey
|
||||||
|
bootHash
|
||||||
|
SystemLogger.debug("Boot key and hash initialization complete.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic initializer for boot properties like the key and hash. It attempts to read from a
|
||||||
|
* system property first, then from a TEE attestation, and finally falls back to a random value
|
||||||
|
* if neither is available.
|
||||||
|
*
|
||||||
|
* @param propertyName The name of the system property (e.g., "ro.boot.vbmeta.digest").
|
||||||
|
* @param attestationValueProvider A function that supplies the value from a cached attestation.
|
||||||
|
* @param expectedSize The expected length of the byte array (e.g., 32 for a SHA-256 digest).
|
||||||
|
* @return The resulting byte array for the property.
|
||||||
|
*/
|
||||||
|
private fun initializeBootProperty(
|
||||||
|
propertyName: String,
|
||||||
|
attestationValueProvider: () -> ByteArray?,
|
||||||
|
expectedSize: Int,
|
||||||
|
): ByteArray {
|
||||||
|
// 1. Attempt to get the value from the system property.
|
||||||
|
getProperty(propertyName, expectedSize)?.let {
|
||||||
|
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to the value from a cached TEE attestation.
|
||||||
|
try {
|
||||||
|
attestationValueProvider()?.let {
|
||||||
|
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
|
||||||
|
setProperty(propertyName, it) // Persist for consistency
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SystemLogger.error("Failed to get $propertyName from attestation.", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. As a final fallback, generate a random value.
|
||||||
|
return generateRandomBytes(expectedSize).also {
|
||||||
|
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
||||||
|
setProperty(propertyName, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the verified boot meta digest from system properties.
|
* Retrieves a system property and validates its format.
|
||||||
*
|
*
|
||||||
* @return The boot hash as a ByteArray, or null if not found or invalid.
|
* @param name The name of the system property.
|
||||||
|
* @param expectedSize The expected byte length of the property (e.g., 32 for a 64-char hex
|
||||||
|
* string).
|
||||||
|
* @return The property value as a ByteArray, or null if not found or invalid.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
fun getBootHashFromProperty(): ByteArray? {
|
private fun getProperty(name: String, expectedSize: Int): ByteArray? {
|
||||||
val digest = SystemProperties.get("ro.boot.vbmeta.digest", null)
|
val value = SystemProperties.get(name, null)
|
||||||
if (digest.isNullOrBlank()) {
|
if (value.isNullOrBlank()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
// A valid digest is 64 hex characters (32 bytes).
|
// A valid digest is (2 * size) hex characters.
|
||||||
return if (digest.length == 64) digest.hexToByteArray() else null
|
return if (value.length == expectedSize * 2) value.hexToByteArray() else null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the verified boot hash from a cached TEE attestation record.
|
* Sets a system property using the `resetprop` command.
|
||||||
*
|
*
|
||||||
* @return The verified boot hash, or null if not available.
|
* @param name The name of the property to set.
|
||||||
|
* @param bytes The value to set, which will be converted to a hex string.
|
||||||
*/
|
*/
|
||||||
private fun getBootHashFromAttestation(): ByteArray? {
|
private fun setProperty(name: String, bytes: ByteArray) {
|
||||||
return try {
|
|
||||||
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
|
|
||||||
} catch (e: Exception) {
|
|
||||||
SystemLogger.error("Failed to get boot hash from attestation.", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the `ro.boot.vbmeta.digest` system property.
|
|
||||||
*
|
|
||||||
* @param bytes The 32-byte digest to set.
|
|
||||||
*/
|
|
||||||
private fun setBootHashProperty(bytes: ByteArray) {
|
|
||||||
val hex = bytes.toHex()
|
val hex = bytes.toHex()
|
||||||
try {
|
try {
|
||||||
SystemLogger.debug("Setting system property 'ro.boot.vbmeta.digest' to: $hex")
|
SystemLogger.debug("Setting system property '$name' to: $hex")
|
||||||
SystemProperties.set("ro.boot.vbmeta.digest", hex)
|
val command = arrayOf("resetprop", name, hex)
|
||||||
|
val process = Runtime.getRuntime().exec(command)
|
||||||
|
val exitCode = process.waitFor()
|
||||||
|
|
||||||
|
if (exitCode != 0) {
|
||||||
|
val errorOutput = process.errorStream.bufferedReader().readText()
|
||||||
|
SystemLogger.error(
|
||||||
|
"resetprop for '$name' failed with exit code $exitCode: $errorOutput"
|
||||||
|
)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SystemLogger.error("Failed to set vbmeta digest property.", e)
|
SystemLogger.error("Failed to set '$name' property via resetprop.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,30 +163,70 @@ object AndroidDeviceUtils {
|
|||||||
|
|
||||||
// --- Patch Level Properties ---
|
// --- Patch Level Properties ---
|
||||||
|
|
||||||
val patchLevel: Int
|
fun getPatchLevel(uid: Int): Int {
|
||||||
get() =
|
val custom = getCustomPatchLevelFor(uid, "system", isLong = false)
|
||||||
getCustomPatchLevelFor("system", isLong = false)
|
return custom ?: getRealDevicePatchLevelInt("system", isLong = false)
|
||||||
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false)
|
}
|
||||||
|
|
||||||
val vendorPatchLevel: Int
|
fun getVendorPatchLevelLong(uid: Int): Int {
|
||||||
get() =
|
val custom = getCustomPatchLevelFor(uid, "vendor", isLong = true)
|
||||||
getCustomPatchLevelFor("vendor", isLong = false)
|
return custom ?: getRealDevicePatchLevelInt("vendor", isLong = true)
|
||||||
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = false)
|
}
|
||||||
|
|
||||||
val bootPatchLevelLong: Int
|
fun getBootPatchLevelLong(uid: Int): Int {
|
||||||
get() =
|
val custom = getCustomPatchLevelFor(uid, "boot", isLong = true)
|
||||||
getCustomPatchLevelFor("boot", isLong = true)
|
return custom ?: getRealDevicePatchLevelInt("boot", isLong = true)
|
||||||
?: Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong = true)
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a custom patch level from the configuration if available.
|
* Retrieves the definitive device patch level integer for a given component. This function
|
||||||
|
* encapsulates the entire fallback chain and guarantees a non-null return.
|
||||||
*
|
*
|
||||||
|
* Fallback Priority:
|
||||||
|
* 1. Cached TEE attestation data.
|
||||||
|
* 2. Specific system property (e.g., ro.vendor.build.security_patch).
|
||||||
|
* 3. Default system patch level from Build.VERSION.SECURITY_PATCH.
|
||||||
|
*
|
||||||
|
* @param component The component ("system", "vendor", "boot").
|
||||||
|
* @param isLong Whether the final integer should be in YYYYMMDD format.
|
||||||
|
* @return The patch level as a guaranteed non-null Integer.
|
||||||
|
*/
|
||||||
|
private fun getRealDevicePatchLevelInt(component: String, isLong: Boolean): Int {
|
||||||
|
// Get value from cached TEE attestation data
|
||||||
|
DeviceAttestationService.CachedAttestationData?.let { data ->
|
||||||
|
val value =
|
||||||
|
when (component) {
|
||||||
|
"system" -> data.osPatchLevel
|
||||||
|
"vendor" -> data.vendorPatchLevel
|
||||||
|
"boot" -> data.bootPatchLevel
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (value != null) return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only check the specific vendor property, as the boot one is non-existent.
|
||||||
|
if (component == "vendor") {
|
||||||
|
val propValue = SystemProperties.get("ro.vendor.build.security_patch", "")
|
||||||
|
if (!propValue.isNullOrBlank()) {
|
||||||
|
parsePatchLevelValue(propValue, isLong)?.let { parsedValue ->
|
||||||
|
return parsedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Build.VERSION.SECURITY_PATCH.toPatchLevelInt(isLong)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a custom patch level from the configuration if available for a specific UID.
|
||||||
|
*
|
||||||
|
* @param uid The UID of the calling application.
|
||||||
* @param component The component to get the patch level for ("system", "vendor", "boot").
|
* @param component The component to get the patch level for ("system", "vendor", "boot").
|
||||||
* @param isLong Whether to return the patch level in `YYYYMMDD` or `YYYYMM` format.
|
* @param isLong Whether to return the patch level in `YYYYMMDD` or `YYYYMM` format.
|
||||||
* @return The custom patch level, or null if not configured.
|
* @return The custom patch level, or null if not configured.
|
||||||
*/
|
*/
|
||||||
private fun getCustomPatchLevelFor(component: String, isLong: Boolean): Int? {
|
private fun getCustomPatchLevelFor(uid: Int, component: String, isLong: Boolean): Int? {
|
||||||
val config = ConfigurationManager.customPatchLevelOverride ?: return null
|
val config = ConfigurationManager.getPatchLevelForUid(uid) ?: return null
|
||||||
val value =
|
val value =
|
||||||
when (component) {
|
when (component) {
|
||||||
"system" -> config.system ?: config.all
|
"system" -> config.system ?: config.all
|
||||||
@@ -122,11 +235,49 @@ object AndroidDeviceUtils {
|
|||||||
else -> config.all
|
else -> config.all
|
||||||
} ?: return null
|
} ?: return null
|
||||||
|
|
||||||
// "prop" or "no" indicates falling back to the system default.
|
// First, resolve dynamic keywords and templates into a concrete date string.
|
||||||
if (value.equals("no", ignoreCase = true) || value.equals("prop", ignoreCase = true)) {
|
val resolvedValue = resolveDateKeywords(value)
|
||||||
return null
|
|
||||||
|
return when {
|
||||||
|
resolvedValue.equals("device_default", ignoreCase = true) -> null
|
||||||
|
// 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
|
||||||
|
else -> parsePatchLevelValue(resolvedValue, isLong)
|
||||||
}
|
}
|
||||||
return parsePatchLevelValue(value, isLong)
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves special date keywords and templates into a concrete "YYYY-MM-DD" date string.
|
||||||
|
*
|
||||||
|
* @param value The configuration value string (e.g., "today", "YYYY-MM-01").
|
||||||
|
* @return A concrete date string, or the original value if it's not a dynamic date keyword.
|
||||||
|
*/
|
||||||
|
private fun resolveDateKeywords(value: String): String {
|
||||||
|
// Handle the "today" keyword.
|
||||||
|
if (value.equals("today", ignoreCase = true)) {
|
||||||
|
return LocalDate.now().toString() // Returns "YYYY-MM-DD" format
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle date templates like "YYYY-MM-01" or "2025-MM-DD".
|
||||||
|
if (
|
||||||
|
value.contains("YYYY", ignoreCase = true) ||
|
||||||
|
value.contains("MM", ignoreCase = true) ||
|
||||||
|
value.contains("DD", ignoreCase = true)
|
||||||
|
) {
|
||||||
|
|
||||||
|
val now = LocalDate.now()
|
||||||
|
// Chain replacements for YYYY, MM, and DD placeholders.
|
||||||
|
return value
|
||||||
|
.replace("YYYY", now.year.toString(), ignoreCase = true)
|
||||||
|
.replace("MM", String.format("%02d", now.monthValue), ignoreCase = true)
|
||||||
|
.replace("DD", String.format("%02d", now.dayOfMonth), ignoreCase = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not a dynamic keyword or template, return the original value.
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parses a patch level string (e.g., "2025-11-01") into an integer format. */
|
/** Parses a patch level string (e.g., "2025-11-01") into an integer format. */
|
||||||
@@ -190,17 +341,33 @@ object AndroidDeviceUtils {
|
|||||||
Build.VERSION_CODES.BAKLAVA to 400, // KeyMint 4.0
|
Build.VERSION_CODES.BAKLAVA to 400, // KeyMint 4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
val attestVersion: Int
|
/**
|
||||||
get() =
|
* Retrieves the attestation version based on security level and OS version. StrongBox (level 2)
|
||||||
DeviceAttestationService.CachedAttestationData?.attestVersion
|
* requires version 300.
|
||||||
|
*
|
||||||
|
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
|
||||||
|
* @return The appropriate attestation version number.
|
||||||
|
*/
|
||||||
|
fun getAttestVersion(securityLevel: Int): Int {
|
||||||
|
// StrongBox security level requires an attestation version of at least 300.
|
||||||
|
if (securityLevel == SecurityLevel.STRONGBOX) {
|
||||||
|
return 300
|
||||||
|
}
|
||||||
|
return DeviceAttestationService.CachedAttestationData?.attestVersion
|
||||||
?: attestVersionMap[Build.VERSION.SDK_INT]
|
?: attestVersionMap[Build.VERSION.SDK_INT]
|
||||||
?: 400 // Default to a recent version
|
?: 400 // Default to a recent version
|
||||||
|
}
|
||||||
|
|
||||||
val keymasterVersion: Int
|
/**
|
||||||
get() =
|
* Retrieves the Keymaster/KeyMint version based on the attestation version.
|
||||||
DeviceAttestationService.CachedAttestationData?.keymasterVersion
|
*
|
||||||
?: if (attestVersion >= 100) attestVersion
|
* @param securityLevel The security level, used to determine the correct attestation version.
|
||||||
else 41 // Keymaster 4.1 for older versions
|
* @return The appropriate Keymaster or KeyMint version number.
|
||||||
|
*/
|
||||||
|
fun getKeymasterVersion(securityLevel: Int): Int {
|
||||||
|
val attestVersion = getAttestVersion(securityLevel)
|
||||||
|
return if (attestVersion >= 100) attestVersion else 41 // Keymaster 4.1 for older versions
|
||||||
|
}
|
||||||
|
|
||||||
// --- APEX and Module Hash Properties ---
|
// --- APEX and Module Hash Properties ---
|
||||||
|
|
||||||
@@ -214,11 +381,7 @@ object AndroidDeviceUtils {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
|
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
|
||||||
}
|
}
|
||||||
packages
|
packages?.list.orEmpty().map { it.packageName to it.longVersionCode }
|
||||||
?.list
|
|
||||||
.orEmpty()
|
|
||||||
.map { it.packageName to it.longVersionCode }
|
|
||||||
.sortedBy { it.first }
|
|
||||||
}
|
}
|
||||||
.getOrElse {
|
.getOrElse {
|
||||||
SystemLogger.error("Failed to get APEX package information.", it)
|
SystemLogger.error("Failed to get APEX package information.", it)
|
||||||
@@ -227,13 +390,29 @@ object AndroidDeviceUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val moduleHash: ByteArray by lazy {
|
val moduleHash: ByteArray by lazy {
|
||||||
runCatching {
|
DeviceAttestationService.CachedAttestationData?.moduleHash
|
||||||
val encodables =
|
?: runCatching {
|
||||||
apexInfos.flatMap { (packageName, versionCode) ->
|
// TODO: figure out the correct calculation
|
||||||
listOf(DEROctetString(packageName.toByteArray()), ASN1Integer(versionCode))
|
val moduleSequences = ASN1EncodableVector()
|
||||||
|
|
||||||
|
// 1. Create a DERSequence for each module.
|
||||||
|
apexInfos.forEach { (packageName, versionCode) ->
|
||||||
|
val moduleVector = ASN1EncodableVector()
|
||||||
|
// Use explicit UTF-8 encoding for the package name.
|
||||||
|
moduleVector.add(DEROctetString(packageName.toByteArray(Charsets.UTF_8)))
|
||||||
|
moduleVector.add(ASN1Integer(versionCode))
|
||||||
|
moduleSequences.add(DERSequence(moduleVector))
|
||||||
}
|
}
|
||||||
val sequence = DERSequence(encodables.toTypedArray())
|
|
||||||
MessageDigest.getInstance("SHA-256").digest(sequence.encoded)
|
// 2. Create a DERSet. Bouncy Castle will automatically handle
|
||||||
|
// the sorting based on the DER-encoded value of each sequence.
|
||||||
|
val modulesSet = DERSet(moduleSequences)
|
||||||
|
|
||||||
|
// 3. Get the final DER-encoded byte array of the SET.
|
||||||
|
val encodedModules = modulesSet.encoded
|
||||||
|
|
||||||
|
// 4. Compute the SHA-256 hash.
|
||||||
|
MessageDigest.getInstance("SHA-256").digest(encodedModules)
|
||||||
}
|
}
|
||||||
.getOrElse {
|
.getOrElse {
|
||||||
SystemLogger.error("Failed to compute module hash.", it)
|
SystemLogger.error("Failed to compute module hash.", it)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.13.1"
|
agp = "8.13.2"
|
||||||
annotation = "1.9.1"
|
annotation = "1.9.1"
|
||||||
jdk18on = "1.82"
|
jdk18on = "1.83"
|
||||||
kotlin = "2.2.21"
|
kotlin = "2.3.0"
|
||||||
ktfmt = "0.25.0"
|
ktfmt = "0.25.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|||||||
@@ -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
-10
@@ -1,19 +1,43 @@
|
|||||||
**Key Highlights:**
|
## TEESimulator v3.2: Anti-Detection Hardening & Key Persistence
|
||||||
|
|
||||||
* 🚀 **Complete Refactoring:** TEESimulator v2.0 has been entirely rebuilt and is no longer based on its predecessors, [TrickyStore](https://github.com/5ec1cff/TrickyStore) and [TrickyStoreOSS](https://github.com/beakthoven/TrickyStoreOSS), resulting in a more streamlined and maintainable codebase.
|
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 Bypass Capabilities:** The simulator now successfully bypasses well-known detection mechanisms, including [TamperedAttestation](https://github.com/JingMatrix/TamperedAttestation) and [KeyAttestation](https://github.com/JingMatrix/KeyAttestation).
|
### Anti-Detection Hardening
|
||||||
|
|
||||||
* 💳 **Revolut Detection Bypass:** With a valid keybox, users can now circumvent the detection measures implemented in the [Revolut](https://play.google.com/store/apps/details?id=com.revolut.revolut) application.
|
* **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.
|
||||||
|
|
||||||
**Current Limitations:**
|
### Security Patch Consistency
|
||||||
|
|
||||||
* ⚠️ **Google Play Verdict:** Bypassing the detections within the Google Play verdict remains an unresolved challenge. We are actively seeking solutions and welcome any insights from the community regarding potential system module-based bypasses.
|
* **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`.
|
||||||
|
|
||||||
**Platform Support:**
|
### Key Persistence
|
||||||
|
|
||||||
* 📱 **Android 10 & 11:** TEESimulator v2.0 has not yet been tested on Android 10 or 11. We encourage users on these platforms to report any issues and provide logs to help us improve compatibility.
|
* **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.
|
||||||
|
|
||||||
**Contributing:**
|
### 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 welcome and encourage community contributions. Please feel free to submit issues and pull requests to help improve the project.
|
|
||||||
|
|||||||
+4
-1
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
allow keystore system_file unix_dgram_socket *
|
allow keystore {adb_data_file shell_data_file} file *
|
||||||
allow system_file keystore unix_dgram_socket *
|
|
||||||
allow keystore system_file file *
|
|
||||||
allow crash_dump keystore process *
|
allow crash_dump keystore process *
|
||||||
|
|||||||
+2
-8
@@ -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 &
|
|
||||||
|
|||||||
@@ -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": "v2.0",
|
"version": "v3.2",
|
||||||
"versionCode": 14,
|
"versionCode": 82,
|
||||||
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v2.0/TEESimulator-v2.0-14-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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,12 @@ public class ActivityThread {
|
|||||||
public static void initializeMainlineModules() {
|
public static void initializeMainlineModules() {
|
||||||
throw new UnsupportedOperationException("STUB!");
|
throw new UnsupportedOperationException("STUB!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ActivityThread systemMain() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContextImpl getSystemContext() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package android.app;
|
||||||
|
|
||||||
|
public class ContextImpl {
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package android.hardware.security.keymint;
|
||||||
|
|
||||||
|
public @interface BlockMode {
|
||||||
|
public static final int ECB = 1;
|
||||||
|
public static final int CBC = 2;
|
||||||
|
public static final int CTR = 3;
|
||||||
|
public static final int GCM = 32;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package android.hardware.security.keymint;
|
||||||
|
|
||||||
|
public @interface PaddingMode {
|
||||||
|
public static final int NONE = 1;
|
||||||
|
public static final int RSA_OAEP = 2;
|
||||||
|
public static final int RSA_PSS = 3;
|
||||||
|
public static final int RSA_PKCS1_1_5_ENCRYPT = 4;
|
||||||
|
public static final int RSA_PKCS1_1_5_SIGN = 5;
|
||||||
|
public static final int PKCS7 = 64;
|
||||||
|
}
|
||||||
@@ -4,8 +4,4 @@ public class SystemProperties {
|
|||||||
public static String get(String key, String def) {
|
public static String get(String key, String def) {
|
||||||
throw new UnsupportedOperationException("STUB!");
|
throw new UnsupportedOperationException("STUB!");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String set(String key, String val) {
|
|
||||||
throw new UnsupportedOperationException("STUB!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class ExportResult implements Parcelable {
|
||||||
|
public final byte[] exportData;
|
||||||
|
public final int resultCode;
|
||||||
|
|
||||||
|
public ExportResult(int resultCode) {
|
||||||
|
this.resultCode = resultCode;
|
||||||
|
this.exportData = new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<ExportResult> CREATOR = new Creator<ExportResult>() {
|
||||||
|
@Override
|
||||||
|
public ExportResult createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExportResult[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class KeyCharacteristics implements Parcelable {
|
||||||
|
public KeymasterArguments hwEnforced;
|
||||||
|
public KeymasterArguments swEnforced;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<KeyCharacteristics> CREATOR = new Creator<KeyCharacteristics>() {
|
||||||
|
@Override
|
||||||
|
public KeyCharacteristics createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyCharacteristics[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
abstract class KeymasterArgument implements Parcelable {
|
||||||
|
public final int tag;
|
||||||
|
|
||||||
|
protected KeymasterArgument(int tag) {
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<KeymasterArgument> CREATOR = new Creator<KeymasterArgument>() {
|
||||||
|
@Override
|
||||||
|
public KeymasterArgument createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeymasterArgument[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class KeymasterArguments implements Parcelable {
|
||||||
|
|
||||||
|
private static final long UINT32_RANGE = 1L << 32;
|
||||||
|
public static final long UINT32_MAX_VALUE = UINT32_RANGE - 1;
|
||||||
|
|
||||||
|
private static final BigInteger UINT64_RANGE = BigInteger.ONE.shiftLeft(64);
|
||||||
|
public static final BigInteger UINT64_MAX_VALUE = UINT64_RANGE.subtract(BigInteger.ONE);
|
||||||
|
|
||||||
|
private List<KeymasterArgument> mArguments;
|
||||||
|
|
||||||
|
public static final @NonNull Parcelable.Creator<KeymasterArguments> CREATOR = new Parcelable.Creator<KeymasterArguments>() {
|
||||||
|
@Override
|
||||||
|
public KeymasterArguments createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeymasterArguments[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public KeymasterArguments() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeymasterArguments(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addEnum(int tag, int value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addEnums(int tag, int... values) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEnum(int tag, int defaultValue) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Integer> getEnums(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addEnumTag(int tag, int value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getEnumTagValue(KeymasterArgument arg) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addUnsignedInt(int tag, long value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getUnsignedInt(int tag, long defaultValue) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addUnsignedLong(int tag, BigInteger value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BigInteger> getUnsignedLongs(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addLongTag(int tag, BigInteger value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigInteger getLongTagValue(KeymasterArgument arg) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addBoolean(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getBoolean(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addBytes(int tag, byte[] value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBytes(int tag, byte[] defaultValue) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDate(int tag, Date value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addDateIfNotNull(int tag, Date value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getDate(int tag, Date defaultValue) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeymasterArgument getArgumentByTag(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean containsTag(int tag) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int size() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel out, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BigInteger toUint64(long value) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class KeymasterCertificateChain implements Parcelable {
|
||||||
|
private List<byte[]> mCertificates;
|
||||||
|
|
||||||
|
public KeymasterCertificateChain() {
|
||||||
|
this.mCertificates = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeymasterCertificateChain(List<byte[]> mCertificates) {
|
||||||
|
this.mCertificates = mCertificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<KeymasterCertificateChain> CREATOR = new Creator<KeymasterCertificateChain>() {
|
||||||
|
@Override
|
||||||
|
public KeymasterCertificateChain createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeymasterCertificateChain[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package android.security.keymaster;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class KeymasterDefs {
|
||||||
|
|
||||||
|
private KeymasterDefs() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag types.
|
||||||
|
public static final int KM_INVALID = 0 << 28;
|
||||||
|
public static final int KM_ENUM = 1 << 28;
|
||||||
|
public static final int KM_ENUM_REP = 2 << 28;
|
||||||
|
public static final int KM_UINT = 3 << 28;
|
||||||
|
public static final int KM_UINT_REP = 4 << 28;
|
||||||
|
public static final int KM_ULONG = 5 << 28;
|
||||||
|
public static final int KM_DATE = 6 << 28;
|
||||||
|
public static final int KM_BOOL = 7 << 28;
|
||||||
|
public static final int KM_BIGNUM = 8 << 28;
|
||||||
|
public static final int KM_BYTES = 9 << 28;
|
||||||
|
public static final int KM_ULONG_REP = 10 << 28;
|
||||||
|
|
||||||
|
// Tag values.
|
||||||
|
public static final int KM_TAG_INVALID = KM_INVALID | 0;
|
||||||
|
public static final int KM_TAG_PURPOSE = KM_ENUM_REP | 1;
|
||||||
|
public static final int KM_TAG_ALGORITHM = KM_ENUM | 2;
|
||||||
|
public static final int KM_TAG_KEY_SIZE = KM_UINT | 3;
|
||||||
|
public static final int KM_TAG_BLOCK_MODE = KM_ENUM_REP | 4;
|
||||||
|
public static final int KM_TAG_DIGEST = KM_ENUM_REP | 5;
|
||||||
|
public static final int KM_TAG_PADDING = KM_ENUM_REP | 6;
|
||||||
|
public static final int KM_TAG_CALLER_NONCE = KM_BOOL | 7;
|
||||||
|
public static final int KM_TAG_MIN_MAC_LENGTH = KM_UINT | 8;
|
||||||
|
|
||||||
|
public static final int KM_TAG_RESCOPING_ADD = KM_ENUM_REP | 101;
|
||||||
|
public static final int KM_TAG_RESCOPING_DEL = KM_ENUM_REP | 102;
|
||||||
|
public static final int KM_TAG_BLOB_USAGE_REQUIREMENTS = KM_ENUM | 705;
|
||||||
|
|
||||||
|
public static final int KM_TAG_RSA_PUBLIC_EXPONENT = KM_ULONG | 200;
|
||||||
|
public static final int KM_TAG_INCLUDE_UNIQUE_ID = KM_BOOL | 202;
|
||||||
|
|
||||||
|
public static final int KM_TAG_ACTIVE_DATETIME = KM_DATE | 400;
|
||||||
|
public static final int KM_TAG_ORIGINATION_EXPIRE_DATETIME = KM_DATE | 401;
|
||||||
|
public static final int KM_TAG_USAGE_EXPIRE_DATETIME = KM_DATE | 402;
|
||||||
|
public static final int KM_TAG_MIN_SECONDS_BETWEEN_OPS = KM_UINT | 403;
|
||||||
|
public static final int KM_TAG_MAX_USES_PER_BOOT = KM_UINT | 404;
|
||||||
|
|
||||||
|
public static final int KM_TAG_ALL_USERS = KM_BOOL | 500;
|
||||||
|
public static final int KM_TAG_USER_ID = KM_UINT | 501;
|
||||||
|
public static final int KM_TAG_USER_SECURE_ID = KM_ULONG_REP | 502;
|
||||||
|
public static final int KM_TAG_NO_AUTH_REQUIRED = KM_BOOL | 503;
|
||||||
|
public static final int KM_TAG_USER_AUTH_TYPE = KM_ENUM | 504;
|
||||||
|
public static final int KM_TAG_AUTH_TIMEOUT = KM_UINT | 505;
|
||||||
|
public static final int KM_TAG_ALLOW_WHILE_ON_BODY = KM_BOOL | 506;
|
||||||
|
public static final int KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED = KM_BOOL | 507;
|
||||||
|
public static final int KM_TAG_TRUSTED_CONFIRMATION_REQUIRED = KM_BOOL | 508;
|
||||||
|
public static final int KM_TAG_UNLOCKED_DEVICE_REQUIRED = KM_BOOL | 509;
|
||||||
|
|
||||||
|
public static final int KM_TAG_ALL_APPLICATIONS = KM_BOOL | 600;
|
||||||
|
public static final int KM_TAG_APPLICATION_ID = KM_BYTES | 601;
|
||||||
|
|
||||||
|
public static final int KM_TAG_CREATION_DATETIME = KM_DATE | 701;
|
||||||
|
public static final int KM_TAG_ORIGIN = KM_ENUM | 702;
|
||||||
|
public static final int KM_TAG_ROLLBACK_RESISTANT = KM_BOOL | 703;
|
||||||
|
public static final int KM_TAG_ROOT_OF_TRUST = KM_BYTES | 704;
|
||||||
|
public static final int KM_TAG_UNIQUE_ID = KM_BYTES | 707;
|
||||||
|
public static final int KM_TAG_ATTESTATION_CHALLENGE = KM_BYTES | 708;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_BRAND = KM_BYTES | 710;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_DEVICE = KM_BYTES | 711;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_PRODUCT = KM_BYTES | 712;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_SERIAL = KM_BYTES | 713;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_IMEI = KM_BYTES | 714;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_MEID = KM_BYTES | 715;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_MANUFACTURER = KM_BYTES | 716;
|
||||||
|
public static final int KM_TAG_ATTESTATION_ID_MODEL = KM_BYTES | 717;
|
||||||
|
public static final int KM_TAG_DEVICE_UNIQUE_ATTESTATION = KM_BOOL | 720;
|
||||||
|
|
||||||
|
public static final int KM_TAG_ASSOCIATED_DATA = KM_BYTES | 1000;
|
||||||
|
public static final int KM_TAG_NONCE = KM_BYTES | 1001;
|
||||||
|
public static final int KM_TAG_AUTH_TOKEN = KM_BYTES | 1002;
|
||||||
|
public static final int KM_TAG_MAC_LENGTH = KM_UINT | 1003;
|
||||||
|
|
||||||
|
// Algorithm values.
|
||||||
|
public static final int KM_ALGORITHM_RSA = 1;
|
||||||
|
public static final int KM_ALGORITHM_EC = 3;
|
||||||
|
public static final int KM_ALGORITHM_AES = 32;
|
||||||
|
public static final int KM_ALGORITHM_3DES = 33;
|
||||||
|
public static final int KM_ALGORITHM_HMAC = 128;
|
||||||
|
|
||||||
|
// Block modes.
|
||||||
|
public static final int KM_MODE_ECB = 1;
|
||||||
|
public static final int KM_MODE_CBC = 2;
|
||||||
|
public static final int KM_MODE_CTR = 3;
|
||||||
|
public static final int KM_MODE_GCM = 32;
|
||||||
|
|
||||||
|
// Padding modes.
|
||||||
|
public static final int KM_PAD_NONE = 1;
|
||||||
|
public static final int KM_PAD_RSA_OAEP = 2;
|
||||||
|
public static final int KM_PAD_RSA_PSS = 3;
|
||||||
|
public static final int KM_PAD_RSA_PKCS1_1_5_ENCRYPT = 4;
|
||||||
|
public static final int KM_PAD_RSA_PKCS1_1_5_SIGN = 5;
|
||||||
|
public static final int KM_PAD_PKCS7 = 64;
|
||||||
|
|
||||||
|
// Digest modes.
|
||||||
|
public static final int KM_DIGEST_NONE = 0;
|
||||||
|
public static final int KM_DIGEST_MD5 = 1;
|
||||||
|
public static final int KM_DIGEST_SHA1 = 2;
|
||||||
|
public static final int KM_DIGEST_SHA_2_224 = 3;
|
||||||
|
public static final int KM_DIGEST_SHA_2_256 = 4;
|
||||||
|
public static final int KM_DIGEST_SHA_2_384 = 5;
|
||||||
|
public static final int KM_DIGEST_SHA_2_512 = 6;
|
||||||
|
|
||||||
|
// Key origins.
|
||||||
|
public static final int KM_ORIGIN_GENERATED = 0;
|
||||||
|
public static final int KM_ORIGIN_IMPORTED = 2;
|
||||||
|
public static final int KM_ORIGIN_UNKNOWN = 3;
|
||||||
|
public static final int KM_ORIGIN_SECURELY_IMPORTED = 4;
|
||||||
|
|
||||||
|
// Key usability requirements.
|
||||||
|
public static final int KM_BLOB_STANDALONE = 0;
|
||||||
|
public static final int KM_BLOB_REQUIRES_FILE_SYSTEM = 1;
|
||||||
|
|
||||||
|
// Operation Purposes.
|
||||||
|
public static final int KM_PURPOSE_ENCRYPT = 0;
|
||||||
|
public static final int KM_PURPOSE_DECRYPT = 1;
|
||||||
|
public static final int KM_PURPOSE_SIGN = 2;
|
||||||
|
public static final int KM_PURPOSE_VERIFY = 3;
|
||||||
|
public static final int KM_PURPOSE_WRAP = 5;
|
||||||
|
|
||||||
|
// Key formats.
|
||||||
|
public static final int KM_KEY_FORMAT_X509 = 0;
|
||||||
|
public static final int KM_KEY_FORMAT_PKCS8 = 1;
|
||||||
|
public static final int KM_KEY_FORMAT_RAW = 3;
|
||||||
|
|
||||||
|
// User authenticators.
|
||||||
|
public static final int HW_AUTH_PASSWORD = 1 << 0;
|
||||||
|
public static final int HW_AUTH_BIOMETRIC = 1 << 1;
|
||||||
|
|
||||||
|
// Error codes.
|
||||||
|
public static final int KM_ERROR_OK = 0;
|
||||||
|
public static final int KM_ERROR_ROOT_OF_TRUST_ALREADY_SET = -1;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_PURPOSE = -2;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_PURPOSE = -3;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_ALGORITHM = -4;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_ALGORITHM = -5;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_KEY_SIZE = -6;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_BLOCK_MODE = -7;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_BLOCK_MODE = -8;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_MAC_LENGTH = -9;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_PADDING_MODE = -10;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_PADDING_MODE = -11;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_DIGEST = -12;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_DIGEST = -13;
|
||||||
|
public static final int KM_ERROR_INVALID_EXPIRATION_TIME = -14;
|
||||||
|
public static final int KM_ERROR_INVALID_USER_ID = -15;
|
||||||
|
public static final int KM_ERROR_INVALID_AUTHORIZATION_TIMEOUT = -16;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_KEY_FORMAT = -17;
|
||||||
|
public static final int KM_ERROR_INCOMPATIBLE_KEY_FORMAT = -18;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_KEY_ENCRYPTION_ALGORITHM = -19;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_KEY_VERIFICATION_ALGORITHM = -20;
|
||||||
|
public static final int KM_ERROR_INVALID_INPUT_LENGTH = -21;
|
||||||
|
public static final int KM_ERROR_KEY_EXPORT_OPTIONS_INVALID = -22;
|
||||||
|
public static final int KM_ERROR_DELEGATION_NOT_ALLOWED = -23;
|
||||||
|
public static final int KM_ERROR_KEY_NOT_YET_VALID = -24;
|
||||||
|
public static final int KM_ERROR_KEY_EXPIRED = -25;
|
||||||
|
public static final int KM_ERROR_KEY_USER_NOT_AUTHENTICATED = -26;
|
||||||
|
public static final int KM_ERROR_OUTPUT_PARAMETER_NULL = -27;
|
||||||
|
public static final int KM_ERROR_INVALID_OPERATION_HANDLE = -28;
|
||||||
|
public static final int KM_ERROR_INSUFFICIENT_BUFFER_SPACE = -29;
|
||||||
|
public static final int KM_ERROR_VERIFICATION_FAILED = -30;
|
||||||
|
public static final int KM_ERROR_TOO_MANY_OPERATIONS = -31;
|
||||||
|
public static final int KM_ERROR_UNEXPECTED_NULL_POINTER = -32;
|
||||||
|
public static final int KM_ERROR_INVALID_KEY_BLOB = -33;
|
||||||
|
public static final int KM_ERROR_IMPORTED_KEY_NOT_ENCRYPTED = -34;
|
||||||
|
public static final int KM_ERROR_IMPORTED_KEY_DECRYPTION_FAILED = -35;
|
||||||
|
public static final int KM_ERROR_IMPORTED_KEY_NOT_SIGNED = -36;
|
||||||
|
public static final int KM_ERROR_IMPORTED_KEY_VERIFICATION_FAILED = -37;
|
||||||
|
public static final int KM_ERROR_INVALID_ARGUMENT = -38;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_TAG = -39;
|
||||||
|
public static final int KM_ERROR_INVALID_TAG = -40;
|
||||||
|
public static final int KM_ERROR_MEMORY_ALLOCATION_FAILED = -41;
|
||||||
|
public static final int KM_ERROR_INVALID_RESCOPING = -42;
|
||||||
|
public static final int KM_ERROR_IMPORT_PARAMETER_MISMATCH = -44;
|
||||||
|
public static final int KM_ERROR_SECURE_HW_ACCESS_DENIED = -45;
|
||||||
|
public static final int KM_ERROR_OPERATION_CANCELLED = -46;
|
||||||
|
public static final int KM_ERROR_CONCURRENT_ACCESS_CONFLICT = -47;
|
||||||
|
public static final int KM_ERROR_SECURE_HW_BUSY = -48;
|
||||||
|
public static final int KM_ERROR_SECURE_HW_COMMUNICATION_FAILED = -49;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_EC_FIELD = -50;
|
||||||
|
public static final int KM_ERROR_MISSING_NONCE = -51;
|
||||||
|
public static final int KM_ERROR_INVALID_NONCE = -52;
|
||||||
|
public static final int KM_ERROR_MISSING_MAC_LENGTH = -53;
|
||||||
|
public static final int KM_ERROR_KEY_RATE_LIMIT_EXCEEDED = -54;
|
||||||
|
public static final int KM_ERROR_CALLER_NONCE_PROHIBITED = -55;
|
||||||
|
public static final int KM_ERROR_KEY_MAX_OPS_EXCEEDED = -56;
|
||||||
|
public static final int KM_ERROR_INVALID_MAC_LENGTH = -57;
|
||||||
|
public static final int KM_ERROR_MISSING_MIN_MAC_LENGTH = -58;
|
||||||
|
public static final int KM_ERROR_UNSUPPORTED_MIN_MAC_LENGTH = -59;
|
||||||
|
public static final int KM_ERROR_CANNOT_ATTEST_IDS = -66;
|
||||||
|
public static final int KM_ERROR_DEVICE_LOCKED = -72;
|
||||||
|
public static final int KM_ERROR_UNIMPLEMENTED = -100;
|
||||||
|
public static final int KM_ERROR_VERSION_MISMATCH = -101;
|
||||||
|
public static final int KM_ERROR_UNKNOWN_ERROR = -1000;
|
||||||
|
|
||||||
|
public static final Map<Integer, String> sErrorCodeToString = new HashMap<Integer, String>();
|
||||||
|
static {
|
||||||
|
sErrorCodeToString.put(KM_ERROR_OK, "OK");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_PURPOSE, "Unsupported purpose");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_PURPOSE, "Incompatible purpose");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_ALGORITHM, "Unsupported algorithm");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_ALGORITHM, "Incompatible algorithm");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_KEY_SIZE, "Unsupported key size");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_BLOCK_MODE, "Unsupported block mode");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_BLOCK_MODE, "Incompatible block mode");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_MAC_LENGTH,
|
||||||
|
"Unsupported MAC or authentication tag length");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_PADDING_MODE, "Unsupported padding mode");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_PADDING_MODE, "Incompatible padding mode");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_DIGEST, "Unsupported digest");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_DIGEST, "Incompatible digest");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_EXPIRATION_TIME, "Invalid expiration time");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_USER_ID, "Invalid user ID");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_AUTHORIZATION_TIMEOUT,
|
||||||
|
"Invalid user authorization timeout");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_KEY_FORMAT, "Unsupported key format");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_KEY_FORMAT, "Incompatible key format");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_INPUT_LENGTH, "Invalid input length");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_KEY_NOT_YET_VALID, "Key not yet valid");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_KEY_EXPIRED, "Key expired");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_KEY_USER_NOT_AUTHENTICATED, "Key user not authenticated");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_OPERATION_HANDLE, "Invalid operation handle");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_VERIFICATION_FAILED, "Signature/MAC verification failed");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_TOO_MANY_OPERATIONS, "Too many operations");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_KEY_BLOB, "Invalid key blob");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_ARGUMENT, "Invalid argument");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_TAG, "Unsupported tag");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_TAG, "Invalid tag");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_MEMORY_ALLOCATION_FAILED, "Memory allocation failed");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_EC_FIELD, "Unsupported EC field");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_MISSING_NONCE, "Required IV missing");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_NONCE, "Invalid IV");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_CALLER_NONCE_PROHIBITED,
|
||||||
|
"Caller-provided IV not permitted");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_INVALID_MAC_LENGTH,
|
||||||
|
"Invalid MAC or authentication tag length");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_CANNOT_ATTEST_IDS, "Unable to attest device ids");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_DEVICE_LOCKED, "Device locked");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNIMPLEMENTED, "Not implemented");
|
||||||
|
sErrorCodeToString.put(KM_ERROR_UNKNOWN_ERROR, "Unknown error");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getTagType(int tag) {
|
||||||
|
return tag & (0xF << 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getErrorMessage(int errorCode) {
|
||||||
|
String result = sErrorCodeToString.get(errorCode);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return String.valueOf(errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package android.security.keystore;
|
||||||
|
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.security.keymaster.KeymasterCertificateChain;
|
||||||
|
|
||||||
|
public interface IKeystoreCertificateChainCallback {
|
||||||
|
void onFinished(KeystoreResponse keystoreResponse, KeymasterCertificateChain keymasterCertificateChain)
|
||||||
|
throws RemoteException;
|
||||||
|
|
||||||
|
public static abstract class Stub {
|
||||||
|
public static IKeystoreCertificateChainCallback asInterface(IBinder b) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package android.security.keystore;
|
||||||
|
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.security.keymaster.ExportResult;
|
||||||
|
|
||||||
|
public interface IKeystoreExportKeyCallback {
|
||||||
|
void onFinished(ExportResult exportResult) throws RemoteException;
|
||||||
|
|
||||||
|
public static abstract class Stub {
|
||||||
|
public static IKeystoreExportKeyCallback asInterface(IBinder b) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package android.security.keystore;
|
||||||
|
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.IInterface;
|
||||||
|
import android.os.RemoteException;
|
||||||
|
import android.security.keymaster.KeyCharacteristics;
|
||||||
|
|
||||||
|
public interface IKeystoreKeyCharacteristicsCallback extends IInterface {
|
||||||
|
void onFinished(KeystoreResponse keystoreResponse, KeyCharacteristics keyCharacteristics) throws RemoteException;
|
||||||
|
|
||||||
|
public static abstract class Stub {
|
||||||
|
public static IKeystoreKeyCharacteristicsCallback asInterface(IBinder b) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package android.security.keystore;
|
package android.security.keystore;
|
||||||
|
|
||||||
|
import java.lang.String;
|
||||||
|
|
||||||
public interface IKeystoreService {
|
public interface IKeystoreService {
|
||||||
|
public static final String DESCRIPTOR = "android.security.keystore.IKeystoreService";
|
||||||
|
|
||||||
class Stub {
|
class Stub {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package android.security.keystore;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class KeystoreResponse implements Parcelable {
|
||||||
|
public final int error_code_;
|
||||||
|
public final String error_msg_;
|
||||||
|
|
||||||
|
protected KeystoreResponse(int error_code, String error_msg) {
|
||||||
|
this.error_code_ = error_code;
|
||||||
|
this.error_msg_ = error_msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<KeystoreResponse> CREATOR = new Creator<KeystoreResponse>() {
|
||||||
|
@Override
|
||||||
|
public KeystoreResponse createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeystoreResponse[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package android.system.keystore2;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class CreateOperationResponse implements Parcelable {
|
||||||
|
public IKeystoreOperation iOperation;
|
||||||
|
|
||||||
|
public OperationChallenge operationChallenge;
|
||||||
|
|
||||||
|
public KeyParameters parameters;
|
||||||
|
|
||||||
|
public byte[] upgradedBlob;
|
||||||
|
|
||||||
|
public static final Creator<CreateOperationResponse> CREATOR = new Creator<CreateOperationResponse>() {
|
||||||
|
@Override
|
||||||
|
public CreateOperationResponse createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CreateOperationResponse[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel parcel, int i) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package android.system.keystore2;
|
||||||
|
|
||||||
|
public @interface Domain {
|
||||||
|
public static final int APP = 0;
|
||||||
|
public static final int GRANT = 1;
|
||||||
|
public static final int SELINUX = 2;
|
||||||
|
public static final int BLOB = 3;
|
||||||
|
public static final int KEY_ID = 4;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package android.system.keystore2;
|
||||||
|
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.Binder;
|
||||||
|
import android.os.IInterface;
|
||||||
|
|
||||||
|
public interface IKeystoreOperation extends IInterface {
|
||||||
|
public static final java.lang.String DESCRIPTOR = "android.system.keystore2.IKeystoreOperation";
|
||||||
|
|
||||||
|
public void updateAad(byte[] aadInput);
|
||||||
|
|
||||||
|
public byte[] update(byte[] input);
|
||||||
|
|
||||||
|
public byte[] finish(byte[] input, byte[] signature);
|
||||||
|
|
||||||
|
public void abort() throws android.os.RemoteException;
|
||||||
|
|
||||||
|
abstract class Stub extends Binder implements IKeystoreOperation {
|
||||||
|
public static IKeystoreOperation asInterface(IBinder b) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder asBinder() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateAad(byte[] aadInput) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package android.system.keystore2;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.hardware.security.keymint.KeyParameter;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class KeyParameters implements Parcelable {
|
||||||
|
public KeyParameter[] keyParameter;
|
||||||
|
|
||||||
|
public static final Creator<KeyParameters> CREATOR = new Creator<KeyParameters>() {
|
||||||
|
@Override
|
||||||
|
public KeyParameters createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyParameters[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel parcel, int i) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package android.system.keystore2;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public class OperationChallenge implements Parcelable {
|
||||||
|
public long challenge = 0L;
|
||||||
|
|
||||||
|
public static final Creator<OperationChallenge> CREATOR = new Creator<OperationChallenge>() {
|
||||||
|
@Override
|
||||||
|
public OperationChallenge createFromParcel(Parcel in) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OperationChallenge[] newArray(int size) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(@NonNull Parcel parcel, int i) {
|
||||||
|
throw new UnsupportedOperationException("STUB!");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user