Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e83a846c9 | ||
|
|
3aff09e7dd | ||
|
|
c7b0af2d29 | ||
|
|
13a1dd7887 | ||
|
|
21a3cb1ec0 | ||
|
|
2b31d4ef47 | ||
|
|
38e9b547a5 | ||
|
|
94c8e5b182 | ||
|
|
8fdc59a142 | ||
|
|
b6f9d7b486 | ||
|
|
99e6b0b5ea | ||
|
|
397bb6338b | ||
|
|
cb81116701 | ||
|
|
45477a7898 | ||
|
|
634a1293c1 | ||
|
|
f115eda2dc | ||
|
|
217edf61fe | ||
|
|
ee5bf2e1a7 | ||
|
|
2181157cb6 | ||
|
|
aa4917e623 | ||
|
|
f06cb30b40 | ||
|
|
03c71bd202 | ||
|
|
7e2fc0b288 | ||
|
|
258a65ba59 |
@@ -0,0 +1,4 @@
|
||||
# Ensure shell scripts always have LF line endings, even on Windows.
|
||||
# These get packaged into flashable zips and run on Android devices.
|
||||
*.sh text eol=lf
|
||||
module/daemon text eol=lf
|
||||
@@ -242,6 +242,9 @@ boot=device_default
|
||||
- **[5ec1cff](https://github.com/5ec1cff/TrickyStore)** — TrickyStore, the project that pioneered keystore interception on Android
|
||||
- **[LSPlt](https://github.com/LSPosed/LSPlt)** — PLT hook library used for binder interception
|
||||
- **[ring](https://github.com/briansmith/ring)** — Rust cryptography library powering native cert generation
|
||||
- **[MhmRdd](https://github.com/MhmRdd)** — AOSP compliance improvements via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157), including authorize_create enforcement, attestation extension alignment, and binder transaction filtering
|
||||
- **[fatalcoder524](https://github.com/fatalcoder524)** — a real contributor and collaborator on this project
|
||||
- **[huguangares](https://github.com/huguangares)** — collaborator and tester
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
|
||||
|
||||
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
|
||||
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
||||
val verName = "v4.8"
|
||||
val verName = "v5.1"
|
||||
|
||||
android {
|
||||
namespace = "org.matrix.TEESimulator"
|
||||
@@ -121,8 +121,8 @@ androidComponents {
|
||||
dependsOn("package${capitalized}")
|
||||
} else {
|
||||
dependsOn("minify${capitalized}WithR8")
|
||||
dependsOn("strip${capitalized}DebugSymbols")
|
||||
}
|
||||
dependsOn("strip${capitalized}DebugSymbols")
|
||||
dependsOn(buildRustCertgen)
|
||||
|
||||
if (isDebug) {
|
||||
@@ -140,12 +140,11 @@ androidComponents {
|
||||
}
|
||||
}
|
||||
|
||||
val nativeLibsDir = if (isDebug) {
|
||||
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
|
||||
} else {
|
||||
from(
|
||||
project.layout.buildDirectory.dir(
|
||||
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
||||
}
|
||||
from(project.layout.buildDirectory.dir(nativeLibsDir)) {
|
||||
)
|
||||
) {
|
||||
into("lib")
|
||||
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DNDEBUG")
|
||||
|
||||
# LSPlt configuration
|
||||
OPTION(LSPLT_BUILD_SHARED OFF)
|
||||
|
||||
@@ -235,6 +235,8 @@ class BinderInterceptor : public BBinder {
|
||||
struct RegistrationEntry {
|
||||
wp<IBinder> target;
|
||||
sp<IBinder> callback_interface;
|
||||
// Transaction codes to intercept. Empty = intercept all (legacy behavior).
|
||||
std::vector<uint32_t> filtered_codes;
|
||||
};
|
||||
|
||||
// Reader-Writer lock for the registry to allow concurrent reads (lookups)
|
||||
@@ -244,10 +246,15 @@ class BinderInterceptor : public BBinder {
|
||||
public:
|
||||
BinderInterceptor() = default;
|
||||
|
||||
// Checks if a specific Binder instance is currently registered for interception
|
||||
bool isBinderIntercepted(const wp<BBinder> &target) const {
|
||||
// Checks if a specific Binder+code combination should be intercepted.
|
||||
// Returns true if the binder is registered AND the code is in its filter
|
||||
// (or the filter is empty, meaning intercept everything).
|
||||
bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
|
||||
std::shared_lock lock(registry_mutex_);
|
||||
return registry_.find(target) != registry_.end();
|
||||
auto it = registry_.find(target);
|
||||
if (it == registry_.end()) return false;
|
||||
const auto &codes = it->second.filtered_codes;
|
||||
return codes.empty() || std::find(codes.begin(), codes.end(), code) != codes.end();
|
||||
}
|
||||
|
||||
// Main entry point for processing the "Man-in-the-Middle" logic
|
||||
@@ -348,19 +355,11 @@ static sp<BinderStub> g_stub_instance = nullptr;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024;
|
||||
|
||||
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||
if (!txn_data || txn_data->target.ptr == 0)
|
||||
return;
|
||||
|
||||
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks
|
||||
if (txn_data->data_size > kMaxInterceptableDataSize)
|
||||
return;
|
||||
|
||||
// AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff).
|
||||
// System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range.
|
||||
// Skip those — intercepting a ping adds measurable latency that timing detectors flag.
|
||||
// Skip system transactions (PING, INTERFACE, DUMP) to avoid latency detectors
|
||||
if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode)
|
||||
return;
|
||||
|
||||
@@ -393,7 +392,7 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||
// This is safe because we are holding a strong reference.
|
||||
wp<BBinder> wp_target = target_binder_ptr;
|
||||
|
||||
if (g_interceptor_instance->isBinderIntercepted(wp_target)) {
|
||||
if (g_interceptor_instance->shouldIntercept(wp_target, txn_data->code)) {
|
||||
info.transaction_code = txn_data->code;
|
||||
info.target_binder = wp_target; // Assign the valid weak pointer
|
||||
hijack = true;
|
||||
@@ -544,12 +543,26 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) {
|
||||
return BAD_TYPE;
|
||||
}
|
||||
|
||||
// Read optional transaction code filter. If present: int32 count + count * uint32 codes.
|
||||
// If absent or count <= 0: intercept all transaction codes (legacy behavior).
|
||||
std::vector<uint32_t> codes;
|
||||
int32_t code_count = 0;
|
||||
if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) {
|
||||
codes.reserve(code_count);
|
||||
for (int32_t i = 0; i < code_count; i++) {
|
||||
uint32_t c = 0;
|
||||
if (data.readUint32(&c) == OK) codes.push_back(c);
|
||||
}
|
||||
LOGI("Interceptor registered for binder %p with %zu filtered codes", target.get(), codes.size());
|
||||
} else {
|
||||
LOGI("Interceptor registered for binder %p (all codes)", target.get());
|
||||
}
|
||||
|
||||
wp<IBinder> weak_target = target;
|
||||
|
||||
std::unique_lock lock(registry_mutex_);
|
||||
registry_[weak_target] = {weak_target, callback};
|
||||
registry_[weak_target] = {weak_target, callback, std::move(codes)};
|
||||
|
||||
LOGI("Interceptor registered for binder %p", target.get());
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -599,15 +612,8 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
|
||||
Parcel pre_req, pre_resp;
|
||||
writeTransactionData(pre_req, tx_id, target, code, flags, request);
|
||||
|
||||
status_t pre_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp);
|
||||
if (pre_status != OK) {
|
||||
// Block when interceptor is dead to prevent privacy leak to third-party apps
|
||||
if (callback->pingBinder() != OK) {
|
||||
LOGE("[TX_ID: %" PRIu64 "] Interceptor DEAD. Blocking to prevent attestation leak.", tx_id);
|
||||
result = DEAD_OBJECT;
|
||||
return true;
|
||||
}
|
||||
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed (not dead). Forwarding.", tx_id);
|
||||
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) {
|
||||
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -661,8 +667,7 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
|
||||
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size));
|
||||
}
|
||||
|
||||
status_t post_status = callback->transact(intercept::kPostTransact, post_req, &post_resp);
|
||||
if (post_status == OK) {
|
||||
if (callback->transact(intercept::kPostTransact, post_req, &post_resp) == OK) {
|
||||
int32_t post_action = post_resp.readInt32();
|
||||
if (post_action == intercept::kActionOverrideReply && reply) {
|
||||
result = post_resp.readInt32(); // Read new status
|
||||
|
||||
@@ -23,6 +23,8 @@ import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
object App {
|
||||
// The delay in milliseconds before retrying to initialize the interceptor.
|
||||
private const val RETRY_DELAY_MS = 1000L
|
||||
// The sleep duration in milliseconds for the main service loop to keep the process alive.
|
||||
private const val SERVICE_SLEEP_MS = 1000000L
|
||||
|
||||
/**
|
||||
* The main entry point of the TEESimulator application.
|
||||
@@ -33,15 +35,16 @@ object App {
|
||||
fun main(args: Array<String>) {
|
||||
SystemLogger.info("Welcome to TEESimulator!")
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
SystemLogger.error("Uncaught exception on ${thread.name}", throwable)
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize the Android framework environment
|
||||
prepareEnvironment()
|
||||
// Initialize and start the appropriate keystore interceptors.
|
||||
initializeInterceptors()
|
||||
|
||||
// Load the package configuration.
|
||||
ConfigurationManager.initialize()
|
||||
// Set up the device's boot key and hash, which are crucial for attestation.
|
||||
AndroidDeviceUtils.setupBootKeyAndHash()
|
||||
|
||||
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
|
||||
|
||||
@@ -2,8 +2,11 @@ package org.matrix.TEESimulator.attestation
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import org.bouncycastle.asn1.ASN1Boolean
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.ASN1Enumerated
|
||||
@@ -112,7 +115,6 @@ object AttestationBuilder {
|
||||
}
|
||||
|
||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
|
||||
SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch")
|
||||
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
|
||||
if (bootPatch != DO_NOT_REPORT) {
|
||||
DERTaggedObject(
|
||||
@@ -133,8 +135,16 @@ object AttestationBuilder {
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
): ASN1Sequence {
|
||||
val creationTime = System.currentTimeMillis()
|
||||
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
|
||||
val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
|
||||
val softwareEnforced = buildSoftwareEnforcedList(params, uid, securityLevel, creationTime)
|
||||
|
||||
val uniqueId =
|
||||
if (params.includeUniqueId == true && params.attestationChallenge != null) {
|
||||
computeUniqueId(creationTime, createApplicationId(uid).octets)
|
||||
} else {
|
||||
ByteArray(0)
|
||||
}
|
||||
|
||||
val fields =
|
||||
arrayOf(
|
||||
@@ -147,13 +157,49 @@ object AttestationBuilder {
|
||||
), // keymasterVersion
|
||||
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
|
||||
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
|
||||
DEROctetString(ByteArray(0)), // uniqueId
|
||||
DEROctetString(uniqueId),
|
||||
softwareEnforced,
|
||||
teeEnforced,
|
||||
)
|
||||
return DERSequence(fields)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the unique ID per the KeyMint HAL spec:
|
||||
* HMAC-SHA256(T || C || R, HBK) truncated to 128 bits.
|
||||
*
|
||||
* T = temporal counter (creationTime / 2592000000, i.e. 30-day periods since epoch)
|
||||
* C = DER-encoded ATTESTATION_APPLICATION_ID
|
||||
* R = 0x00 (no factory reset since ID rotation)
|
||||
* HBK = device-unique secret generated once during module installation
|
||||
*/
|
||||
private fun computeUniqueId(creationTimeMs: Long, aaidDer: ByteArray): ByteArray {
|
||||
val temporalCounter = creationTimeMs / 2592000000L
|
||||
|
||||
val message =
|
||||
ByteBuffer.allocate(8 + aaidDer.size + 1)
|
||||
.putLong(temporalCounter)
|
||||
.put(aaidDer)
|
||||
.put(0x00) // RESET_SINCE_ID_ROTATION = false
|
||||
.array()
|
||||
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
|
||||
return mac.doFinal(message).copyOf(16)
|
||||
}
|
||||
|
||||
/** Device-unique key seed, generated once at module installation. */
|
||||
private val hbk: ByteArray by lazy {
|
||||
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
|
||||
if (file.exists() && file.length() == 32L) {
|
||||
file.readBytes()
|
||||
} else {
|
||||
// Fallback: generate in-memory (won't persist across reboots)
|
||||
SystemLogger.warning("hbk not found, generating ephemeral HBK.")
|
||||
ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
|
||||
private fun buildTeeEnforcedList(
|
||||
params: KeyMintAttestation,
|
||||
@@ -194,6 +240,16 @@ object AttestationBuilder {
|
||||
)
|
||||
}
|
||||
|
||||
if (params.blockMode.isNotEmpty()) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_BLOCK_MODE,
|
||||
DERSet(params.blockMode.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.padding.isNotEmpty()) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
@@ -214,14 +270,79 @@ object AttestationBuilder {
|
||||
)
|
||||
}
|
||||
|
||||
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
|
||||
|
||||
if (params.rsaOaepMgfDigest.isNotEmpty() && attestVersion >= 100) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST,
|
||||
DERSet(
|
||||
params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray()
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.rollbackResistance == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ROLLBACK_RESISTANCE,
|
||||
DERNull.INSTANCE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.earlyBootOnly == true && attestVersion >= 4) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_EARLY_BOOT_ONLY, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.noAuthRequired == true) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.allowWhileOnBody == true) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ALLOW_WHILE_ON_BODY,
|
||||
DERNull.INSTANCE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED,
|
||||
DERNull.INSTANCE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED,
|
||||
DERNull.INSTANCE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
list.addAll(
|
||||
listOf(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ORIGIN,
|
||||
ASN1Integer(0L),
|
||||
), // KeyOrigin.GENERATED
|
||||
ASN1Integer((params.origin ?: 0).toLong()),
|
||||
),
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ROOT_OF_TRUST,
|
||||
@@ -325,20 +446,32 @@ object AttestationBuilder {
|
||||
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
||||
* Keystore.
|
||||
*/
|
||||
private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence {
|
||||
val list =
|
||||
mutableListOf<ASN1Encodable>(
|
||||
private fun buildSoftwareEnforcedList(
|
||||
params: KeyMintAttestation,
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
creationTimeMs: Long = System.currentTimeMillis(),
|
||||
): DERSequence {
|
||||
val list = mutableListOf<ASN1Encodable>()
|
||||
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_CREATION_DATETIME,
|
||||
ASN1Integer(System.currentTimeMillis()),
|
||||
),
|
||||
ASN1Integer(creationTimeMs),
|
||||
)
|
||||
)
|
||||
|
||||
// ATTESTATION_APPLICATION_ID is only included when an attestation challenge is present.
|
||||
if (params.attestationChallenge != null) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
|
||||
createApplicationId(uid),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
@@ -348,7 +481,52 @@ object AttestationBuilder {
|
||||
)
|
||||
)
|
||||
}
|
||||
return DERSequence(list.toTypedArray())
|
||||
|
||||
// Keystore2-enforced tags belong in softwareEnforced, not teeEnforced.
|
||||
// The HAL does not enforce these; keystore2's authorize_create handles them.
|
||||
params.activeDateTime?.let {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
|
||||
)
|
||||
}
|
||||
params.originationExpireDateTime?.let {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME,
|
||||
ASN1Integer(it.time),
|
||||
)
|
||||
)
|
||||
}
|
||||
params.usageExpireDateTime?.let {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_USAGE_EXPIRE_DATETIME,
|
||||
ASN1Integer(it.time),
|
||||
)
|
||||
)
|
||||
}
|
||||
params.usageCountLimit?.let {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_USAGE_COUNT_LIMIT,
|
||||
ASN1Integer(it.toLong()),
|
||||
)
|
||||
)
|
||||
}
|
||||
if (params.unlockedDeviceRequired == true) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED,
|
||||
DERNull.INSTANCE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,6 +554,17 @@ object AttestationBuilder {
|
||||
*/
|
||||
@Throws(Throwable::class)
|
||||
internal fun createApplicationId(uid: Int): DEROctetString {
|
||||
// AOSP keystore_attestation_id.cpp: gather_attestation_application_id()
|
||||
// uses a hardcoded identity for AID_SYSTEM (1000) and AID_ROOT (0):
|
||||
// packageName = "AndroidSystem", versionCode = 1, no signing digests.
|
||||
val appUid = uid % 100000
|
||||
if (appUid == 0 || appUid == 1000) {
|
||||
return buildApplicationIdDer(
|
||||
listOf("AndroidSystem" to 1L),
|
||||
emptySet(),
|
||||
)
|
||||
}
|
||||
|
||||
val pm =
|
||||
ConfigurationManager.getPackageManager()
|
||||
?: throw IllegalStateException("PackageManager not found!")
|
||||
@@ -383,12 +572,11 @@ object AttestationBuilder {
|
||||
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
|
||||
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
val packageInfoList = mutableListOf<DERSequence>()
|
||||
val packageInfoList = mutableListOf<Pair<String, Long>>()
|
||||
val signatureDigests = mutableSetOf<Digest>()
|
||||
|
||||
// Process all packages associated with the UID in a single loop.
|
||||
packages.forEach { packageName ->
|
||||
val userId = uid / 100000
|
||||
packages.forEach { packageName ->
|
||||
val packageInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.getPackageInfo(
|
||||
@@ -401,34 +589,36 @@ object AttestationBuilder {
|
||||
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
|
||||
}
|
||||
|
||||
// Add package information (name and version code) to our list.
|
||||
packageInfoList.add(
|
||||
packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode)
|
||||
|
||||
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
|
||||
signatureDigests.add(Digest(sha256.digest(signature.toByteArray())))
|
||||
}
|
||||
}
|
||||
|
||||
return buildApplicationIdDer(packageInfoList, signatureDigests)
|
||||
}
|
||||
|
||||
private fun buildApplicationIdDer(
|
||||
packages: List<Pair<String, Long>>,
|
||||
digests: Set<Digest>,
|
||||
): DEROctetString {
|
||||
val packageInfoList =
|
||||
packages.map { (name, version) ->
|
||||
DERSequence(
|
||||
arrayOf(
|
||||
DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)),
|
||||
ASN1Integer(packageInfo.longVersionCode),
|
||||
DEROctetString(name.toByteArray(StandardCharsets.UTF_8)),
|
||||
ASN1Integer(version),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// 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()),
|
||||
DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
|
||||
return DEROctetString(applicationIdSequence.encoded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,11 @@ object AttestationConstants {
|
||||
|
||||
// --- Key Lifetime and Usage Control ---
|
||||
const val TAG_ROLLBACK_RESISTANCE = 303
|
||||
const val TAG_EARLY_BOOT_ONLY = 305
|
||||
const val TAG_ACTIVE_DATETIME = 400
|
||||
const val TAG_ORIGINATION_EXPIRE_DATETIME = 401
|
||||
const val TAG_USAGE_EXPIRE_DATETIME = 402
|
||||
const val TAG_MAX_BOOT_LEVEL = 403
|
||||
const val TAG_MAX_USES_PER_BOOT = 404
|
||||
const val TAG_USAGE_COUNT_LIMIT = 405
|
||||
|
||||
@@ -56,6 +58,10 @@ object AttestationConstants {
|
||||
const val TAG_NO_AUTH_REQUIRED = 503
|
||||
const val TAG_USER_AUTH_TYPE = 504
|
||||
const val TAG_AUTH_TIMEOUT = 505
|
||||
const val TAG_ALLOW_WHILE_ON_BODY = 506
|
||||
const val TAG_TRUSTED_USER_PRESENCE_REQUIRED = 507
|
||||
const val TAG_TRUSTED_CONFIRMATION_REQUIRED = 508
|
||||
const val TAG_UNLOCKED_DEVICE_REQUIRED = 509
|
||||
|
||||
// --- Attestation and Application Info ---
|
||||
const val TAG_APPLICATION_ID = 601
|
||||
@@ -89,5 +95,5 @@ object AttestationConstants {
|
||||
|
||||
// --- Other Constants ---
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
|
||||
const val CHALLENGE_LENGTH_LIMIT = 128
|
||||
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package org.matrix.TEESimulator.attestation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.ASN1OctetString
|
||||
@@ -57,55 +52,14 @@ object DeviceAttestationService {
|
||||
val bootPatchLevel: Int?,
|
||||
)
|
||||
|
||||
// A unique alias for the key used to perform the TEE functionality check.
|
||||
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
|
||||
|
||||
/**
|
||||
* Lazily determines if the device's TEE is functional by attempting to generate an
|
||||
* attestation-backed key pair. The result is cached.
|
||||
*/
|
||||
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
|
||||
|
||||
/**
|
||||
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
|
||||
* is cached. Returns null if the TEE is not functional or parsing fails.
|
||||
*/
|
||||
val CachedAttestationData: AttestationData? by lazy { fetchAttestationData() }
|
||||
|
||||
/**
|
||||
* Checks if the TEE is working correctly by generating a key in the Android Keystore with an
|
||||
* attestation challenge.
|
||||
*
|
||||
* @return `true` if a key with attestation was generated successfully, `false` otherwise.
|
||||
*/
|
||||
private fun checkTeeFunctionality(): Boolean {
|
||||
SystemLogger.info("Performing TEE functionality check...")
|
||||
return try {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||
val keyPairGenerator =
|
||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
||||
|
||||
// A random challenge is required for attestation.
|
||||
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
|
||||
|
||||
val spec =
|
||||
KeyGenParameterSpec.Builder(TEE_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN)
|
||||
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||
.setAttestationChallenge(challenge)
|
||||
.build()
|
||||
|
||||
keyPairGenerator.initialize(spec)
|
||||
keyPairGenerator.generateKeyPair()
|
||||
|
||||
SystemLogger.info("TEE functionality check successful.")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.warning("TEE functionality check failed.", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the attestation certificate generated during the TEE check. The key entry is
|
||||
* deleted after retrieval to clean up.
|
||||
@@ -113,8 +67,6 @@ object DeviceAttestationService {
|
||||
* @return The leaf `X509Certificate` containing the attestation, or `null` if unavailable.
|
||||
*/
|
||||
private fun getAttestationCertificate(): X509Certificate? {
|
||||
if (!isTeeFunctional) return null
|
||||
|
||||
return try {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||
val certChain = keyStore.getCertificateChain(TEE_CHECK_KEY_ALIAS)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.matrix.TEESimulator.attestation
|
||||
|
||||
import android.hardware.security.keymint.*
|
||||
import android.hardware.security.keymint.KeyOrigin
|
||||
import java.math.BigInteger
|
||||
import java.util.Date
|
||||
import javax.security.auth.x500.X500Principal
|
||||
@@ -17,11 +16,12 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
// Reference:
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/key_parameter.rs
|
||||
data class KeyMintAttestation(
|
||||
val keySize: Int,
|
||||
val algorithm: Int,
|
||||
val ecCurve: Int?,
|
||||
val ecCurveName: String,
|
||||
val keySize: Int,
|
||||
val origin: Int?,
|
||||
val noAuthRequired: Boolean?,
|
||||
val blockMode: List<Int>,
|
||||
val padding: List<Int>,
|
||||
val purpose: List<Int>,
|
||||
@@ -41,17 +41,35 @@ data class KeyMintAttestation(
|
||||
val manufacturer: ByteArray?,
|
||||
val model: ByteArray?,
|
||||
val secondImei: ByteArray?,
|
||||
// Enforcement tags
|
||||
val activeDateTime: Date?,
|
||||
val originationExpireDateTime: Date?,
|
||||
val usageExpireDateTime: Date?,
|
||||
val usageCountLimit: Int?,
|
||||
val callerNonce: Boolean?,
|
||||
val unlockedDeviceRequired: Boolean?,
|
||||
val includeUniqueId: Boolean?,
|
||||
val rollbackResistance: Boolean?,
|
||||
val earlyBootOnly: Boolean?,
|
||||
val allowWhileOnBody: Boolean?,
|
||||
val trustedUserPresenceRequired: Boolean?,
|
||||
val trustedConfirmationRequired: Boolean?,
|
||||
val maxUsesPerBoot: Int?,
|
||||
val maxBootLevel: Int?,
|
||||
val minMacLength: Int?,
|
||||
val rsaOaepMgfDigest: List<Int>,
|
||||
) {
|
||||
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
||||
constructor(
|
||||
params: Array<KeyParameter>
|
||||
) : this(
|
||||
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
|
||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: 0,
|
||||
|
||||
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
|
||||
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
|
||||
|
||||
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
|
||||
// For EC keys, derive keySize from EC_CURVE when KEY_SIZE is absent.
|
||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(),
|
||||
|
||||
// AOSP: [key_param(tag = EC_CURVE, field = EcCurve)]
|
||||
ecCurve = params.findEcCurve(Tag.EC_CURVE),
|
||||
ecCurveName = params.deriveEcCurveName(),
|
||||
@@ -59,6 +77,9 @@ data class KeyMintAttestation(
|
||||
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
|
||||
origin = params.findOrigin(Tag.ORIGIN),
|
||||
|
||||
// AOSP: [key_param(tag = NO_AUTH_REQUIRED, field = BoolValue)]
|
||||
noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED),
|
||||
|
||||
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
|
||||
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
|
||||
|
||||
@@ -100,18 +121,44 @@ data class KeyMintAttestation(
|
||||
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
|
||||
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
|
||||
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
||||
|
||||
// Enforcement tags
|
||||
activeDateTime = params.findDate(Tag.ACTIVE_DATETIME),
|
||||
originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME),
|
||||
usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME),
|
||||
usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT),
|
||||
callerNonce = params.findBoolean(Tag.CALLER_NONCE),
|
||||
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
|
||||
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
|
||||
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
|
||||
earlyBootOnly = params.findBoolean(Tag.EARLY_BOOT_ONLY),
|
||||
allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY),
|
||||
trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED),
|
||||
trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED),
|
||||
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
|
||||
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
|
||||
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
|
||||
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
|
||||
) {
|
||||
// Log all parsed parameters for debugging purposes.
|
||||
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
||||
}
|
||||
|
||||
fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
|
||||
fun isAttestKey(): Boolean {
|
||||
return purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
|
||||
}
|
||||
|
||||
fun isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
|
||||
fun isImportKey(): Boolean {
|
||||
return origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
|
||||
}
|
||||
}
|
||||
|
||||
// --- Private helper extension functions for parsing KeyParameter arrays ---
|
||||
|
||||
/** Maps to AOSP field = Integer */
|
||||
private fun Array<KeyParameter>.findBoolean(tag: Int): Boolean? =
|
||||
this.find { it.tag == tag }?.value?.boolValue
|
||||
|
||||
/** Maps to AOSP field = Integer */
|
||||
private fun Array<KeyParameter>.findInteger(tag: Int): Int? =
|
||||
this.find { it.tag == tag }?.value?.integer
|
||||
@@ -144,7 +191,7 @@ private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
|
||||
private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> =
|
||||
this.filter { it.tag == tag }.map { it.value.blockMode }
|
||||
|
||||
/** Maps to AOSP field = BlockMode (Repeated) */
|
||||
/** Maps to AOSP field = PaddingMode (Repeated) */
|
||||
private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> =
|
||||
this.filter { it.tag == tag }.map { it.value.paddingMode }
|
||||
|
||||
@@ -156,6 +203,19 @@ private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
|
||||
private fun Array<KeyParameter>.findAllDigests(tag: Int): List<Int> =
|
||||
this.filter { it.tag == tag }.map { it.value.digest }
|
||||
|
||||
/** Derives keySize from EC_CURVE tag when KEY_SIZE is not explicitly provided. */
|
||||
private fun Array<KeyParameter>.deriveKeySizeFromCurve(): Int {
|
||||
val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0
|
||||
return when (curveId) {
|
||||
EcCurve.P_224 -> 224
|
||||
EcCurve.P_256 -> 256
|
||||
EcCurve.P_384 -> 384
|
||||
EcCurve.P_521 -> 521
|
||||
EcCurve.CURVE_25519 -> 256
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
|
||||
* to KEY_SIZE (field=Integer).
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.os.IBinder
|
||||
import android.os.ServiceManager
|
||||
import java.io.File
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||
|
||||
@@ -31,7 +30,6 @@ object ConfigurationManager {
|
||||
// --- Configuration Paths ---
|
||||
const val CONFIG_PATH = "/data/adb/tricky_store"
|
||||
private const val TARGET_PACKAGES_FILE = "target.txt"
|
||||
private const val TEE_STATUS_FILE = "tee_status.txt"
|
||||
private const val PATCH_LEVEL_FILE = "security_patch.txt"
|
||||
private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
|
||||
private val configRoot = File(CONFIG_PATH)
|
||||
@@ -39,7 +37,6 @@ object ConfigurationManager {
|
||||
// --- In-Memory Configuration State ---
|
||||
@Volatile private var packageModes = mapOf<String, Mode>()
|
||||
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
||||
@Volatile private var isTeeBroken: Boolean? = null
|
||||
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
|
||||
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
|
||||
|
||||
@@ -68,7 +65,6 @@ object ConfigurationManager {
|
||||
// Initial load of all configuration files.
|
||||
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
|
||||
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
|
||||
storeTeeStatus() // Check and store the current TEE status.
|
||||
|
||||
// Start watching for any subsequent file changes.
|
||||
ConfigObserver.startWatching()
|
||||
@@ -88,7 +84,10 @@ object ConfigurationManager {
|
||||
}
|
||||
|
||||
/** Determines if the certificate for a given UID needs to be patched. */
|
||||
fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH
|
||||
fun shouldPatch(uid: Int): Boolean {
|
||||
val mode = getPackageModeForUid(uid)
|
||||
return mode == Mode.PATCH || mode == Mode.AUTO
|
||||
}
|
||||
|
||||
/** Determines if a new certificate needs to be generated for a given UID. */
|
||||
fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE
|
||||
@@ -96,24 +95,23 @@ object ConfigurationManager {
|
||||
/** Determines if no operation is needed for a given UID. */
|
||||
fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null
|
||||
|
||||
/** Determines if the UID is in AUTO mode (no explicit ! or ? suffix). */
|
||||
fun isAutoMode(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.AUTO
|
||||
|
||||
/** Resolves the operating mode for a given UID based on its packages and the TEE status. */
|
||||
private fun getPackageModeForUid(uid: Int): Mode? {
|
||||
val packages = getPackagesForUid(uid)
|
||||
if (packages.isEmpty()) return null
|
||||
|
||||
// Lazily load TEE status if it hasn't been checked yet.
|
||||
if (isTeeBroken == null) loadTeeStatus()
|
||||
|
||||
// Find the first configured mode for any of the UID's packages.
|
||||
for (pkg in packages) {
|
||||
when (packageModes[pkg]) {
|
||||
Mode.GENERATE -> return Mode.GENERATE
|
||||
Mode.PATCH -> return Mode.PATCH
|
||||
Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH
|
||||
null -> continue // No config for this package, check the next one.
|
||||
Mode.AUTO -> return Mode.AUTO
|
||||
null -> continue
|
||||
}
|
||||
}
|
||||
return null // No configuration found for this UID.
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,25 +156,25 @@ object ConfigurationManager {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val mode: Mode
|
||||
val rawPkg: String
|
||||
when {
|
||||
// Suffix '!' means force GENERATE mode.
|
||||
trimmedLine.endsWith("!") -> {
|
||||
val pkg = trimmedLine.removeSuffix("!").trim()
|
||||
newModes[pkg] = Mode.GENERATE
|
||||
newKeyboxes[pkg] = currentKeybox
|
||||
mode = Mode.GENERATE
|
||||
rawPkg = trimmedLine.removeSuffix("!").trim()
|
||||
}
|
||||
// Suffix '?' means force PATCH mode.
|
||||
trimmedLine.endsWith("?") -> {
|
||||
val pkg = trimmedLine.removeSuffix("?").trim()
|
||||
newModes[pkg] = Mode.PATCH
|
||||
newKeyboxes[pkg] = currentKeybox
|
||||
mode = Mode.PATCH
|
||||
rawPkg = trimmedLine.removeSuffix("?").trim()
|
||||
}
|
||||
// No suffix means AUTO mode.
|
||||
else -> {
|
||||
newModes[trimmedLine] = Mode.AUTO
|
||||
newKeyboxes[trimmedLine] = currentKeybox
|
||||
mode = Mode.AUTO
|
||||
rawPkg = trimmedLine
|
||||
}
|
||||
}
|
||||
|
||||
newModes[rawPkg] = mode
|
||||
newKeyboxes[rawPkg] = currentKeybox
|
||||
}
|
||||
|
||||
// Atomically update the configuration maps.
|
||||
@@ -253,14 +251,7 @@ object ConfigurationManager {
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
val newGlobalLevel = parseLines(contextLines[""])
|
||||
contextLines.remove("") // Remove global context to iterate over packages next
|
||||
|
||||
for ((pkg, lines) in contextLines) {
|
||||
@@ -280,29 +271,6 @@ object ConfigurationManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks the device's TEE status and writes the result to a file for persistence. */
|
||||
private fun storeTeeStatus() {
|
||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
||||
isTeeBroken = !DeviceAttestationService.isTeeFunctional
|
||||
try {
|
||||
statusFile.writeText("tee_broken=$isTeeBroken")
|
||||
SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken")
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to write TEE status to file.", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** Loads the TEE status from the file. */
|
||||
private fun loadTeeStatus() {
|
||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
||||
isTeeBroken =
|
||||
if (statusFile.exists()) {
|
||||
statusFile.readText().trim() == "tee_broken=true"
|
||||
} else {
|
||||
null // Status is unknown.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A FileObserver that monitors the configuration directory for changes and triggers reloads of
|
||||
* the relevant settings.
|
||||
@@ -314,10 +282,8 @@ object ConfigurationManager {
|
||||
|
||||
val file = if (event != DELETE) File(configRoot, path) else null
|
||||
when (path) {
|
||||
TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) }
|
||||
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
|
||||
PATCH_LEVEL_FILE -> file?.let { loadPatchLevelConfig(it) }
|
||||
?: SystemLogger.warning("$PATCH_LEVEL_FILE was deleted.")
|
||||
TARGET_PACKAGES_FILE -> loadTargetPackages(file!!)
|
||||
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!)
|
||||
// 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 ->
|
||||
@@ -360,6 +326,32 @@ object ConfigurationManager {
|
||||
return iPackageManager
|
||||
}
|
||||
|
||||
/** Checks if any package belonging to the UID holds the given permission. */
|
||||
/** Checks a SELinux permission for a caller identified by PID against the keystore context. */
|
||||
fun checkSELinuxPermission(callingPid: Int, tclass: String, perm: String): Boolean {
|
||||
return try {
|
||||
val callerCtx =
|
||||
java.io.File("/proc/$callingPid/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||
val selfCtx =
|
||||
java.io.File("/proc/self/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||
android.os.SELinux.checkSELinuxAccess(callerCtx, selfCtx, tclass, perm)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks if any package belonging to the UID holds the given permission. */
|
||||
fun hasPermissionForUid(uid: Int, permission: String): Boolean {
|
||||
val userId = uid / 100000
|
||||
return getPackagesForUid(uid).any { pkg ->
|
||||
try {
|
||||
getPackageManager()?.checkPermission(permission, pkg, userId) == 0
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Retrieves the package names associated with a UID. */
|
||||
fun getPackagesForUid(uid: Int): Array<String> {
|
||||
return uidToPackagesCache.getOrPut(uid) {
|
||||
|
||||
@@ -109,17 +109,17 @@ abstract class BinderInterceptor : Binder() {
|
||||
* `handlePostTransact`).
|
||||
*/
|
||||
final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||
// The native hook prepends a transaction ID to the data parcel.
|
||||
val txId = data.readLong()
|
||||
val result =
|
||||
val result = try {
|
||||
when (code) {
|
||||
// These codes are defined in the native layer to distinguish hook types.
|
||||
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
|
||||
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
|
||||
else -> return super.onTransact(code, data, reply, flags)
|
||||
}
|
||||
|
||||
// The reply parcel is guaranteed to be non-null for our custom transactions.
|
||||
} catch (e: Throwable) {
|
||||
SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e)
|
||||
TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
writeResultToReply(result, reply!!)
|
||||
return true
|
||||
}
|
||||
@@ -293,15 +293,27 @@ abstract class BinderInterceptor : Binder() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Uses the backdoor binder to register an interceptor for a specific target service. */
|
||||
fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) {
|
||||
/**
|
||||
* Uses the backdoor binder to register an interceptor for a specific target service.
|
||||
*
|
||||
* @param filteredCodes If non-empty, only these transaction codes will be intercepted at
|
||||
* the native level. All other codes pass through without the round-trip to Java.
|
||||
*/
|
||||
fun register(
|
||||
backdoor: IBinder,
|
||||
target: IBinder,
|
||||
interceptor: BinderInterceptor,
|
||||
filteredCodes: IntArray = intArrayOf(),
|
||||
) {
|
||||
val data = Parcel.obtain()
|
||||
val reply = Parcel.obtain()
|
||||
try {
|
||||
data.writeStrongBinder(target)
|
||||
data.writeStrongBinder(interceptor)
|
||||
data.writeInt(filteredCodes.size)
|
||||
for (code in filteredCodes) data.writeInt(code)
|
||||
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||
SystemLogger.info("Registered interceptor for target: $target")
|
||||
SystemLogger.info("Registered interceptor for target: $target (${filteredCodes.size} filtered codes)")
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to register binder interceptor.", e)
|
||||
} finally {
|
||||
|
||||
+7
-1
@@ -68,11 +68,17 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction codes this interceptor needs to handle at the native level. Override in
|
||||
* subclasses to filter; empty means intercept everything (legacy behavior).
|
||||
*/
|
||||
protected open val interceptedCodes: IntArray = intArrayOf()
|
||||
|
||||
/** Registers this interceptor with the native hook layer and sets up a death recipient. */
|
||||
private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
|
||||
keystoreService = service
|
||||
SystemLogger.info("Registering interceptor for service: $serviceName")
|
||||
register(backdoor, service, this)
|
||||
register(backdoor, service, this, interceptedCodes)
|
||||
service.linkToDeath(createDeathRecipient(), 0)
|
||||
onInterceptorReady(service, backdoor)
|
||||
}
|
||||
|
||||
+67
-1
@@ -1,11 +1,16 @@
|
||||
package org.matrix.TEESimulator.interception.keystore
|
||||
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.security.KeyStore
|
||||
import android.security.keystore.KeystoreResponse
|
||||
import android.system.keystore2.Authorization
|
||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
|
||||
data class KeyIdentifier(val uid: Int, val alias: String)
|
||||
|
||||
@@ -18,7 +23,7 @@ object InterceptorUtils {
|
||||
val parcel = Parcel.obtain().apply {
|
||||
writeInt(EX_SERVICE_SPECIFIC)
|
||||
writeString(null)
|
||||
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
||||
writeInt(0)
|
||||
writeInt(errorCode)
|
||||
}
|
||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||
@@ -124,4 +129,65 @@ object InterceptorUtils {
|
||||
if (exception != null) reply.setDataPosition(0)
|
||||
return exception != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an `OverrideReply` that writes a `ServiceSpecificException` with the given error
|
||||
* code via EX_SERVICE_SPECIFIC.
|
||||
*/
|
||||
fun createServiceSpecificErrorReply(
|
||||
errorCode: Int
|
||||
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||
val parcel =
|
||||
Parcel.obtain().apply {
|
||||
writeException(android.os.ServiceSpecificException(errorCode))
|
||||
}
|
||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches the system-level authorization values (OS_PATCHLEVEL, VENDOR_PATCHLEVEL,
|
||||
* BOOT_PATCHLEVEL) in an authorization array to match the configured patch levels for the
|
||||
* given calling UID. Each authorization's original [Authorization.securityLevel] is preserved.
|
||||
*
|
||||
* When a patch level is configured as "no" ([AndroidDeviceUtils.DO_NOT_REPORT]), the original
|
||||
* hardware value is kept as-is.
|
||||
*/
|
||||
fun patchAuthorizations(
|
||||
authorizations: Array<Authorization>?,
|
||||
callingUid: Int,
|
||||
): Array<Authorization>? {
|
||||
if (authorizations == null) return null
|
||||
|
||||
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
|
||||
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||
|
||||
return authorizations
|
||||
.map { auth ->
|
||||
val replacement =
|
||||
when (auth.keyParameter.tag) {
|
||||
Tag.OS_PATCHLEVEL ->
|
||||
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) osPatch else null
|
||||
Tag.VENDOR_PATCHLEVEL ->
|
||||
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) vendorPatch
|
||||
else null
|
||||
Tag.BOOT_PATCHLEVEL ->
|
||||
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) bootPatch else null
|
||||
else -> null
|
||||
}
|
||||
if (replacement != null) {
|
||||
Authorization().apply {
|
||||
keyParameter =
|
||||
KeyParameter().apply {
|
||||
tag = auth.keyParameter.tag
|
||||
value = KeyParameterValue.integer(replacement)
|
||||
}
|
||||
securityLevel = auth.securityLevel
|
||||
}
|
||||
} else {
|
||||
auth
|
||||
}
|
||||
}
|
||||
.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
+259
-81
@@ -5,16 +5,18 @@ import android.hardware.security.keymint.SecurityLevel
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.Domain
|
||||
import android.system.keystore2.IKeystoreSecurityLevel
|
||||
import android.system.keystore2.IKeystoreService
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.Certificate
|
||||
import java.util.Collections
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||
import org.matrix.TEESimulator.interception.keystore.shim.GeneratedKeyPersistence
|
||||
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
@@ -45,6 +47,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
if (Build.VERSION.SDK_INT >= 34)
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
|
||||
else null
|
||||
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
|
||||
private val GET_SECURITY_LEVEL_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "getSecurityLevel")
|
||||
|
||||
private val transactionNames: Map<Int, String> by lazy {
|
||||
stubBinderClass.declaredFields
|
||||
@@ -55,18 +61,43 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||
}
|
||||
|
||||
private const val RESPONSE_KEY_NOT_FOUND = 7
|
||||
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||
// Keys whose certs were updated via updateSubcomponent; skip re-patching on getKeyEntry.
|
||||
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||
|
||||
// Backdoor binder for registering new interceptors at runtime.
|
||||
private var backdoorBinder: IBinder? = null
|
||||
|
||||
// Per-security-level interceptor instances, keyed by SecurityLevel constant.
|
||||
private val securityLevelInterceptors = ConcurrentHashMap<Int, KeyMintSecurityLevelInterceptor>()
|
||||
|
||||
// Identity set of SecurityLevel binders already registered with the native hook,
|
||||
// tracked by System.identityHashCode to avoid re-registering the same BBinder.
|
||||
private val registeredSecurityLevelBinders: MutableSet<Int> =
|
||||
Collections.newSetFromMap(ConcurrentHashMap())
|
||||
|
||||
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
||||
override val processName = "keystore2"
|
||||
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
|
||||
|
||||
override val interceptedCodes: IntArray by lazy {
|
||||
listOfNotNull(
|
||||
GET_KEY_ENTRY_TRANSACTION,
|
||||
DELETE_KEY_TRANSACTION,
|
||||
UPDATE_SUBCOMPONENT_TRANSACTION,
|
||||
LIST_ENTRIES_TRANSACTION,
|
||||
LIST_ENTRIES_BATCHED_TRANSACTION,
|
||||
GET_NUMBER_OF_ENTRIES_TRANSACTION,
|
||||
GET_SECURITY_LEVEL_TRANSACTION,
|
||||
)
|
||||
.toIntArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
backdoorBinder = backdoor
|
||||
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
|
||||
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
|
||||
}
|
||||
@@ -78,7 +109,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
|
||||
val interceptor =
|
||||
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
||||
register(backdoor, tee.asBinder(), interceptor)
|
||||
securityLevelInterceptors[SecurityLevel.TRUSTED_ENVIRONMENT] = interceptor
|
||||
registerSecurityLevelBinder(
|
||||
backdoor,
|
||||
tee.asBinder(),
|
||||
interceptor,
|
||||
)
|
||||
interceptor.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
@@ -90,13 +126,42 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
|
||||
val interceptor =
|
||||
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
|
||||
register(backdoor, strongbox.asBinder(), interceptor)
|
||||
securityLevelInterceptors[SecurityLevel.STRONGBOX] = interceptor
|
||||
registerSecurityLevelBinder(
|
||||
backdoor,
|
||||
strongbox.asBinder(),
|
||||
interceptor,
|
||||
)
|
||||
interceptor.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an interceptor for a SecurityLevel binder, tracking the binder identity
|
||||
* to avoid duplicate registrations when keystore2 returns the same BBinder.
|
||||
*/
|
||||
private fun registerSecurityLevelBinder(
|
||||
backdoor: IBinder,
|
||||
binder: IBinder,
|
||||
interceptor: KeyMintSecurityLevelInterceptor,
|
||||
) {
|
||||
val identity = System.identityHashCode(binder)
|
||||
if (registeredSecurityLevelBinders.add(identity)) {
|
||||
register(
|
||||
backdoor,
|
||||
binder,
|
||||
interceptor,
|
||||
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
||||
)
|
||||
} else {
|
||||
SystemLogger.debug(
|
||||
"SecurityLevel binder $binder (identity=$identity) already registered, skipping."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreTransact(
|
||||
txId: Long,
|
||||
target: IBinder,
|
||||
@@ -106,7 +171,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
): TransactionResult {
|
||||
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||
return if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||
TransactionResult.ContinueAndSkipPost
|
||||
else TransactionResult.Continue
|
||||
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||
|
||||
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
||||
@@ -114,22 +184,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
|
||||
if (isGMS || 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
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
} else if (
|
||||
code == GET_KEY_ENTRY_TRANSACTION ||
|
||||
@@ -149,37 +205,43 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
|
||||
if (code == DELETE_KEY_TRANSACTION) {
|
||||
// Handle delete by alias (APP domain) or nspace (KEY_ID domain).
|
||||
val keyId =
|
||||
if (descriptor.alias != null) {
|
||||
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
|
||||
} else {
|
||||
KeyIdentifier(callingUid, descriptor.alias)
|
||||
} else if (descriptor.domain == Domain.KEY_ID) {
|
||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||
callingUid, descriptor.nspace
|
||||
)?.let { info ->
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||
.find { it.value.nspace == info.nspace && it.key.uid == callingUid }
|
||||
?.key
|
||||
}
|
||||
} else null
|
||||
|
||||
if (keyId != null) {
|
||||
val isSoftwareKey =
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId)
|
||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||
if (isSoftwareKey) {
|
||||
SystemLogger.info(
|
||||
"Skip ${transactionNames[code]!!} for key [alias, blob, domain, nspace]: [${descriptor.alias}, ${descriptor.blob}, ${descriptor.domain}, ${descriptor.nspace}]"
|
||||
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
|
||||
)
|
||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||
}
|
||||
}
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
if (descriptor.alias == null) {
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||
|
||||
if (code == DELETE_KEY_TRANSACTION) {
|
||||
val wasSoftwareKey = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null
|
||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||
if (wasSoftwareKey) {
|
||||
deletedSoftwareKeys.add(keyId)
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
||||
)
|
||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||
}
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||
if (response == null) {
|
||||
if (deletedSoftwareKeys.remove(keyId)) {
|
||||
SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}")
|
||||
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||
}
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
val response =
|
||||
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||
?: return TransactionResult.Continue
|
||||
|
||||
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
||||
SystemLogger.info("${descriptor.alias} was an attestation key")
|
||||
@@ -189,6 +251,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
KeyMintParameterLogger.logParameter(it.keyParameter)
|
||||
}
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
} else if (code == GET_SECURITY_LEVEL_TRANSACTION) {
|
||||
// Pass through to post-hook so we can register interceptors for newly-created
|
||||
// SecurityLevel binders. keystore2 may create a new BBinder per call, so the
|
||||
// initial registration in setupSecurityLevelInterceptors might not cover all
|
||||
// binder instances that clients receive.
|
||||
logTransaction(txId, "getSecurityLevel", callingUid, callingPid)
|
||||
return TransactionResult.Continue
|
||||
} else {
|
||||
logTransaction(
|
||||
txId,
|
||||
@@ -217,12 +286,39 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
||||
return TransactionResult.SkipTransaction
|
||||
|
||||
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
if (code == GET_SECURITY_LEVEL_TRANSACTION) {
|
||||
return handlePostGetSecurityLevel(txId, data, reply)
|
||||
}
|
||||
|
||||
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||
return runCatching {
|
||||
val hardwareCount = reply.readInt()
|
||||
val softwareCount =
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.keys.count {
|
||||
it.uid == callingUid
|
||||
}
|
||||
val totalCount = hardwareCount + softwareCount
|
||||
val parcel = Parcel.obtain().apply {
|
||||
writeNoException()
|
||||
writeInt(totalCount)
|
||||
}
|
||||
TransactionResult.OverrideReply(parcel)
|
||||
}
|
||||
.getOrElse {
|
||||
SystemLogger.error("[TX_ID: $txId] Failed to modify getNumberOfEntries.", it)
|
||||
TransactionResult.SkipTransaction
|
||||
}
|
||||
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||
|
||||
return runCatching {
|
||||
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
|
||||
val params =
|
||||
ListEntriesHandler.cacheParameters(txId, data, isBatchMode)
|
||||
?: throw Exception("Abort updating entries for invalid parameters.")
|
||||
val updatedKeyDescriptors =
|
||||
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, reply)
|
||||
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, params, reply)
|
||||
InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors)
|
||||
}
|
||||
.getOrElse {
|
||||
@@ -252,27 +348,25 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
|
||||
// Skip patching for keys whose certs were explicitly set via updateSubcomponent.
|
||||
if (userUpdatedKeys.remove(keyId)) {
|
||||
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
val authorizations = response.metadata.authorizations
|
||||
val parsedParameters =
|
||||
KeyMintAttestation(
|
||||
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
|
||||
)
|
||||
|
||||
if (parsedParameters.isImportKey()) {
|
||||
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||
if (retainedChain == null) {
|
||||
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
||||
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
if (parsedParameters.isAttestKey()) {
|
||||
if (parsedParameters.isAttestKey() &&
|
||||
!KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)
|
||||
) {
|
||||
SystemLogger.warning(
|
||||
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
|
||||
)
|
||||
// Attest keys that are not under our control should be overriden.
|
||||
val keyData =
|
||||
CertificateGenerator.generateAttestedKeyPair(
|
||||
callingUid,
|
||||
@@ -287,35 +381,29 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
keyData.second.toTypedArray(),
|
||||
)
|
||||
.getOrThrow()
|
||||
response.metadata.authorizations =
|
||||
InterceptorUtils.patchAuthorizations(
|
||||
response.metadata.authorizations,
|
||||
callingUid,
|
||||
)
|
||||
|
||||
keyDescriptor.nspace = SecureRandom().nextLong()
|
||||
val newNspace = SecureRandom().nextLong()
|
||||
response.metadata.key?.let { it.nspace = newNspace }
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
|
||||
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
|
||||
keyData.first,
|
||||
keyDescriptor.nspace,
|
||||
null,
|
||||
newNspace,
|
||||
response,
|
||||
parsedParameters,
|
||||
)
|
||||
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
||||
|
||||
GeneratedKeyPersistence.save(
|
||||
keyId = keyId,
|
||||
keyPair = keyData.first,
|
||||
nspace = keyDescriptor.nspace,
|
||||
securityLevel = response.metadata.keySecurityLevel,
|
||||
certChain = keyData.second,
|
||||
algorithm = parsedParameters.algorithm,
|
||||
keySize = parsedParameters.keySize,
|
||||
ecCurve = parsedParameters.ecCurve ?: 0,
|
||||
purposes = parsedParameters.purpose,
|
||||
digests = parsedParameters.digest,
|
||||
isAttestationKey = true,
|
||||
)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
val originalChain = CertificateHelper.getCertificateChain(response)
|
||||
|
||||
// Check if we should perform attestation patch.
|
||||
if (originalChain == null || originalChain.size < 2) {
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
|
||||
@@ -323,6 +411,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
// First, try to retrieve the already-patched chain from our cache to ensure
|
||||
// consistency.
|
||||
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||
|
||||
val finalChain: Array<Certificate>
|
||||
@@ -332,16 +422,25 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
)
|
||||
finalChain = cachedChain
|
||||
} else {
|
||||
// If no chain is cached (e.g., key existed before simulator started),
|
||||
// perform a live patch as a fallback. This may still be detectable.
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
|
||||
)
|
||||
finalChain =
|
||||
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||
|
||||
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
|
||||
SystemLogger.debug("Cached patched certificate chain for $keyId.")
|
||||
}
|
||||
|
||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
||||
.getOrThrow()
|
||||
response.metadata.authorizations =
|
||||
InterceptorUtils.patchAuthorizations(
|
||||
response.metadata.authorizations,
|
||||
callingUid,
|
||||
)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
@@ -359,10 +458,29 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
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
|
||||
|
||||
// Resolve by nspace (KEY_ID) or alias (APP), same as createOperation.
|
||||
val generatedKeyInfo =
|
||||
when (descriptor.domain) {
|
||||
Domain.KEY_ID ->
|
||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||
callingUid,
|
||||
descriptor.nspace,
|
||||
)
|
||||
Domain.APP ->
|
||||
descriptor.alias?.let {
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys[KeyIdentifier(callingUid, it)]
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (generatedKeyInfo == null) {
|
||||
// Hardware key: mark so getKeyEntry skips cert re-patching.
|
||||
descriptor.alias?.let { userUpdatedKeys.add(KeyIdentifier(callingUid, it)) }
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
|
||||
val metadata = generatedKeyInfo.response.metadata
|
||||
val publicCert = data.createByteArray()
|
||||
@@ -370,13 +488,73 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
|
||||
metadata.certificate = publicCert
|
||||
metadata.certificateChain = certificateChain
|
||||
|
||||
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
|
||||
|
||||
SystemLogger.verbose(
|
||||
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
|
||||
)
|
||||
|
||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts the reply from getSecurityLevel to dynamically register our interceptor
|
||||
* for the returned IKeystoreSecurityLevel binder.
|
||||
*
|
||||
* keystore2 may create a new BBinder for each getSecurityLevel call, so the binder
|
||||
* registered during initial setup (in setupSecurityLevelInterceptors) might not be the
|
||||
* same one that client apps receive. By intercepting every getSecurityLevel reply, we
|
||||
* ensure that all SecurityLevel binders are covered.
|
||||
*/
|
||||
private fun handlePostGetSecurityLevel(
|
||||
txId: Long,
|
||||
data: Parcel,
|
||||
reply: Parcel,
|
||||
): TransactionResult {
|
||||
val backdoor = backdoorBinder
|
||||
if (backdoor == null) {
|
||||
SystemLogger.warning("[TX_ID: $txId] post-getSecurityLevel: backdoor not available")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
// Read the security level argument from the original request.
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
val requestedLevel = data.readInt()
|
||||
|
||||
// hasException already consumed the exception header from the reply.
|
||||
// Next item is the IKeystoreSecurityLevel binder.
|
||||
val secLevelBinder = reply.readStrongBinder()
|
||||
if (secLevelBinder == null) {
|
||||
SystemLogger.verbose(
|
||||
"[TX_ID: $txId] getSecurityLevel($requestedLevel) returned null binder"
|
||||
)
|
||||
return@runCatching TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
// Only intercept TEE and StrongBox security levels.
|
||||
if (requestedLevel != SecurityLevel.TRUSTED_ENVIRONMENT &&
|
||||
requestedLevel != SecurityLevel.STRONGBOX
|
||||
) {
|
||||
return@runCatching TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
// Get or create the interceptor for this security level. The interceptor may not
|
||||
// exist yet if the initial setupSecurityLevelInterceptors call failed for this level.
|
||||
val interceptor = securityLevelInterceptors.getOrPut(requestedLevel) {
|
||||
val secLevelInterface =
|
||||
IKeystoreSecurityLevel.Stub.asInterface(secLevelBinder)
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Late-creating interceptor for security level $requestedLevel"
|
||||
)
|
||||
KeyMintSecurityLevelInterceptor(secLevelInterface, requestedLevel).also {
|
||||
it.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
|
||||
registerSecurityLevelBinder(backdoor, secLevelBinder, interceptor)
|
||||
TransactionResult.SkipTransaction
|
||||
}.getOrElse {
|
||||
SystemLogger.error("[TX_ID: $txId] Failed to process post-getSecurityLevel.", it)
|
||||
TransactionResult.SkipTransaction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-4
@@ -399,17 +399,18 @@ private data class LegacyKeygenParameters(
|
||||
|
||||
/**
|
||||
* Converts the legacy parameters into the modern [KeyMintAttestation] data structure, which is
|
||||
* required by the refactored [AttestationBuilder] and [CertificateGenerator].
|
||||
* required by [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,
|
||||
ecCurve = 0, // Not explicitly available in legacy args, but not critical
|
||||
ecCurveName = this.ecCurveName ?: "",
|
||||
origin = null,
|
||||
keySize = this.keySize,
|
||||
origin = null, // Not needed to build attestaion
|
||||
noAuthRequired = null,
|
||||
blockMode = listOf<Int>(),
|
||||
padding = listOf<Int>(),
|
||||
purpose = this.purpose,
|
||||
@@ -431,6 +432,22 @@ private data class LegacyKeygenParameters(
|
||||
manufacturer = null,
|
||||
model = null,
|
||||
secondImei = null,
|
||||
activeDateTime = null,
|
||||
originationExpireDateTime = null,
|
||||
usageExpireDateTime = null,
|
||||
usageCountLimit = null,
|
||||
callerNonce = null,
|
||||
unlockedDeviceRequired = null,
|
||||
includeUniqueId = null,
|
||||
rollbackResistance = null,
|
||||
earlyBootOnly = null,
|
||||
allowWhileOnBody = null,
|
||||
trustedUserPresenceRequired = null,
|
||||
trustedConfirmationRequired = null,
|
||||
maxUsesPerBoot = null,
|
||||
maxBootLevel = null,
|
||||
minMacLength = null,
|
||||
rsaOaepMgfDigest = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+14
-20
@@ -5,7 +5,6 @@ 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
|
||||
|
||||
@@ -22,15 +21,6 @@ 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>,
|
||||
@@ -60,7 +50,7 @@ object ListEntriesHandler {
|
||||
}
|
||||
|
||||
// Parse and store parameters for later use (in post-transaction).
|
||||
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): Boolean {
|
||||
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): ListEntriesParams? {
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
|
||||
val domain = data.readInt()
|
||||
@@ -71,20 +61,21 @@ object ListEntriesHandler {
|
||||
// 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
|
||||
val params = ListEntriesParams(domain, namespace, startPastAlias)
|
||||
SystemLogger.debug("[TX_ID: $txId] Cached $params.")
|
||||
return params
|
||||
}
|
||||
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
fun injectGeneratedKeys(
|
||||
txId: Long,
|
||||
callingUid: Int,
|
||||
params: ListEntriesParams,
|
||||
reply: Parcel,
|
||||
): Array<KeyDescriptor> {
|
||||
// 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.
|
||||
@@ -140,3 +131,6 @@ object ListEntriesHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parameters of AOSP function `list_key_entries` in utils.rs.
|
||||
data class ListEntriesParams(val domain: Int, val namespace: Long, val startPastAlias: String?)
|
||||
|
||||
+1
-116
@@ -16,7 +16,7 @@ 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,
|
||||
@@ -250,76 +250,6 @@ object GeneratedKeyPersistence {
|
||||
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
|
||||
@@ -330,49 +260,4 @@ object GeneratedKeyPersistence {
|
||||
.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+708
-337
File diff suppressed because it is too large
Load Diff
+3
@@ -44,6 +44,9 @@ class OperationInterceptor(
|
||||
private val ABORT_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
|
||||
|
||||
/** Only intercept finish/abort for cleanup. Other ops pass through without round-trip. */
|
||||
val INTERCEPTED_CODES = intArrayOf(FINISH_TRANSACTION, ABORT_TRANSACTION)
|
||||
|
||||
private val transactionNames: Map<Int, String> by lazy {
|
||||
IKeystoreOperation.Stub::class
|
||||
.java
|
||||
|
||||
+197
-83
@@ -3,11 +3,15 @@ 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.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.hardware.security.keymint.PaddingMode
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.RemoteException
|
||||
import android.os.ServiceSpecificException
|
||||
import android.system.keystore2.IKeystoreOperation
|
||||
import android.system.keystore2.KeyParameters
|
||||
import java.security.KeyPair
|
||||
import java.security.Signature
|
||||
import java.security.SignatureException
|
||||
@@ -16,12 +20,45 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
/** Keystore2 error codes for ServiceSpecificException. Negative = KeyMint, positive = Keystore. */
|
||||
internal object KeystoreErrorCode {
|
||||
const val INVALID_OPERATION_HANDLE = -28
|
||||
const val VERIFICATION_FAILED = -30
|
||||
const val UNSUPPORTED_PURPOSE = -2
|
||||
const val INCOMPATIBLE_PURPOSE = -3
|
||||
const val SYSTEM_ERROR = 4
|
||||
const val TOO_MUCH_DATA = 21
|
||||
const val KEY_EXPIRED = -25
|
||||
const val KEY_NOT_YET_VALID = -24
|
||||
|
||||
/** KeyMint ErrorCode::CALLER_NONCE_PROHIBITED */
|
||||
const val CALLER_NONCE_PROHIBITED = -55
|
||||
|
||||
/** KeyMint ErrorCode::INVALID_ARGUMENT */
|
||||
const val INVALID_ARGUMENT = -38
|
||||
|
||||
/** KeyMint ErrorCode::INVALID_TAG */
|
||||
const val INVALID_TAG = -40
|
||||
|
||||
/** Keystore2 ResponseCode::PERMISSION_DENIED */
|
||||
const val PERMISSION_DENIED = 6
|
||||
|
||||
/** Keystore2 ResponseCode::KEY_NOT_FOUND */
|
||||
const val KEY_NOT_FOUND = 7
|
||||
}
|
||||
|
||||
// A sealed interface to represent the different cryptographic operations we can perform.
|
||||
private sealed interface CryptoPrimitive {
|
||||
fun updateAad(aadInput: ByteArray?) {}
|
||||
fun updateAad(data: ByteArray?)
|
||||
|
||||
fun update(data: ByteArray?): ByteArray?
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||
|
||||
fun abort()
|
||||
|
||||
/** Returns parameters from the begin phase (e.g. GCM nonce), or null if none. */
|
||||
fun getBeginParameters(): Array<KeyParameter>? = null
|
||||
}
|
||||
|
||||
// Helper object to map KeyMint constants to JCA algorithm strings.
|
||||
@@ -34,17 +71,17 @@ private object JcaAlgorithmMapper {
|
||||
Digest.SHA_2_512 -> "SHA512"
|
||||
else -> "NONE"
|
||||
}
|
||||
return when (params.algorithm) {
|
||||
Algorithm.EC -> "${digest}withECDSA"
|
||||
Algorithm.RSA -> {
|
||||
val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS
|
||||
if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
|
||||
}
|
||||
val keyAlgo =
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> "ECDSA"
|
||||
Algorithm.RSA -> "RSA"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported signature algorithm: ${params.algorithm}"
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.SYSTEM_ERROR,
|
||||
"Unsupported signature algorithm: ${params.algorithm}",
|
||||
)
|
||||
}
|
||||
return "${digest}with${keyAlgo}"
|
||||
}
|
||||
|
||||
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||
@@ -53,26 +90,25 @@ private object JcaAlgorithmMapper {
|
||||
Algorithm.RSA -> "RSA"
|
||||
Algorithm.AES -> "AES"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported cipher algorithm: ${params.algorithm}"
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.SYSTEM_ERROR,
|
||||
"Unsupported cipher algorithm: ${params.algorithm}",
|
||||
)
|
||||
}
|
||||
val blockMode =
|
||||
when (params.blockMode.firstOrNull()) {
|
||||
BlockMode.ECB -> "ECB"
|
||||
BlockMode.CBC -> "CBC"
|
||||
BlockMode.CTR -> "CTR"
|
||||
BlockMode.GCM -> "GCM"
|
||||
else -> "ECB"
|
||||
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_PKCS1_1_5_SIGN -> "PKCS1Padding"
|
||||
PaddingMode.RSA_OAEP -> "OAEPPadding"
|
||||
else -> "NoPadding"
|
||||
else -> "NoPadding" // Default for GCM
|
||||
}
|
||||
return "$keyAlgo/$blockMode/$padding"
|
||||
}
|
||||
@@ -85,6 +121,10 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi
|
||||
initSign(keyPair.private)
|
||||
}
|
||||
|
||||
override fun updateAad(data: ByteArray?) {
|
||||
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||
}
|
||||
|
||||
override fun update(data: ByteArray?): ByteArray? {
|
||||
if (data != null) signature.update(data)
|
||||
return null
|
||||
@@ -105,6 +145,10 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
|
||||
initVerify(keyPair.public)
|
||||
}
|
||||
|
||||
override fun updateAad(data: ByteArray?) {
|
||||
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||
}
|
||||
|
||||
override fun update(data: ByteArray?): ByteArray? {
|
||||
if (data != null) signature.update(data)
|
||||
return null
|
||||
@@ -112,12 +156,17 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
|
||||
|
||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
if (data != null) update(data)
|
||||
if (signature == null) throw SignatureException("Signature to verify is null")
|
||||
if (signature == null)
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.VERIFICATION_FAILED,
|
||||
"Signature to verify is null",
|
||||
)
|
||||
if (!this.signature.verify(signature)) {
|
||||
// Throwing an exception is how Keystore signals verification failure.
|
||||
throw SignatureException("Signature verification failed")
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.VERIFICATION_FAILED,
|
||||
"Signature/MAC verification failed",
|
||||
)
|
||||
}
|
||||
// A successful verification returns no data.
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -126,14 +175,17 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
|
||||
|
||||
// Concrete implementation for Encryption/Decryption.
|
||||
private class CipherPrimitive(
|
||||
keyPair: KeyPair,
|
||||
cryptoKey: java.security.Key,
|
||||
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)
|
||||
init(opMode, cryptoKey)
|
||||
}
|
||||
|
||||
override fun updateAad(data: ByteArray?) {
|
||||
if (data != null) cipher.updateAAD(data)
|
||||
}
|
||||
|
||||
override fun update(data: ByteArray?): ByteArray? =
|
||||
@@ -143,17 +195,63 @@ private class CipherPrimitive(
|
||||
if (data != null) cipher.doFinal(data) else cipher.doFinal()
|
||||
|
||||
override fun abort() {}
|
||||
|
||||
/** Returns the cipher IV as a NONCE parameter for GCM operations. */
|
||||
override fun getBeginParameters(): Array<KeyParameter>? {
|
||||
val iv = cipher.iv ?: return null
|
||||
return arrayOf(
|
||||
KeyParameter().apply {
|
||||
tag = Tag.NONCE
|
||||
value = KeyParameterValue.blob(iv)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Concrete implementation for ECDH Key Agreement.
|
||||
private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
|
||||
private val agreement: javax.crypto.KeyAgreement =
|
||||
javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) }
|
||||
|
||||
override fun updateAad(data: ByteArray?) {
|
||||
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||
}
|
||||
|
||||
override fun update(data: ByteArray?): ByteArray? = null
|
||||
|
||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
if (data == null)
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.INVALID_ARGUMENT,
|
||||
"Peer public key required for key agreement",
|
||||
)
|
||||
val peerKey =
|
||||
java.security.KeyFactory.getInstance("EC")
|
||||
.generatePublic(java.security.spec.X509EncodedKeySpec(data))
|
||||
agreement.doPhase(peerKey, true)
|
||||
return agreement.generateSecret()
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* Tracks operation lifecycle: once [finish] or [abort] is called, subsequent calls throw
|
||||
* [ServiceSpecificException] with [KeystoreErrorCode.INVALID_OPERATION_HANDLE].
|
||||
*/
|
||||
class SoftwareOperation(
|
||||
private val txId: Long,
|
||||
keyPair: KeyPair,
|
||||
keyPair: KeyPair?,
|
||||
secretKey: javax.crypto.SecretKey?,
|
||||
params: KeyMintAttestation,
|
||||
private val latencyFloorMs: Long = 0L,
|
||||
var onFinishCallback: (() -> Unit)? = null,
|
||||
) {
|
||||
private val primitive: CryptoPrimitive
|
||||
@Volatile var finalized = false
|
||||
private set
|
||||
|
||||
@Volatile private var finalized = false
|
||||
|
||||
init {
|
||||
val purpose = params.purpose.firstOrNull()
|
||||
@@ -162,118 +260,134 @@ class SoftwareOperation(
|
||||
|
||||
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")
|
||||
KeyPurpose.SIGN -> Signer(keyPair!!, params)
|
||||
KeyPurpose.VERIFY -> Verifier(keyPair!!, params)
|
||||
KeyPurpose.ENCRYPT -> {
|
||||
val key: java.security.Key = secretKey ?: keyPair!!.public
|
||||
CipherPrimitive(key, params, Cipher.ENCRYPT_MODE)
|
||||
}
|
||||
KeyPurpose.DECRYPT -> {
|
||||
val key: java.security.Key = secretKey ?: keyPair!!.private
|
||||
CipherPrimitive(key, params, Cipher.DECRYPT_MODE)
|
||||
}
|
||||
KeyPurpose.AGREE_KEY -> KeyAgreementPrimitive(keyPair!!)
|
||||
else ->
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.UNSUPPORTED_PURPOSE,
|
||||
"Unsupported operation purpose: $purpose",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Parameters produced during begin (e.g. GCM nonce), to populate CreateOperationResponse. */
|
||||
val beginParameters: KeyParameters?
|
||||
get() {
|
||||
val params = primitive.getBeginParameters() ?: return null
|
||||
if (params.isEmpty()) return null
|
||||
return KeyParameters().apply { keyParameter = params }
|
||||
}
|
||||
|
||||
private fun checkActive() {
|
||||
if (finalized) {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
|
||||
}
|
||||
if (finalized)
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.INVALID_OPERATION_HANDLE,
|
||||
"Operation already finalized.",
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkInputLength(data: ByteArray?) {
|
||||
if (data != null && data.size > MAX_RECEIVE_DATA) {
|
||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})")
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAad(aadInput: ByteArray?) {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] updateAad() inputSize=${aadInput?.size ?: 0}")
|
||||
fun updateAad(data: ByteArray?) {
|
||||
checkActive()
|
||||
checkInputLength(aadInput)
|
||||
primitive.updateAad(aadInput)
|
||||
try {
|
||||
primitive.updateAad(data)
|
||||
} catch (e: ServiceSpecificException) {
|
||||
finalized = true
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
finalized = true
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to updateAad.", e)
|
||||
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(data: ByteArray?): ByteArray? {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] update() inputSize=${data?.size ?: 0}")
|
||||
checkActive()
|
||||
checkInputLength(data)
|
||||
try {
|
||||
return primitive.update(data)
|
||||
} catch (e: ServiceSpecificException) {
|
||||
finalized = true
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
finalized = true
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||
throw e
|
||||
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
checkActive()
|
||||
checkInputLength(data)
|
||||
try {
|
||||
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
|
||||
val result = primitive.finish(data, signature)
|
||||
if (latencyFloorMs > 0) {
|
||||
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||
val delayMs = latencyFloorMs - elapsedMs
|
||||
if (delayMs > 0) Thread.sleep(delayMs)
|
||||
}
|
||||
finalized = true
|
||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||
onFinishCallback?.invoke()
|
||||
return result
|
||||
} catch (e: ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
||||
throw e
|
||||
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
|
||||
} finally {
|
||||
finalized = true
|
||||
}
|
||||
}
|
||||
|
||||
fun abort() {
|
||||
checkActive()
|
||||
finalized = true
|
||||
primitive.abort()
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||
}
|
||||
|
||||
companion object {
|
||||
// AOSP keystore2 operation.rs: const MAX_RECEIVE_DATA: usize = 0x8000
|
||||
private const val MAX_RECEIVE_DATA = 0x8000
|
||||
}
|
||||
}
|
||||
|
||||
private object KeystoreErrorCodes {
|
||||
val tooMuchData: Int by lazy {
|
||||
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21)
|
||||
}
|
||||
|
||||
val invalidOperationHandle: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28)
|
||||
}
|
||||
|
||||
private fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
||||
runCatching {
|
||||
Class.forName(className).getField(fieldName).getInt(null)
|
||||
}.getOrElse {
|
||||
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
|
||||
fallback
|
||||
}
|
||||
}
|
||||
|
||||
/** Binder interface for [SoftwareOperation]. Synchronized and input-length validated. */
|
||||
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||
IKeystoreOperation.Stub() {
|
||||
|
||||
private fun checkInputLength(data: ByteArray?) {
|
||||
if (data != null && data.size > MAX_RECEIVE_DATA)
|
||||
throw ServiceSpecificException(KeystoreErrorCode.TOO_MUCH_DATA)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun updateAad(aadInput: ByteArray?) {
|
||||
synchronized(this) {
|
||||
checkInputLength(aadInput)
|
||||
operation.updateAad(aadInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun update(input: ByteArray?): ByteArray? {
|
||||
synchronized(this) {
|
||||
checkInputLength(input)
|
||||
return operation.update(input)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
synchronized(this) {
|
||||
checkInputLength(input)
|
||||
checkInputLength(signature)
|
||||
return operation.finish(input, signature)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun abort() {
|
||||
operation.abort()
|
||||
synchronized(this) { operation.abort() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_RECEIVE_DATA = 0x8000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,22 @@ object KeyMintParameterLogger {
|
||||
.associate { field -> (field.get(null) as Int) to field.name }
|
||||
}
|
||||
|
||||
val hardwareAuthenticatorTypeNames: Map<Int, String> by lazy {
|
||||
HardwareAuthenticatorType::class
|
||||
.java
|
||||
.fields
|
||||
.filter { it.type == Int::class.java }
|
||||
.associate { field -> (field.get(null) as Int) to field.name }
|
||||
}
|
||||
|
||||
val keyOriginNames: Map<Int, String> by lazy {
|
||||
KeyOrigin::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
|
||||
@@ -81,22 +97,33 @@ object KeyMintParameterLogger {
|
||||
when (param.tag) {
|
||||
Tag.ALGORITHM -> algorithmNames[value.algorithm]
|
||||
Tag.BLOCK_MODE -> blockModeNames[value.blockMode]
|
||||
Tag.DIGEST -> digestNames[value.digest]
|
||||
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
|
||||
Tag.ORIGIN -> keyOriginNames[value.origin]
|
||||
Tag.PADDING -> paddingNames[value.paddingMode]
|
||||
Tag.PURPOSE -> purposeNames[value.keyPurpose]
|
||||
Tag.DIGEST -> digestNames[value.digest]
|
||||
Tag.USER_AUTH_TYPE ->
|
||||
hardwareAuthenticatorTypeNames[value.hardwareAuthenticatorType]
|
||||
Tag.AUTH_TIMEOUT,
|
||||
Tag.BOOT_PATCHLEVEL,
|
||||
Tag.KEY_SIZE,
|
||||
Tag.MIN_MAC_LENGTH -> value.integer.toString()
|
||||
Tag.MAC_LENGTH,
|
||||
Tag.MIN_MAC_LENGTH,
|
||||
Tag.OS_VERSION,
|
||||
Tag.OS_PATCHLEVEL,
|
||||
Tag.USER_ID,
|
||||
Tag.VENDOR_PATCHLEVEL -> value.integer.toString()
|
||||
Tag.CERTIFICATE_SERIAL -> BigInteger(value.blob).toString()
|
||||
Tag.ACTIVE_DATETIME,
|
||||
Tag.CERTIFICATE_NOT_AFTER,
|
||||
Tag.CERTIFICATE_NOT_BEFORE,
|
||||
Tag.CREATION_DATETIME,
|
||||
Tag.ORIGINATION_EXPIRE_DATETIME,
|
||||
Tag.USAGE_EXPIRE_DATETIME -> Date(value.dateTime).toString()
|
||||
Tag.CERTIFICATE_SUBJECT -> X500Name(X500Principal(value.blob).name).toString()
|
||||
Tag.USER_SECURE_ID,
|
||||
Tag.RSA_PUBLIC_EXPONENT -> value.longInteger.toString()
|
||||
Tag.NO_AUTH_REQUIRED -> "true"
|
||||
Tag.NO_AUTH_REQUIRED -> value.boolValue.toString()
|
||||
Tag.ATTESTATION_CHALLENGE,
|
||||
Tag.ATTESTATION_ID_BRAND,
|
||||
Tag.ATTESTATION_ID_DEVICE,
|
||||
|
||||
@@ -8,7 +8,6 @@ import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
import java.security.spec.RSAKeyGenParameterSpec
|
||||
import java.util.Date
|
||||
@@ -36,6 +35,9 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
||||
*/
|
||||
object CertificateGenerator {
|
||||
|
||||
// RFC 5280 GeneralizedTime maximum: 9999-12-31T23:59:59 UTC (millis since epoch).
|
||||
private const val UNDEFINED_NOT_AFTER = 253402300799000L
|
||||
|
||||
/**
|
||||
* Generates a software-based cryptographic key pair.
|
||||
*
|
||||
@@ -49,7 +51,10 @@ object CertificateGenerator {
|
||||
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
|
||||
Algorithm.RSA ->
|
||||
"RSA" to
|
||||
RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent)
|
||||
RSAKeyGenParameterSpec(
|
||||
params.keySize,
|
||||
params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4,
|
||||
)
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported algorithm: ${params.algorithm}"
|
||||
@@ -88,11 +93,9 @@ object CertificateGenerator {
|
||||
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
|
||||
)
|
||||
|
||||
return runCatching {
|
||||
return try {
|
||||
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 }
|
||||
@@ -101,20 +104,20 @@ object CertificateGenerator {
|
||||
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
|
||||
}
|
||||
} catch (e: android.os.ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to generate certificate chain.", e)
|
||||
null
|
||||
}
|
||||
.onFailure { SystemLogger.error("Failed to generate certificate chain.", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +131,7 @@ object CertificateGenerator {
|
||||
params: KeyMintAttestation,
|
||||
securityLevel: Int,
|
||||
): Pair<KeyPair, List<Certificate>>? {
|
||||
return runCatching {
|
||||
return try {
|
||||
SystemLogger.info(
|
||||
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
|
||||
)
|
||||
@@ -144,11 +147,12 @@ object CertificateGenerator {
|
||||
"Successfully generated new certificate chain for alias: '$alias'."
|
||||
)
|
||||
Pair(newKeyPair, chain)
|
||||
} catch (e: android.os.ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e)
|
||||
null
|
||||
}
|
||||
.onFailure {
|
||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it)
|
||||
}
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
fun getIssuerFromKeybox(keybox: KeyBox) =
|
||||
@@ -163,7 +167,10 @@ object CertificateGenerator {
|
||||
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
|
||||
}
|
||||
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
|
||||
?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName")
|
||||
?: throw android.os.ServiceSpecificException(
|
||||
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
|
||||
"No attestation key for algorithm $algorithmName in $keyboxFile",
|
||||
)
|
||||
}
|
||||
|
||||
/** Retrieves the key pair and issuer name for a given attestation key alias. */
|
||||
@@ -192,7 +199,9 @@ object CertificateGenerator {
|
||||
private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int {
|
||||
var bits = 0
|
||||
for (purpose in purposes) {
|
||||
bits = bits or when (purpose) {
|
||||
bits =
|
||||
bits or
|
||||
when (purpose) {
|
||||
KeyPurpose.SIGN -> KeyUsage.digitalSignature
|
||||
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
|
||||
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
|
||||
@@ -214,16 +223,17 @@ object CertificateGenerator {
|
||||
securityLevel: Int,
|
||||
): Certificate {
|
||||
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||
val leafNotAfter =
|
||||
(signingKeyPair.public as? X509Certificate)?.notAfter
|
||||
?: Date(System.currentTimeMillis() + 31536000000L)
|
||||
|
||||
// Default validity: epoch to 9999-12-31T23:59:59 UTC (matches add_required_parameters).
|
||||
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||
|
||||
val builder =
|
||||
JcaX509v3CertificateBuilder(
|
||||
issuer,
|
||||
params.certificateSerial ?: BigInteger.ONE,
|
||||
params.certificateNotBefore ?: Date(),
|
||||
params.certificateNotAfter ?: leafNotAfter,
|
||||
notBefore,
|
||||
notAfter,
|
||||
subject,
|
||||
subjectKeyPair.public,
|
||||
)
|
||||
@@ -238,11 +248,16 @@ object CertificateGenerator {
|
||||
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
|
||||
)
|
||||
|
||||
// The signature algorithm must match the SIGNING key, not the subject key.
|
||||
// An EC attestation key may sign an RSA subject key's certificate (or vice versa).
|
||||
val signerAlgorithm =
|
||||
when (signingKeyPair.private.algorithm) {
|
||||
"EC", "ECDSA" -> "SHA256withECDSA"
|
||||
"RSA" -> "SHA256withRSA"
|
||||
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
|
||||
when (signingKeyPair.private) {
|
||||
is java.security.interfaces.ECKey -> "SHA256withECDSA"
|
||||
is java.security.interfaces.RSAKey -> "SHA256withRSA"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported signing key type: ${signingKeyPair.private.javaClass}"
|
||||
)
|
||||
}
|
||||
val contentSigner =
|
||||
JcaContentSignerBuilder(signerAlgorithm)
|
||||
|
||||
@@ -45,6 +45,13 @@ data class CertGenConfig(
|
||||
val idManufacturer: ByteArray?,
|
||||
val idModel: ByteArray?,
|
||||
val idSecondImei: ByteArray?,
|
||||
val activeDatetime: Long = -1L,
|
||||
val originationExpireDatetime: Long = -1L,
|
||||
val usageExpireDatetime: Long = -1L,
|
||||
val usageCountLimit: Int = -1,
|
||||
val callerNonce: Boolean = false,
|
||||
val unlockedDeviceRequired: Boolean = false,
|
||||
val noAuthRequired: Boolean = true,
|
||||
)
|
||||
|
||||
object NativeCertGen {
|
||||
|
||||
@@ -91,33 +91,27 @@ object AndroidDeviceUtils {
|
||||
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()}")
|
||||
persistToFile(propertyName, it)
|
||||
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)
|
||||
persistToFile(propertyName, it)
|
||||
setProperty(propertyName, it) // Persist for consistency
|
||||
return it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to get $propertyName from attestation.", e)
|
||||
}
|
||||
|
||||
readFromFile(propertyName, expectedSize)?.let {
|
||||
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
|
||||
setProperty(propertyName, it)
|
||||
return it
|
||||
}
|
||||
|
||||
// 3. As a final fallback, generate a random value.
|
||||
return generateRandomBytes(expectedSize).also {
|
||||
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
||||
setProperty(propertyName, it)
|
||||
persistToFile(propertyName, it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,37 +158,10 @@ object AndroidDeviceUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** Generates a cryptographically random byte array of a specified length. */
|
||||
private fun generateRandomBytes(size: Int): ByteArray =
|
||||
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
|
||||
|
||||
private val PERSIST_DIR = File("/data/adb/tricky_store")
|
||||
|
||||
private fun fileForProperty(propertyName: String): File = when (propertyName) {
|
||||
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
|
||||
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
|
||||
else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.bin")
|
||||
}
|
||||
|
||||
private fun persistToFile(propertyName: String, bytes: ByteArray) {
|
||||
try {
|
||||
fileForProperty(propertyName).writeBytes(bytes)
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to persist $propertyName to file.", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readFromFile(propertyName: String, expectedSize: Int): ByteArray? {
|
||||
return try {
|
||||
val file = fileForProperty(propertyName)
|
||||
if (!file.exists()) return null
|
||||
val bytes = file.readBytes()
|
||||
if (bytes.size == expectedSize) bytes else null
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to read $propertyName from file.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// --- Patch Level Properties ---
|
||||
|
||||
fun getPatchLevel(uid: Int): Int {
|
||||
@@ -273,12 +240,11 @@ object AndroidDeviceUtils {
|
||||
val resolvedValue = resolveDateKeywords(value)
|
||||
|
||||
return when {
|
||||
// "device_default" indicates falling back to the system property.
|
||||
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)
|
||||
// "no" indicates this value should not be reported.
|
||||
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
|
||||
// Otherwise, parse the resolved date string.
|
||||
else -> parsePatchLevelValue(resolvedValue, isLong)
|
||||
}
|
||||
}
|
||||
@@ -405,7 +371,10 @@ object AndroidDeviceUtils {
|
||||
|
||||
// --- APEX and Module Hash Properties ---
|
||||
|
||||
// Minimal protobuf parser for apex_manifest.pb (field 1: name, field 2: version)
|
||||
// https://cs.android.com/android/platform/superproject/+/android-latest-release:system/apex/proto/apex_manifest.proto
|
||||
// --- Minimal Protobuf Parser for ApexManifest ---
|
||||
// Field 1: name (string)
|
||||
// Field 2: version (int64)
|
||||
private class MinimalApexManifestParser(private val data: ByteArray) {
|
||||
var pos = 0
|
||||
|
||||
@@ -419,13 +388,13 @@ object AndroidDeviceUtils {
|
||||
val wireType = (tag and 0x07).toInt()
|
||||
|
||||
when (fieldNum) {
|
||||
1L -> {
|
||||
1L -> { // name
|
||||
val length = readVarint().toInt()
|
||||
if (pos + length > data.size) return null
|
||||
name = String(data, pos, length, Charsets.UTF_8)
|
||||
pos += length
|
||||
}
|
||||
2L -> {
|
||||
2L -> { // version
|
||||
version = readVarint()
|
||||
}
|
||||
else -> skipField(wireType)
|
||||
@@ -453,18 +422,19 @@ object AndroidDeviceUtils {
|
||||
|
||||
private fun skipField(wireType: Int) {
|
||||
when (wireType) {
|
||||
0 -> readVarint()
|
||||
1 -> pos += 8
|
||||
2 -> {
|
||||
0 -> readVarint() // Varint
|
||||
1 -> pos += 8 // 64-bit
|
||||
2 -> { // Length-delimited
|
||||
val len = readVarint().toInt()
|
||||
pos += len
|
||||
}
|
||||
5 -> pos += 4
|
||||
5 -> pos += 4 // 32-bit
|
||||
else -> throw IllegalStateException("Unknown wire type $wireType")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/apex/libs/libapexutil/apexutil.cpp
|
||||
private val apexInfos: List<Pair<String, Long>> by lazy {
|
||||
val results = mutableListOf<Pair<String, Long>>()
|
||||
val apexRoot = File("/apex")
|
||||
@@ -473,14 +443,22 @@ object AndroidDeviceUtils {
|
||||
return@lazy emptyList()
|
||||
}
|
||||
|
||||
// Logic from: GetActivePackages in apexutil.cpp
|
||||
apexRoot.listFiles()?.forEach { file ->
|
||||
if (!file.isDirectory) return@forEach
|
||||
val name = file.name
|
||||
|
||||
// 1. Ignore "." (and implicitly "..")
|
||||
if (name.startsWith(".")) return@forEach
|
||||
|
||||
// 2. Ignore directories containing '@' (active mounts usually don't have version in
|
||||
// path)
|
||||
if (name.contains("@")) return@forEach
|
||||
|
||||
// 3. Ignore "sharedlibs"
|
||||
if (name == "sharedlibs") return@forEach
|
||||
|
||||
// 4. Parse apex_manifest.pb
|
||||
val manifestFile = File(file, "apex_manifest.pb")
|
||||
if (manifestFile.exists()) {
|
||||
runCatching {
|
||||
@@ -491,46 +469,59 @@ object AndroidDeviceUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure uniqueness (though filesystem scan usually prevents exact dupes,
|
||||
// strictly speaking we want to behave like a Map keyed by package name)
|
||||
results.distinctBy { it.first }
|
||||
}
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/maintenance.rs
|
||||
val moduleHash: ByteArray by lazy {
|
||||
DeviceAttestationService.CachedAttestationData?.moduleHash
|
||||
?: runCatching {
|
||||
// 1. Create a container to hold the sort key (name encoded) and the full data
|
||||
// (sequence encoded)
|
||||
data class ModuleEntry(
|
||||
val nameEncoded: ByteArray,
|
||||
val fullEncoded: ByteArray,
|
||||
val nameEncoded: ByteArray, // The sort key
|
||||
val fullEncoded: ByteArray, // The data to hash
|
||||
)
|
||||
|
||||
val modules =
|
||||
apexInfos.map { (packageName, versionCode) ->
|
||||
// Create the components
|
||||
val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
|
||||
val versionInt = ASN1Integer(versionCode)
|
||||
|
||||
// Create the Sequence: SEQUENCE { packageName, version }
|
||||
val vec = ASN1EncodableVector()
|
||||
vec.add(nameOctet)
|
||||
vec.add(versionInt)
|
||||
val sequence = DERSequence(vec)
|
||||
|
||||
// AOSP sorts by encoded name only, not full sequence
|
||||
// We store the encoded name separately because Rust sorts ONLY by this
|
||||
ModuleEntry(
|
||||
nameEncoded = nameOctet.encoded,
|
||||
fullEncoded = sequence.encoded,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Sort manually based on the encoded Package Name (lexicographically)
|
||||
// This mimics the Rust 'impl DerOrd for ModuleInfo' which delegates to
|
||||
// 'self.name'
|
||||
val sortedModules =
|
||||
modules.sortedWith { m1, m2 ->
|
||||
compareByteArrays(m1.nameEncoded, m2.nameEncoded)
|
||||
}
|
||||
|
||||
// 3. Concatenate the full sequences in the specific sorted order
|
||||
val payloadStream = ByteArrayOutputStream()
|
||||
sortedModules.forEach { payloadStream.write(it.fullEncoded) }
|
||||
val payload = payloadStream.toByteArray()
|
||||
|
||||
// Wrap in DER SET tag manually — DERSet() re-sorts by full encoding
|
||||
// 4. Wrap manually in a DER SET tag (0x31)
|
||||
// We cannot use DERSet(vector) because it would re-sort incorrectly.
|
||||
val finalDerSet = encodeAsDerSet(payload)
|
||||
|
||||
// 5. Compute SHA-256
|
||||
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
|
||||
}
|
||||
.getOrElse {
|
||||
@@ -539,6 +530,7 @@ object AndroidDeviceUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** Compares two byte arrays lexicographically (unsigned). */
|
||||
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
|
||||
val length = minOf(a.size, b.size)
|
||||
for (i in 0 until length) {
|
||||
@@ -551,25 +543,31 @@ object AndroidDeviceUtils {
|
||||
return a.size - b.size
|
||||
}
|
||||
|
||||
/** Manually wraps the payload in an ASN.1 SET (0x31) tag with correct length encoding. */
|
||||
private fun encodeAsDerSet(payload: ByteArray): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
out.write(0x31)
|
||||
out.write(0x31) // ASN.1 Tag for SET
|
||||
writeDerLength(out, payload.size)
|
||||
out.write(payload)
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
/** Writes the ASN.1 length field to the stream. */
|
||||
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
|
||||
if (length < 128) {
|
||||
// Short form
|
||||
out.write(length)
|
||||
} else {
|
||||
// Long form
|
||||
var size = length
|
||||
val bytes = ArrayList<Byte>()
|
||||
while (size > 0) {
|
||||
bytes.add((size and 0xFF).toByte())
|
||||
size = size ushr 8
|
||||
}
|
||||
// First byte: 0x80 | number of length bytes
|
||||
out.write(0x80 or bytes.size)
|
||||
// Write length bytes in big-endian (reverse of how we extracted them)
|
||||
for (i in bytes.indices.reversed()) {
|
||||
out.write(bytes[i].toInt())
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
package org.matrix.TEESimulator.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
object AndroidPermissionUtils {
|
||||
|
||||
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
|
||||
private fun getGlobalContext(): Context? {
|
||||
return try {
|
||||
// 1. Get the hidden ActivityThread class via reflection
|
||||
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
||||
|
||||
// 2. Invoke the static currentActivityThread() method
|
||||
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
|
||||
currentActivityThreadMethod.isAccessible = true
|
||||
val activityThread = currentActivityThreadMethod.invoke(null)
|
||||
|
||||
if (activityThread == null) {
|
||||
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
|
||||
return null
|
||||
}
|
||||
|
||||
// 3. Try to get the application context
|
||||
val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication")
|
||||
getApplicationMethod.isAccessible = true
|
||||
val application = getApplicationMethod.invoke(activityThread) as? Context
|
||||
|
||||
if (application != null) return application
|
||||
|
||||
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
|
||||
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
|
||||
getSystemContextMethod.isAccessible = true
|
||||
getSystemContextMethod.invoke(activityThread) as? Context
|
||||
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Reflection failed to get global context for permission check", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core permission check.
|
||||
*/
|
||||
fun hasPermission(uid: Int, permission: String): Boolean {
|
||||
val context = getGlobalContext() ?: run {
|
||||
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
|
||||
return false
|
||||
}
|
||||
|
||||
val result = context.checkPermission(permission, -1, uid)
|
||||
return result == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun hasDeviceAttestationPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE")
|
||||
}
|
||||
|
||||
fun hasUniqueIdAttestationPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION")
|
||||
}
|
||||
|
||||
fun hasManageUsersPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.MANAGE_USERS")
|
||||
}
|
||||
|
||||
fun hasDumpPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.DUMP")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.matrix.TEESimulator.util
|
||||
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.locks.LockSupport
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Simulates realistic TEE hardware latency for software key generation.
|
||||
*
|
||||
* The delay model is derived from 64+ timing measurements across QTEE (Qualcomm) and Trustonic
|
||||
* (MediaTek) hardware. It combines four independent noise sources that model different physical
|
||||
* latency origins in a real TrustZone-based TEE:
|
||||
*
|
||||
* 1. Base crypto processing (log-normal): hardware RNG + key derivation + cert signing
|
||||
* 2. Binder/kernel transit (exponential): IPC scheduling, context switches
|
||||
* 3. TrustZone scheduler jitter (Gaussian): world-switch non-determinism
|
||||
* 4. Cold-start penalty (half-normal): first operation after idle is slower due to TEE
|
||||
* secure world re-initialization and TLB/cache warming
|
||||
*
|
||||
* Per-boot session bias models manufacturing variance between TEE hardware instances.
|
||||
*/
|
||||
object TeeLatencySimulator {
|
||||
|
||||
private val rng = SecureRandom()
|
||||
|
||||
private val sessionBiasMs: Double by lazy { rng.nextGaussian() * 5.0 }
|
||||
private val coldPenaltyMs: Double by lazy { abs(rng.nextGaussian() * 12.0) }
|
||||
|
||||
@Volatile private var firstCall = true
|
||||
|
||||
fun simulateGenerateKeyDelay(algorithm: Int, elapsedNanos: Long) {
|
||||
val elapsedMs = elapsedNanos / 1_000_000.0
|
||||
val targetMs = sampleTotalDelay(algorithm)
|
||||
val remainingMs = targetMs - elapsedMs
|
||||
|
||||
if (remainingMs > 1.0) {
|
||||
LockSupport.parkNanos((remainingMs * 1_000_000).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
private fun sampleTotalDelay(algorithm: Int): Double {
|
||||
val base = sampleBaseCryptoDelay(algorithm)
|
||||
val transit = sampleExponential(2.5)
|
||||
val jitter = (rng.nextGaussian() * 2.5).coerceIn(-8.0, 12.0)
|
||||
|
||||
var cold = 0.0
|
||||
if (firstCall) {
|
||||
firstCall = false
|
||||
cold = coldPenaltyMs
|
||||
}
|
||||
|
||||
return max(20.0, base + transit + jitter + sessionBiasMs + cold)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log-normal base delay. Parameters tuned to match observed hardware profiles:
|
||||
* EC P-256 on QTEE averages ~65ms, RSA-2048 ~75ms, AES ~40ms.
|
||||
* Sigma kept low (0.08) to match the tight clustering seen in real measurements.
|
||||
*/
|
||||
private fun sampleBaseCryptoDelay(algorithm: Int): Double {
|
||||
val (mu, sigma) =
|
||||
when (algorithm) {
|
||||
Algorithm.EC -> ln(60.0) to 0.08
|
||||
Algorithm.RSA -> ln(70.0) to 0.08
|
||||
Algorithm.AES -> ln(35.0) to 0.10
|
||||
else -> ln(40.0) to 0.10
|
||||
}
|
||||
return sampleLogNormal(mu, sigma)
|
||||
}
|
||||
|
||||
private fun sampleLogNormal(mu: Double, sigma: Double): Double {
|
||||
return exp(mu + sigma * rng.nextGaussian())
|
||||
}
|
||||
|
||||
private fun sampleExponential(mean: Double): Double {
|
||||
var u = rng.nextDouble()
|
||||
while (u == 0.0) u = rng.nextDouble()
|
||||
return -mean * ln(u)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,115 @@
|
||||
## TEESimulator-RS v5.1: Interception Architecture Rewrite
|
||||
|
||||
Major release. 27 files changed, 2300 lines rewritten. The entire Kotlin interception layer has been rebuilt with a clean architecture, proper AIDL alignment, and significantly lower binder overhead.
|
||||
|
||||
### Interception Layer Rewrite
|
||||
- KeyMintSecurityLevelInterceptor completely restructured: GeneratedKeyInfo now carries full KeyMintAttestation instead of nullable stub, eliminating scattered null checks across every operation path
|
||||
- SoftwareOperation rewritten with sealed CryptoPrimitive interface separating Signer, Verifier, Encryptor, and Decryptor into isolated implementations with proper JCA algorithm mapping
|
||||
- KeystoreErrorCode centralized object replaces scattered magic numbers for all KeyMint and Keystore2 error codes
|
||||
- listEntries moved from pre-transact parameter caching to post-transact injection, eliminating a race condition where cached params could go stale
|
||||
- deleteKey now handles both APP domain (by alias) and KEY_ID domain (by nspace) resolution paths correctly
|
||||
- AuthorizeCreate and AndroidPermissionUtils removed, authorization logic consolidated into the operation dispatch path
|
||||
- DeviceAttestationService removed, attestation routing simplified into the main interceptor
|
||||
|
||||
### Software Crypto Operations
|
||||
- GCM nonce returned in CreateOperationResponse.parameters for encrypt operations, matching real KeyMint HAL behavior
|
||||
- updateAad correctly throws INVALID_TAG on non-AEAD operations instead of silently succeeding
|
||||
- Cipher algorithm mapping cleaned up: dropped CTR block mode and RSA_PKCS1_1_5_SIGN padding that caused JCA provider mismatches
|
||||
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes instead of raw exceptions
|
||||
|
||||
### Attestation & Certificate Generation
|
||||
- CertificateGenerator rewritten with clean Kotlin Pair return type instead of Android's util.Pair
|
||||
- AttestationBuilder field ordering aligned with AOSP KeyDescription ASN.1 schema
|
||||
- Unique ID computation follows KeyMint HAL spec: HMAC-SHA256(temporal_counter || AAID || reset_flag, HBK) truncated to 128 bits
|
||||
- Patch level logging removed from hot path to reduce logcat noise on every attestation
|
||||
|
||||
### Configuration & Device Properties
|
||||
- ConfigurationManager target package parsing refactored: mode/package extraction deduplicated across GENERATE/PATCH/AUTO branches
|
||||
- system=prop forced boot/vendor override removed, now respects explicit per-component patch level configuration
|
||||
- FileObserver delete handler simplified with direct file access instead of defensive null-checks
|
||||
- AndroidDeviceUtils expanded with additional device property accessors for attestation fields
|
||||
|
||||
### Binder Performance
|
||||
- Safe parcel reads at 6 deserialization sites, replacing force-unwrap NPE paths with early-return on null. A single NPE generates a full stack trace that blocks the binder thread for ~2ms
|
||||
- teeResponses cache populated on generateKey/importKey post-transact, reducing getKeyEntry from 2+ binder round-trips to 1
|
||||
- pingBinder liveness check removed from pre-transact failure path, eliminating a synchronous IPC call on every failed transaction
|
||||
- Native transaction code filtering at C++ level, skipping JNI entirely for PING/INTERFACE/DUMP
|
||||
|
||||
### Dynamic SecurityLevel Binder Registration
|
||||
- Intercepts getSecurityLevel replies to register hooks on every new BBinder instance keystore2 returns, not just the initial one from setup
|
||||
- Identity hash deduplication prevents double-hooking when keystore2 returns the same binder across multiple calls
|
||||
- Resolves apps that call getSecurityLevel independently and receive a different binder than the one registered at startup
|
||||
|
||||
### Build & Packaging
|
||||
- Rust native build task integrated into Gradle with cargo-ndk for aarch64/armv7/x86/x86_64
|
||||
- Module ZIP includes all 4 native libraries (libTEESimulator, libsupervisor, libcertgen, libinject)
|
||||
- customize.sh extraction restored for supervisor daemon and native cert gen library
|
||||
- TeeLatencySimulator added as standalone utility for log-normal hardware latency emulation
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v5.0: AOSP Compliance Overhaul
|
||||
|
||||
Major release integrating 30+ AOSP compliance improvements from upstream PR #157 analysis, layered on top of our StrongBox hardening and native cert gen architecture.
|
||||
|
||||
### Attestation Extension Alignment
|
||||
- 17 enforcement tags added to KeyMintAttestation (ACTIVE_DATETIME, ORIGINATION_EXPIRE, USAGE_EXPIRE, USAGE_COUNT_LIMIT, CALLER_NONCE, UNLOCKED_DEVICE_REQUIRED, INCLUDE_UNIQUE_ID, ROLLBACK_RESISTANCE, EARLY_BOOT_ONLY, ALLOW_WHILE_ON_BODY, TRUSTED_USER_PRESENCE_REQUIRED, TRUSTED_CONFIRMATION_REQUIRED, NO_AUTH_REQUIRED, MAX_USES_PER_BOOT, MAX_BOOT_LEVEL, MIN_MAC_LENGTH, RSA_OAEP_MGF_DIGEST)
|
||||
- BLOCK_MODE encoded as SET OF INTEGER per AOSP attestation_record.h
|
||||
- Version-guarded tags (RSA_OAEP_MGF_DIGEST >=100, ROLLBACK_RESISTANCE >=3, EARLY_BOOT_ONLY >=4)
|
||||
- INCLUDE_UNIQUE_ID computed via HMAC-SHA256 per KeyMint HAL spec using device HBK
|
||||
- AAID gated on attestation challenge presence
|
||||
- Certificate validity defaults aligned with AOSP (epoch notBefore, 9999-12-31 notAfter)
|
||||
|
||||
### Binder Infrastructure
|
||||
- Native transaction code filtering at C++ level, skipping JNI for non-intercepted codes
|
||||
- getNumberOfEntries includes software-generated key count
|
||||
- deleteKey resolves KEY_ID domain via generatedKeys lookup
|
||||
- patchAuthorizations for OS/VENDOR/BOOT patch levels in authorization arrays
|
||||
|
||||
### Software Operation AOSP Conformance
|
||||
- updateAad on non-AEAD operations returns INVALID_TAG (-76), matching AOSP operation.rs
|
||||
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes
|
||||
- GCM IV returned in CreateOperationResponse.parameters for encrypt operations
|
||||
- SoftwareOperationBinder methods @Synchronized, matching AOSP Mutex per operation
|
||||
- authorize_create enforcement: PURPOSE validation, algorithm-purpose compatibility, temporal constraints, CALLER_NONCE prohibition, WRAP_KEY rejection
|
||||
|
||||
### Security and Configuration
|
||||
- SELinux permission checks via /proc/pid/attr/current
|
||||
- Per-UID permission verification through IPackageManager.checkPermission
|
||||
- Imported key tracking prevents stale attest-key overrides in getKeyEntry
|
||||
- nspace consistency fix in attest-key override path
|
||||
- TeeLatencySimulator with log-normal distribution matching real hardware profiles
|
||||
- Device-unique HBK seed generated on install (32 bytes from /dev/random)
|
||||
|
||||
### Preserved from v4.8
|
||||
- StrongBox op limits (4 concurrent max, TOO_MANY_OPERATIONS rejection)
|
||||
- LRU operation pruning per security level
|
||||
- Hardware keygen rate limiting (2/30s sliding window, 2 concurrent cap)
|
||||
- Native Rust cert generation with BouncyCastle fallback
|
||||
- Key persistence across reboots
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.8.1: StrongBox Op Rejection Fix
|
||||
|
||||
- **StrongBox op limit gate fix** — `trackAndEnforceOpLimit` was only called in the `Domain.KEY_ID` not-found path, so software-generated keys (found via `Domain.APP`) bypassed `STRONGBOX_MAX_CONCURRENT_OPS=4` entirely. DuckDetector's concurrent signing handles test created 24+ operations that all succeeded via LRU pruning instead of being rejected with `TOO_MANY_OPERATIONS (-29)`. Now enforced for all StrongBox createOperation paths.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.8: StrongBox Hardening & LRU Pruning
|
||||
|
||||
Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped from 32 to 8.
|
||||
|
||||
- **LRU operation pruning** — Concurrent software operations capped at 15 per UID (TEE) and 4 per UID (StrongBox), with oldest-first eviction. Pruned operations return `INVALID_OPERATION_HANDLE (-28)`, matching AOSP keystore2 malus-based pruning.
|
||||
- **StrongBox param guard** — Unsupported StrongBox params (RSA >2048-bit, non-P256 EC curves) forwarded to real HAL for proper rejection instead of generating in software.
|
||||
- **StrongBox timing** — Key generation floors at 250ms, signing at 80ms on StrongBox security level to match real secure element latency.
|
||||
- **StrongBox op limit** — Sliding-window enforcer caps concurrent StrongBox operations for both software and hardware key paths, returning `TOO_MANY_OPERATIONS (-29)` when exceeded.
|
||||
- **ECDSA algorithm alias** — Accept "ECDSA" in addition to "EC" as JCA private key algorithm name. Fixes SIGSEGV crash on Android 10 devices where the provider reports EC keys as "ECDSA". Closes #4.
|
||||
- **createOperation domain handling** — Software-generated keys now found via both `Domain.APP` (alias) and `Domain.KEY_ID` (nspace) lookup paths.
|
||||
- **Permission guards** — Device ID attestation tags (IMEI, MEID, serial) require caller permission checks.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.7: Operation & Attestation Fixes
|
||||
|
||||
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) and [Key Attestation](https://github.com/nickel-lang/nickel) on OnePlus (Android 16) and Xiaomi Redmi 14C (Android 14).
|
||||
|
||||
@@ -91,3 +91,10 @@ if [ ! -f "$CONFIG_DIR/target.txt" ]; then
|
||||
ui_print "- Adding default target scope"
|
||||
install_file "target.txt" "$CONFIG_DIR"
|
||||
fi
|
||||
|
||||
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||
|
||||
if [ ! -f "$CONFIG_DIR/hbk" ]; then
|
||||
ui_print "- Generating device-unique hardware-bound key seed"
|
||||
head -c 32 /dev/random > "$CONFIG_DIR/hbk"
|
||||
fi
|
||||
|
||||
@@ -33,6 +33,36 @@ pub fn build_attestation_extension(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
|
||||
|
||||
// Tag 303: CALLER_NONCE — NULL (presence = true)
|
||||
if params.caller_nonce {
|
||||
fields.push((303, enc_null()));
|
||||
}
|
||||
|
||||
// Tag 400: ACTIVE_DATETIME — INTEGER (milliseconds)
|
||||
if params.active_datetime >= 0 {
|
||||
fields.push((400, enc_integer(params.active_datetime)));
|
||||
}
|
||||
|
||||
// Tag 401: ORIGINATION_EXPIRE_DATETIME — INTEGER (milliseconds)
|
||||
if params.origination_expire_datetime >= 0 {
|
||||
fields.push((401, enc_integer(params.origination_expire_datetime)));
|
||||
}
|
||||
|
||||
// Tag 402: USAGE_EXPIRE_DATETIME — INTEGER (milliseconds)
|
||||
if params.usage_expire_datetime >= 0 {
|
||||
fields.push((402, enc_integer(params.usage_expire_datetime)));
|
||||
}
|
||||
|
||||
// Tag 405: USAGE_COUNT_LIMIT — INTEGER
|
||||
if params.usage_count_limit >= 0 {
|
||||
fields.push((405, enc_integer(params.usage_count_limit as i64)));
|
||||
}
|
||||
|
||||
// Tag 509: UNLOCKED_DEVICE_REQUIRED — NULL
|
||||
if params.unlocked_device_required {
|
||||
fields.push((509, enc_null()));
|
||||
}
|
||||
|
||||
// Tag 701: CREATION_DATETIME — INTEGER (milliseconds)
|
||||
fields.push((701, enc_integer(params.creation_datetime)));
|
||||
|
||||
@@ -77,8 +107,10 @@ fn build_tee_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
fields.push((10, enc_integer(curve as i32 as i64)));
|
||||
}
|
||||
|
||||
// Tag 503: NO_AUTH_REQUIRED — NULL (presence = true)
|
||||
// Tag 503: NO_AUTH_REQUIRED — NULL (conditional)
|
||||
if params.no_auth_required {
|
||||
fields.push((503, enc_null()));
|
||||
}
|
||||
|
||||
// Tag 702: ORIGIN — INTEGER 0 (GENERATED)
|
||||
fields.push((702, enc_integer(0)));
|
||||
@@ -519,6 +551,44 @@ mod tests {
|
||||
assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enforcement_tags_in_software_enforced() {
|
||||
let mut params = make_test_params();
|
||||
params.usage_count_limit = 3;
|
||||
params.unlocked_device_required = true;
|
||||
params.caller_nonce = true;
|
||||
params.active_datetime = 1709913600000;
|
||||
let sw = build_software_enforced(¶ms).unwrap();
|
||||
let inner = skip_tlv_header(&sw);
|
||||
let tags = extract_tag_numbers(inner);
|
||||
assert!(tags.contains(&303), "CALLER_NONCE (303) must be in softwareEnforced");
|
||||
assert!(tags.contains(&400), "ACTIVE_DATETIME (400) must be in softwareEnforced");
|
||||
assert!(tags.contains(&405), "USAGE_COUNT_LIMIT (405) must be in softwareEnforced");
|
||||
assert!(tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED (509) must be in softwareEnforced");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_auth_required_conditional() {
|
||||
let mut params = make_test_params();
|
||||
params.no_auth_required = false;
|
||||
let tee = build_tee_enforced(¶ms).unwrap();
|
||||
let inner = skip_tlv_header(&tee);
|
||||
let tags = extract_tag_numbers(inner);
|
||||
assert!(!tags.contains(&503), "NO_AUTH_REQUIRED (503) must be absent when false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enforcement_tags_omitted_when_unset() {
|
||||
let params = make_test_params();
|
||||
let sw = build_software_enforced(¶ms).unwrap();
|
||||
let inner = skip_tlv_header(&sw);
|
||||
let tags = extract_tag_numbers(inner);
|
||||
assert!(!tags.contains(&303), "CALLER_NONCE should be absent when false");
|
||||
assert!(!tags.contains(&400), "ACTIVE_DATETIME should be absent when -1");
|
||||
assert!(!tags.contains(&405), "USAGE_COUNT_LIMIT should be absent when -1");
|
||||
assert!(!tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED should be absent when false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_extension_roundtrip() {
|
||||
let params = make_test_params();
|
||||
@@ -568,6 +638,13 @@ mod tests {
|
||||
id_manufacturer: None,
|
||||
id_model: None,
|
||||
id_second_imei: None,
|
||||
active_datetime: -1,
|
||||
origination_expire_datetime: -1,
|
||||
usage_expire_datetime: -1,
|
||||
usage_count_limit: -1,
|
||||
caller_nonce: false,
|
||||
unlocked_device_required: false,
|
||||
no_auth_required: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +199,14 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
|
||||
let id_model = get_nullable_byte_array(env, config, "idModel")?;
|
||||
let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?;
|
||||
|
||||
let active_datetime = get_long(env, config, "activeDatetime")?;
|
||||
let origination_expire_datetime = get_long(env, config, "originationExpireDatetime")?;
|
||||
let usage_expire_datetime = get_long(env, config, "usageExpireDatetime")?;
|
||||
let usage_count_limit = get_int(env, config, "usageCountLimit")?;
|
||||
let caller_nonce = get_boolean(env, config, "callerNonce")?;
|
||||
let unlocked_device_required = get_boolean(env, config, "unlockedDeviceRequired")?;
|
||||
let no_auth_required = get_boolean(env, config, "noAuthRequired")?;
|
||||
|
||||
Ok(CertGenParams {
|
||||
algorithm: Algorithm::try_from(algorithm)?,
|
||||
key_size: key_size as u32,
|
||||
@@ -238,6 +246,13 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
|
||||
id_manufacturer,
|
||||
id_model,
|
||||
id_second_imei,
|
||||
active_datetime,
|
||||
origination_expire_datetime,
|
||||
usage_expire_datetime,
|
||||
usage_count_limit,
|
||||
caller_nonce,
|
||||
unlocked_device_required,
|
||||
no_auth_required,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -253,6 +268,10 @@ fn get_long(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i64> {
|
||||
Ok(env.get_field(obj, name, "J")?.j()?)
|
||||
}
|
||||
|
||||
fn get_boolean(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<bool> {
|
||||
Ok(env.get_field(obj, name, "Z")?.z()?)
|
||||
}
|
||||
|
||||
fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> {
|
||||
let field = env.get_field(obj, name, "[B")?.l()?;
|
||||
if field.is_null() {
|
||||
|
||||
@@ -42,35 +42,6 @@ impl TryFrom<i32> for EcCurve {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum KeyPurpose {
|
||||
Encrypt = 0,
|
||||
Decrypt = 1,
|
||||
Sign = 2,
|
||||
Verify = 3,
|
||||
WrapKey = 5,
|
||||
AgreeKey = 6,
|
||||
AttestKey = 7,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum SecurityLevel {
|
||||
Software = 0,
|
||||
TrustedEnvironment = 1,
|
||||
StrongBox = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum VerifiedBootState {
|
||||
Verified = 0,
|
||||
SelfSigned = 1,
|
||||
Unverified = 2,
|
||||
Failed = 3,
|
||||
}
|
||||
|
||||
pub struct CertGenParams {
|
||||
pub algorithm: Algorithm,
|
||||
pub key_size: u32,
|
||||
@@ -114,6 +85,14 @@ pub struct CertGenParams {
|
||||
pub id_manufacturer: Option<Vec<u8>>,
|
||||
pub id_model: Option<Vec<u8>>,
|
||||
pub id_second_imei: Option<Vec<u8>>,
|
||||
|
||||
pub active_datetime: i64,
|
||||
pub origination_expire_datetime: i64,
|
||||
pub usage_expire_datetime: i64,
|
||||
pub usage_count_limit: i32,
|
||||
pub caller_nonce: bool,
|
||||
pub unlocked_device_required: bool,
|
||||
pub no_auth_required: bool,
|
||||
}
|
||||
|
||||
pub struct GeneratedKeyPair {
|
||||
|
||||
@@ -13,6 +13,8 @@ public interface IPackageManager {
|
||||
|
||||
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
|
||||
|
||||
int checkPermission(String permName, String pkgName, int userId);
|
||||
|
||||
class Stub {
|
||||
public static IPackageManager asInterface(IBinder binder) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package android.hardware.security.keymint;
|
||||
|
||||
public @interface HardwareAuthenticatorType {
|
||||
int NONE = 0;
|
||||
int PASSWORD = 1;
|
||||
int FINGERPRINT = 2;
|
||||
int ANY = -1;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package android.os;
|
||||
|
||||
/** Stub for android.os.SELinux. */
|
||||
public class SELinux {
|
||||
public static boolean checkSELinuxAccess(
|
||||
String scon, String tcon, String tclass, String perm) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ public class ServiceManager {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
public static boolean isDeclared(String name) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
public static String[] listServices() {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
package android.os;
|
||||
|
||||
/**
|
||||
* Stub for android.os.ServiceSpecificException.
|
||||
*
|
||||
* <p>Used by AIDL-generated binder stubs to report service-specific errors with numeric codes.
|
||||
* The binder framework serializes this as EX_SERVICE_SPECIFIC on the wire, preserving the integer
|
||||
* error code for the client.
|
||||
*/
|
||||
public class ServiceSpecificException extends RuntimeException {
|
||||
public final int errorCode;
|
||||
|
||||
public ServiceSpecificException(int errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public ServiceSpecificException(int errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public ServiceSpecificException(int errorCode) {
|
||||
this(errorCode, null);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user