Compare commits

..
27 Commits
Author SHA1 Message Date
Enginex0 ca3978888e docs(release): bump to v4.7 with operation and attestation fixes changelog 2026-03-17 07:12:30 +01:00
Enginex0 023d7f929d fix(operation): match AOSP error-path semantics for software operations
KeyDetector's OperationErrorPathChecker (flag 0x400000) probes three
error-path behaviors that real keystore2 operations expose. Our
SoftwareOperationBinder was missing all three, plus had no updateAad
implementation which caused AbstractMethodError on Android 16 where
the runtime Stub declares it abstract.

SoftwareOperation changes:
- Add finalized state tracking; post-abort calls now throw
  INVALID_OPERATION_HANDLE (-28) matching AOSP operation.rs
- Add input length guard (0x8000) throwing TOO_MUCH_DATA (29)
  matching AOSP operation.rs MAX_RECEIVE_DATA
- Add updateAad to CryptoPrimitive interface and SoftwareOperationBinder
- Add KeystoreErrorCodes with runtime reflection + AOSP fallback values

KeyMintSecurityLevelInterceptor changes:
- Infer algorithm from stored key pair when operation params omit
  ALGORITHM tag, matching AOSP behavior where createOperation uses
  the key's stored algorithm rather than requiring it in op params

Stub addition:
- ServiceSpecificException compile stub (framework-internal class
  resolved at runtime on device)

Tested on OnePlus Android 16 (SDK 36) — KeyDetector passes all three
probes: updateAad succeeds, TOO_MUCH_DATA returns code=21,
INVALID_OPERATION_HANDLE returns after abort.
2026-03-17 07:04:59 +01:00
Enginex0 bd40f4b950 fix(attestation): encode PADDING as SET OF INTEGER per AOSP schema
PADDING (tag 6) is ENUM_REP in Tag.aidl, meaning SET OF INTEGER in the
attestation extension ASN.1 — same as PURPOSE and DIGEST. Commit f8bfa0d
added it as individual [6] INTEGER entries, causing parsers to fail with
CertificateParsingException on any RSA key attestation.
2026-03-17 04:36:49 +01:00
Enginex0 23696d2f61 ci(release): fetch full history for accurate commit count 2026-03-17 03:43:16 +01:00
Enginex0 dfacb34cf9 chore(brand): rebrand to TEESimulator-RS with simplified versioning
Fork identity: rename across module metadata, CI pipeline, and build
scripts. Version scheme changed from v4.5-115-7e87766 to v4.6-117
format — commit count auto-increments, git hash dropped from filenames.
2026-03-17 03:35:32 +01:00
Enginex0 06d9db443c perf(keygen): replace Gaussian RTT normalization with 15ms floor fence
The old Gaussian sleep (mean=55ms, stddev=12ms) triggered detection on
Chunqiu Native Check 2.8. A flat 15ms floor satisfies the minimum RTT
threshold without creating a detectable delay pattern — both attested
and non-attested paths get identical treatment, keeping the D50 ratio
at ~1.0 while staying above the >=15ms requirement.
2026-03-17 03:35:20 +01:00
Enginex0 7e87766493 fix(certgen): derive signing algorithm from attestation key and allow device ID tags
signerAlgorithm was derived from params.algorithm (the generated key)
instead of the signing key, causing BouncyCastle to throw when signing
RSA keys with an EC attestation key. Now reads signingKeyPair.private.algorithm.

Device ID tags (serial/imei/meid/secondImei) were blanket-rejected
instead of flowing through to software cert gen like AOSP does.
Narrowed rejection to DEVICE_UNIQUE_ATTESTATION only.
2026-03-17 00:05:56 +01:00
Enginex0 f8bfa0dfd8 fix(attestation): align authorization list and cert extension with AOSP keystore2 semantics
toAuthorizations() was missing OS_VERSION, OS_PATCHLEVEL, VENDOR_PATCHLEVEL,
BOOT_PATCHLEVEL, CREATION_DATETIME, USER_ID, PADDING, and RSA_PUBLIC_EXPONENT
tags that real TEE-generated KeyMetadata always includes. EC_CURVE was also
hardcoded unconditionally, producing invalid authorizations for RSA keys.

Additionally, live-patched certificate chains in getKeyEntry weren't cached,
causing re-patching on every call with potentially different signatures.

Ports upstream JingMatrix/TEESimulator#148 and #150.
2026-03-16 23:01:28 +01:00
Enginex0 90ff59e0aa fix(interception): check generatedKeys before deletedSoftwareKeys on getKeyEntry
The deletion guard must not shadow re-generated keys. If an app
deletes a key then re-creates it, getKeyEntry was still returning
KEY_NOT_FOUND because deletedSoftwareKeys was checked first.
2026-03-16 22:36:02 +01:00
Enginex0 8bdf0d59fa docs(release): bump to v4.5 with detection hardening changelog 2026-03-16 22:07:56 +01:00
Enginex0 6ab09f4889 fix(interception): prevent ghost key responses after software key deletion
After deleting a software-generated key, getKeyEntry was falling
through to the real keystore2 service which could return a stale
hardware key with the same alias. The post-transact live-patch
fallback would then resurrect the key with a patched chain —
detectors flag this as binder inconsistency.

Track deleted software key aliases and return KEY_NOT_FOUND (7) for
subsequent getKeyEntry calls. Also always invoke cleanupKeyData on
delete to clear stale patchedChains entries for hardware keys.
2026-03-16 22:06:53 +01:00
Enginex0 f4559bcd19 perf(keygen): normalize software generateKey RTT to match TEE latency
Software-generated keys complete in ~4ms, real TEE averages 55-65ms
with a floor around 15ms. Detectors measure this RTT to distinguish
software from hardware paths. Gaussian delay sampling (mean=55ms,
σ=12ms, floor=15ms) brings total RTT into the expected range.
2026-03-16 22:06:38 +01:00
Enginex0 3b5043a1bb docs(release): bump to v4.4 with AOSP conformance changelog 2026-03-16 13:26:34 +01:00
Enginex0 8001a8678a fix(interception): absorb upstream correctness fixes and patch error reply format
Cherry-pick three upstream fixes: Parcel position reset in hasException()
so the method doesn't consume reply data (7804743), list_past_alias
enumeration filter inversion (2aac65c), and KeyMetadata alignment with
AOSP semantics — modificationTimeMs, Tag.ORIGIN, KeyDescriptor
normalization (86db5bf).

Additionally, createErrorReply() was missing the empty remote stack
trace header int between the exception message and error code, per
AOSP Status.cpp:196. Binder readers expecting the standard
EX_SERVICE_SPECIFIC wire format would misparse our error replies.
2026-03-16 13:23:36 +01:00
Enginex0 d21822eb9d docs(release): bump to v4.3 with changelog and update metadata 2026-03-11 13:12:03 +01:00
Enginex0 095a658996 ci(build): fix pipeline trigger and release job gating
paths-ignore for .github/** was preventing workflow-only pushes
from triggering the pipeline at all. Release job was gated to
push events only, so workflow_dispatch never published. Simplify
paths-ignore to just **.md and allow both push and dispatch to
trigger the release job.
2026-03-11 13:03:35 +01:00
Enginex0 70e8968e44 ci(build): use mv instead of cp to avoid duplicate artifacts
cp left the original zip alongside the renamed copy, so the glob
matched both — doubling artifact size. mv removes the original.
2026-03-11 12:51:02 +01:00
Enginex0 c122ded7bf perf(daemon): add restart backoff, process priority, and map eviction
Supervisor had zero-delay restart on crash loops — pins CPU core at
100% if daemon keeps dying. Add exponential backoff (500ms to 30s cap,
resets after 30s stable). Set nice=10 on daemon child to yield CPU
to foreground apps. Evict stale entries from fileLocks and rate limiter
ConcurrentHashMaps that grew unbounded. Upload pre-built flashable zips
in CI instead of unpacking and re-compressing loose files.
2026-03-11 12:41:57 +01:00
Enginex0 7b510a9915 perf(logging): gate debug-level logs behind isDebugBuild
debug() was hitting Log.d() unconditionally in release builds —
every intercepted binder transaction triggered string formatting
and logcat syscalls. verbose() already had the guard; debug() was
just missing it. Also remove dead SERVICE_SLEEP_MS constant.
2026-03-11 12:41:46 +01:00
Enginex0 8f63dda31b ci(build): add release job with changelog and both ZIPs
The workflow only uploaded unzipped contents as CI artifacts —
no GitHub release was ever created from CI. Restructured into
build + release jobs: build produces both debug and release ZIPs
(renamed to clean `TEESimulator-vX.Y-{Variant}.zip` format),
release extracts changelog from module/changelog.md and publishes
a GitHub release with both ZIPs attached.
2026-03-11 04:06:20 +01:00
Enginex0 9896df93de build(gradle): keep debug symbols in debug variant
Debug ZIPs now ship unstripped native libs sourced from
merged_native_libs instead of stripped_native_libs. Gives
meaningful stack traces for crash debugging on-device.
2026-03-11 00:45:00 +01:00
Enginex0 0280bcf189 docs(readme): add build badge and building-from-source section 2026-03-11 00:28:28 +01:00
Enginex0 438a462bdf ci(build): add Rust toolchain and cargo-ndk for native-certgen
Gradle's buildRustCertgen task requires cargo-ndk and Android NDK
targets to cross-compile libcertgen.so. Without these, CI fails on
any commit after 32cfcb3 which wired the Rust crate into the pipeline.
2026-03-11 00:12:02 +01:00
Enginex0 40b08cd648 docs(release): bump to v4.2 with changelog and update metadata 2026-03-10 16:55:39 +01:00
Enginex0 c5ed627f68 chore(module): bump versionCode to 95 2026-03-10 16:37:58 +01:00
Enginex0 bee73eb39b perf(binder): skip interception for system transaction codes
AIDL methods use codes 1..0x00ffffff. System transactions like
PING_TRANSACTION (0x5f4e4750) fall above that range. Intercepting
pings forces a full JNI round-trip to Java and back, adding enough
latency for timing detectors to flag the ratio (3.85x vs 3.0x
threshold). Early-return for codes above LAST_CALL_TRANSACTION
eliminates this overhead while preserving all AIDL interception.
2026-03-10 16:37:50 +01:00
Enginex0 a0ee77202c fix(attestation): correct leaf CN casing and enforce keystore2 parameter policy
Leaf cert Subject CN used "KeyStore" (capital S) but AOSP
KeyGenParameterSpec uses "Keystore" (lowercase s). Fixed in both
the Rust native certgen and BouncyCastle paths.

Replicate keystore2's security_level.rs parameter validation for
software-generated keys: reject CREATION_DATETIME (output-only tag,
ResponseCode 20) and device ID attestation tags (CANNOT_ATTEST_IDS
-66) that real keystore2 blocks before they reach the HAL.

Also fix createErrorReply parcel write order — AIDL protocol expects
exception_code, message, error_code but we had message and error_code
swapped, causing malformed replies for positive error codes.
2026-03-10 14:23:33 +01:00
25 changed files with 447 additions and 149 deletions
+92 -73
View File
@@ -3,16 +3,10 @@ name: Build
on:
push:
branches: [ "main" ]
paths-ignore:
- '**.md'
- '.github/**'
- '!.github/workflows/**'
paths-ignore: [ '**.md' ]
pull_request:
branches: [ "main" ]
paths-ignore:
- '**.md'
- '.github/**'
- '!.github/workflows/**'
paths-ignore: [ '**.md' ]
workflow_dispatch:
concurrency:
@@ -22,18 +16,9 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write
attestations: write
contents: read
outputs:
releaseName: ${{ steps.prepareArtifact.outputs.releaseName }}
debugName: ${{ steps.prepareArtifact.outputs.debugName }}
steps:
- name: Check out
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
submodules: "recursive"
fetch-depth: 0
@@ -45,6 +30,25 @@ jobs:
java-version: 21
cache: 'gradle'
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android
- name: Cache Rust artifacts
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cargo/bin/cargo-ndk
native-certgen/target
key: rust-${{ runner.os }}-${{ hashFiles('native-certgen/Cargo.lock') }}
restore-keys: rust-${{ runner.os }}-
- name: Install cargo-ndk
run: command -v cargo-ndk || cargo install cargo-ndk
- name: Set up ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
@@ -60,73 +64,88 @@ jobs:
- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m
- name: Prepare artifact
if: success()
id: prepareArtifact
- name: Read version
id: ver
run: |
set -e
RELEASE_FILE=$(find out -name "*Release*.zip" | head -1)
DEBUG_FILE=$(find out -name "*Debug*.zip" | head -1)
if [[ -z "$RELEASE_FILE" || -z "$DEBUG_FILE" ]]; then
echo "Error: Could not find release or debug files in out/"
echo "Contents of out/ directory:"
ls -la out/ || echo "out/ directory does not exist"
exit 1
fi
# Extract names
RELEASE_NAME=$(basename "$RELEASE_FILE" .zip)
DEBUG_NAME=$(basename "$DEBUG_FILE" .zip)
echo "releaseName=$RELEASE_NAME" >> $GITHUB_OUTPUT
echo "debugName=$DEBUG_NAME" >> $GITHUB_OUTPUT
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
count=$(git rev-list HEAD --count)
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
mkdir -p module-release module-debug
unzip -q "$RELEASE_FILE" -d module-release
unzip -q "$DEBUG_FILE" -d module-debug
echo " Release: $RELEASE_NAME"
echo " Debug: $DEBUG_NAME"
- name: List build artifacts
run: |
echo "Release: $(ls out/*Release*.zip | head -1) ($(du -h out/*Release*.zip | head -1 | cut -f1))"
echo "Debug: $(ls out/*Debug*.zip | head -1) ($(du -h out/*Debug*.zip | head -1 | cut -f1))"
- name: Upload release
if: success()
id: release
uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v4
with:
name: ${{ steps.prepareArtifact.outputs.releaseName }}
path: "./module-release/*"
name: TEESimulator-RS-release-zip
path: out/TEESimulator-RS-*-Release.zip
retention-days: 30
compression-level: 6
compression-level: 0
- name: Upload debug
if: success()
id: debug
uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v4
with:
name: ${{ steps.prepareArtifact.outputs.debugName }}
path: "./module-debug/*"
name: TEESimulator-RS-debug-zip
path: out/TEESimulator-RS-*-Debug.zip
retention-days: 7
compression-level: 6
compression-level: 0
- name: Upload release mappings
if: success()
uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v4
with:
name: release-mappings-${{ github.run_number }}
path: "./app/build/outputs/mapping/release"
name: release-mappings
path: app/build/outputs/mapping/release
retention-days: 30
compression-level: 9
- name: Summary
if: always()
release:
needs: build
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Read version
id: ver
run: |
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
echo "- **Gradle Tasks**: assembleRelease, assembleDebug" >> $GITHUB_STEP_SUMMARY
if [[ "${{ job.status }}" == "success" ]]; then
echo "- **Release Artifact**: ${{ steps.prepareArtifact.outputs.releaseName }}" >> $GITHUB_STEP_SUMMARY
echo "- **Debug Artifact**: ${{ steps.prepareArtifact.outputs.debugName }}" >> $GITHUB_STEP_SUMMARY
fi
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
count=$(git rev-list HEAD --count)
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
- uses: actions/download-artifact@v4
with:
name: TEESimulator-RS-release-zip
path: zips
- uses: actions/download-artifact@v4
with:
name: TEESimulator-RS-debug-zip
path: zips
- name: Extract changelog
run: |
ver="${VER#v}"
awk "/^## TEESimulator-RS v${ver%%-*}/{flag=1; next} /^## TEESimulator-RS v/{if(flag) exit} flag" module/changelog.md > /tmp/notes.md
cat /tmp/notes.md
env:
VER: ${{ steps.ver.outputs.version }}
- name: Create release
run: |
gh release delete "$VER" --yes 2>/dev/null || true
RELEASE=$(ls zips/*Release*.zip | head -1)
DEBUG=$(ls zips/*Debug*.zip | head -1)
gh release create "$VER" \
--title "$VER" \
--latest \
--notes-file /tmp/notes.md \
"$RELEASE" \
"$DEBUG"
env:
VER: ${{ steps.ver.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+20 -1
View File
@@ -3,7 +3,8 @@
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
<p align="center">Hardware attestation. Software keys. Zero detection.</p>
<p align="center">
<img src="https://img.shields.io/badge/version-v4.0-blue?style=for-the-badge" alt="v4.0">
<a href="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml/badge.svg" alt="Build"></a>
<img src="https://img.shields.io/badge/version-v4.2-blue?style=for-the-badge" alt="v4.2">
<img src="https://img.shields.io/badge/Android-10%2B-green?style=for-the-badge&logo=android" alt="Android 10+">
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
</p>
@@ -112,6 +113,24 @@ TEESimulator replaces TrickyStore, TrickyStoreOSS, and their forks. Existing con
---
## 🔨 Building from Source
The CI workflow builds on every push to `main`. You can also build locally or trigger a build from your own fork.
**Prerequisites:** JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
```bash
git clone https://github.com/Enginex0/TEESimulator.git
cd TEESimulator
./gradlew zipRelease zipDebug
```
Output ZIPs land in `out/`. The Gradle build automatically invokes `cargo ndk` to cross-compile `libcertgen.so` before packaging.
To rebuild from a fork, push to `main` or use **Actions → Build → Run workflow**. The workflow installs all toolchains (Java, Rust, cargo-ndk, ccache) and uploads Release + Debug ZIPs as artifacts.
---
## ⚙️ Configuration
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
+17 -17
View File
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v4.1"
val verName = "v4.7"
android {
namespace = "org.matrix.TEESimulator"
@@ -73,7 +73,7 @@ dependencies {
// --- Rust native cert gen build task ---
val buildRustCertgen by tasks.registering(Exec::class) {
group = "TEESimulator Native Build"
group = "TEESimulator-RS Native Build"
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
workingDir = rootProject.projectDir.resolve("native-certgen")
@@ -108,21 +108,21 @@ androidComponents {
// --- Define output locations and file names ---
// Stage all files in a temporary directory inside 'build' before zipping
val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}")
val zipFileName = "TEESimulator-$verName-$gitCommitCount-$gitCommitHash-$capitalized.zip"
val zipFileName = "TEESimulator-RS-$verName-$gitCommitCount-$capitalized.zip"
// Task 1: Prepare all module files in the temporary build directory.
// Using Sync ensures that stale files from previous runs are removed.
val prepareModuleFilesTask =
tasks.register<Sync>("prepareModuleFiles${capitalized}") {
group = "TEESimulator Module Packaging"
group = "TEESimulator-RS Module Packaging"
description = "Prepares all files for the ${variant.name} module zip."
if (isDebug) {
dependsOn("package${capitalized}")
} else {
dependsOn("minify${capitalized}WithR8")
dependsOn("strip${capitalized}DebugSymbols")
}
dependsOn("strip${capitalized}DebugSymbols")
dependsOn(buildRustCertgen)
if (isDebug) {
@@ -140,12 +140,13 @@ androidComponents {
}
}
from(
project.layout.buildDirectory.dir(
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
)
) {
into("lib") // Place them in the 'lib' subfolder of the staging directory.
val nativeLibsDir = if (isDebug) {
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
} else {
"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")
}
@@ -161,8 +162,7 @@ androidComponents {
// Use expand() for simple key-value replacement.
expand(
"REPLACEMEVERCODE" to gitCommitCount.toString(),
"REPLACEMEVER" to
"$verName ($gitCommitCount-$gitCommitHash-${variant.name})",
"REPLACEMEVER" to "$verName-$gitCommitCount",
)
}
@@ -173,7 +173,7 @@ androidComponents {
// Task 2: Zip the prepared files from the temporary directory.
val zipTask =
tasks.register<Zip>("zip${capitalized}") {
group = "TEESimulator Module Packaging"
group = "TEESimulator-RS Module Packaging"
description = "Creates the flashable zip for the ${variant.name} module."
dependsOn(prepareModuleFilesTask)
@@ -186,7 +186,7 @@ androidComponents {
fun createInstallTasks(rootProvider: String, installCli: String) {
val pushTask =
tasks.register<Exec>("push${rootProvider}Module${capitalized}") {
group = "TEESimulator Module Installation"
group = "TEESimulator-RS Module Installation"
description =
"Pushes the ${variant.name} module to the device for $rootProvider."
dependsOn(zipTask)
@@ -200,7 +200,7 @@ androidComponents {
val installTask =
tasks.register<Exec>("install${rootProvider}${capitalized}") {
group = "TEESimulator Module Installation"
group = "TEESimulator-RS Module Installation"
description = "Installs the ${variant.name} module via $rootProvider."
dependsOn(pushTask)
commandLine(
@@ -213,7 +213,7 @@ androidComponents {
}
tasks.register<Exec>("install${rootProvider}AndReboot${capitalized}") {
group = "TEESimulator Module Installation"
group = "TEESimulator-RS Module Installation"
description = "Installs the ${variant.name} module via $rootProvider and reboots."
dependsOn(installTask)
commandLine("adb", "reboot")
+6
View File
@@ -358,6 +358,12 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
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.
if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode)
return;
bool hijack = false;
ThreadTransactionInfo info;
+20 -1
View File
@@ -2,11 +2,13 @@
#include <unistd.h>
#include <sys/wait.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <time.h>
static volatile sig_atomic_t should_exit = 0;
@@ -27,7 +29,12 @@ int main(int argc, char *argv[]) {
const char *daemon_path = argv[1];
char **daemon_argv = &argv[1];
int backoff_ms = 500;
while (!should_exit) {
struct timespec child_start;
clock_gettime(CLOCK_MONOTONIC, &child_start);
pid_t pid = fork();
if (pid < 0) {
@@ -39,6 +46,7 @@ int main(int argc, char *argv[]) {
if (pid == 0) {
// Child: become the daemon
prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies
setpriority(PRIO_PROCESS, 0, 10); // lower CPU priority than foreground
execv(daemon_path, daemon_argv);
perror("execv failed");
_exit(127);
@@ -50,7 +58,18 @@ int main(int argc, char *argv[]) {
if (should_exit) break;
// Instant restart - no delay
// Exponential backoff on rapid crashes, reset if child was stable
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long lived_ms = (now.tv_sec - child_start.tv_sec) * 1000 +
(now.tv_nsec - child_start.tv_nsec) / 1000000;
if (lived_ms > 30000) {
backoff_ms = 500;
} else {
usleep(backoff_ms * 1000);
if (backoff_ms < 30000) backoff_ms *= 2;
}
}
return 0;
@@ -23,8 +23,6 @@ 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.
@@ -182,11 +182,40 @@ object AttestationBuilder {
AttestationConstants.TAG_DIGEST,
DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
),
)
if (params.ecCurve != null) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_EC_CURVE,
ASN1Integer(params.ecCurve.toLong()),
),
)
)
}
if (params.padding.isNotEmpty()) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_PADDING,
DERSet(params.padding.map { ASN1Integer(it.toLong()) }.toTypedArray()),
)
)
}
if (params.rsaPublicExponent != null) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_RSA_PUBLIC_EXPONENT,
ASN1Integer(params.rsaPublicExponent.toLong()),
)
)
}
list.addAll(
listOf(
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
DERTaggedObject(
true,
@@ -199,6 +228,7 @@ object AttestationBuilder {
buildRootOfTrust(null),
),
)
)
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
val simulatedProperties = getSimulatedHardwareProperties(uid)
@@ -89,5 +89,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 // kMaximumAttestationChallengeLength
const val CHALLENGE_LENGTH_LIMIT = 128
}
@@ -19,7 +19,7 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
data class KeyMintAttestation(
val keySize: Int,
val algorithm: Int,
val ecCurve: Int,
val ecCurve: Int?,
val ecCurveName: String,
val origin: Int?,
val blockMode: List<Int>,
@@ -53,7 +53,7 @@ data class KeyMintAttestation(
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
// AOSP: [key_param(tag = EC_CURVE, field = EcCurve)]
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
ecCurve = params.findEcCurve(Tag.EC_CURVE),
ecCurveName = params.deriveEcCurveName(),
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
@@ -17,8 +17,9 @@ object InterceptorUtils {
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
val parcel = Parcel.obtain().apply {
writeInt(EX_SERVICE_SPECIFIC)
writeInt(errorCode)
writeString(null)
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
writeInt(errorCode)
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
@@ -119,6 +120,8 @@ object InterceptorUtils {
/** Checks if a reply parcel contains an exception without consuming it. */
fun hasException(reply: Parcel): Boolean {
return runCatching { reply.readException() }.exceptionOrNull() != null
val exception = runCatching { reply.readException() }.exceptionOrNull()
if (exception != null) reply.setDataPosition(0)
return exception != null
}
}
@@ -10,6 +10,7 @@ import android.system.keystore2.KeyDescriptor
import android.system.keystore2.KeyEntryResponse
import java.security.SecureRandom
import java.security.cert.Certificate
import java.util.concurrent.ConcurrentHashMap
import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager
@@ -54,6 +55,9 @@ 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()
override val serviceName = "android.system.keystore2.IKeystoreService/default"
override val processName = "keystore2"
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
@@ -156,8 +160,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
val keyId = KeyIdentifier(callingUid, descriptor.alias)
if (code == DELETE_KEY_TRANSACTION) {
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
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."
)
@@ -166,9 +172,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
return TransactionResult.ContinueAndSkipPost
}
val response =
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
?: return TransactionResult.Continue
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
}
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
SystemLogger.info("${descriptor.alias} was an attestation key")
@@ -294,7 +305,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
certChain = keyData.second,
algorithm = parsedParameters.algorithm,
keySize = parsedParameters.keySize,
ecCurve = parsedParameters.ecCurve,
ecCurve = parsedParameters.ecCurve ?: 0,
purposes = parsedParameters.purpose,
digests = parsedParameters.digest,
isAttestationKey = true,
@@ -326,6 +337,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
)
finalChain =
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
}
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
@@ -129,7 +129,7 @@ object ListEntriesHandler {
startPastAlias: String?,
): List<KeyDescriptor> {
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
.filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) }
.filter { it.uid == uid && (startPastAlias == null || it.alias > startPastAlias) }
.map { keyId ->
KeyDescriptor().apply {
this.domain = Domain.APP
@@ -129,6 +129,7 @@ object GeneratedKeyPersistence {
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
if (file.exists()) {
if (file.delete()) {
fileLocks.remove(keyFileName(keyId.uid, keyId.alias))
SystemLogger.debug("Deleted persisted key: $keyId")
} else {
SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
@@ -158,6 +159,7 @@ object GeneratedKeyPersistence {
if (file.delete()) count++
}
}
fileLocks.clear()
SystemLogger.info("Deleted $count persisted key files")
}.onFailure { e ->
SystemLogger.error("Failed to delete all persisted keys", e)
@@ -3,6 +3,7 @@ package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.Tag
import android.os.IBinder
import android.os.Parcel
@@ -217,7 +218,14 @@ class KeyMintSecurityLevelInterceptor(
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params)
val parsedParams = KeyMintAttestation(params).let { p ->
if (p.algorithm != 0) p
else p.copy(algorithm = when (generatedKeyInfo.keyPair.private.algorithm) {
"EC" -> Algorithm.EC
"RSA" -> Algorithm.RSA
else -> p.algorithm
})
}
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
val operationBinder = SoftwareOperationBinder(softwareOperation)
@@ -254,6 +262,16 @@ class KeyMintSecurityLevelInterceptor(
return InterceptorUtils.createErrorReply(KEYMINT_INVALID_INPUT_LENGTH)
}
if (params.any { it.tag == Tag.CREATION_DATETIME }) {
SystemLogger.warning("[TX_ID: $txId] Rejecting CREATION_DATETIME in generateKey params")
return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT)
}
if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION }) {
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid")
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
}
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val isAttestKeyRequest = parsedParams.isAttestKey()
@@ -304,6 +322,7 @@ class KeyMintSecurityLevelInterceptor(
keyId: KeyIdentifier,
isAttestKeyRequest: Boolean,
): TransactionResult {
val startNs = System.nanoTime()
keyDescriptor.nspace = secureRandom.nextLong()
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
@@ -319,7 +338,7 @@ class KeyMintSecurityLevelInterceptor(
} ?: throw Exception("Both native and BouncyCastle cert gen failed.")
cleanupKeyData(keyId)
val response = buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor)
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
if (isAttestKeyRequest) attestationKeys.add(keyId)
@@ -331,12 +350,16 @@ class KeyMintSecurityLevelInterceptor(
certChain = keyData.second.toList(),
algorithm = parsedParams.algorithm,
keySize = parsedParams.keySize,
ecCurve = parsedParams.ecCurve,
ecCurve = parsedParams.ecCurve ?: 0,
purposes = parsedParams.purpose,
digests = parsedParams.digest,
isAttestationKey = isAttestKeyRequest,
)
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
val delayMs = TEE_LATENCY_FLOOR_MS - elapsedMs
if (delayMs > 0) Thread.sleep(delayMs)
return InterceptorUtils.createTypedObjectReply(response.metadata)
}
@@ -365,7 +388,7 @@ class KeyMintSecurityLevelInterceptor(
val config = CertGenConfig(
algorithm = params.algorithm,
keySize = params.keySize,
ecCurve = params.ecCurve,
ecCurve = params.ecCurve ?: 0,
rsaPublicExponent = params.rsaPublicExponent?.toLong() ?: 65537L,
attestationChallenge = params.attestationChallenge,
purposes = params.purpose.toIntArray(),
@@ -409,16 +432,25 @@ class KeyMintSecurityLevelInterceptor(
}
private fun buildKeyEntryResponse(
callingUid: Int,
chain: List<Certificate>,
params: KeyMintAttestation,
descriptor: KeyDescriptor,
): KeyEntryResponse {
val normalizedKeyDescriptor =
KeyDescriptor().apply {
domain = Domain.KEY_ID
nspace = descriptor.nspace
alias = null
blob = null
}
val metadata =
KeyMetadata().apply {
keySecurityLevel = securityLevel
key = descriptor
key = normalizedKeyDescriptor
CertificateHelper.updateCertificateChain(this, chain.toTypedArray()).getOrThrow()
authorizations = params.toAuthorizations(securityLevel)
authorizations = params.toAuthorizations(callingUid, securityLevel)
modificationTimeMs = System.currentTimeMillis()
}
return KeyEntryResponse().apply {
this.metadata = metadata
@@ -495,7 +527,7 @@ class KeyMintSecurityLevelInterceptor(
secondImei = null,
)
val response = buildKeyEntryResponse(certChain, attestation, descriptor)
val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor)
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response)
if (record.isAttestationKey) attestationKeys.add(keyId)
@@ -515,11 +547,13 @@ class KeyMintSecurityLevelInterceptor(
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
private const val MAX_ALIAS_LENGTH = 256 * 1024
private const val KEYMINT_INVALID_INPUT_LENGTH = -21
private const val RESPONSE_INVALID_ARGUMENT = 20
private const val TEE_LATENCY_FLOOR_MS = 15L
private const val KEYMINT_CANNOT_ATTEST_IDS = -66
private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2
// Sliding window: max hardware keygen permits per UID within the burst window
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
private const val BURST_WINDOW_MS = 30_000L
private val uidHardwareKeygenCount = ConcurrentHashMap<Int, AtomicInteger>()
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
@@ -532,6 +566,10 @@ class KeyMintSecurityLevelInterceptor(
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
synchronized(timestamps) {
timestamps.removeAll { now - it > BURST_WINDOW_MS }
if (timestamps.isEmpty()) {
uidKeygenTimestamps.remove(uid, timestamps)
uidHardwareKeygenCount.remove(uid)
}
return timestamps.size
}
}
@@ -565,8 +603,7 @@ class KeyMintSecurityLevelInterceptor(
}
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
// Caches patched chains to prevent re-generation and signature inconsistencies
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
@@ -626,7 +663,10 @@ class KeyMintSecurityLevelInterceptor(
}
}
private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Authorization> {
private fun KeyMintAttestation.toAuthorizations(
callingUid: Int,
securityLevel: Int,
): Array<Authorization> {
val authList = mutableListOf<Authorization>()
fun createAuth(tag: Int, value: KeyParameterValue): Authorization {
@@ -641,13 +681,35 @@ private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Autho
}
}
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
if (this.ecCurve != null) {
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
}
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) }
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
if (this.rsaPublicExponent != null) {
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
}
authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true)))
authList.add(createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED)))
authList.add(createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion)))
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
authList.add(createAuth(Tag.OS_PATCHLEVEL, KeyParameterValue.integer(osPatch)))
}
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
authList.add(createAuth(Tag.VENDOR_PATCHLEVEL, KeyParameterValue.integer(vendorPatch)))
}
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch)))
}
authList.add(createAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())))
authList.add(createAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000)))
return authList.toTypedArray()
}
@@ -6,6 +6,7 @@ import android.hardware.security.keymint.Digest
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.PaddingMode
import android.os.RemoteException
import android.os.ServiceSpecificException
import android.system.keystore2.IKeystoreOperation
import java.security.KeyPair
import java.security.Signature
@@ -17,10 +18,9 @@ import org.matrix.TEESimulator.logging.SystemLogger
// A sealed interface to represent the different cryptographic operations we can perform.
private sealed interface CryptoPrimitive {
fun updateAad(aadInput: ByteArray?) {}
fun update(data: ByteArray?): ByteArray?
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
fun abort()
}
@@ -142,17 +142,11 @@ private class CipherPrimitive(
override fun abort() {}
}
/**
* A software-only implementation of a cryptographic operation. This class acts as a controller,
* delegating to a specific cryptographic primitive based on the operation's purpose.
*/
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
// This now holds the specific strategy object (Signer, Verifier, etc.)
private val primitive: CryptoPrimitive
@Volatile private var finalized = false
init {
// The "Strategy" pattern: choose the implementation based on the purpose.
// For simplicity, we only consider the first purpose listed.
val purpose = params.purpose.firstOrNull()
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
@@ -168,9 +162,28 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
}
}
private fun checkActive() {
if (finalized) throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
}
private fun checkInputLength(data: ByteArray?) {
if (data != null && data.size > MAX_RECEIVE_DATA)
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
}
fun updateAad(aadInput: ByteArray?) {
checkActive()
checkInputLength(aadInput)
primitive.updateAad(aadInput)
}
fun update(data: ByteArray?): ByteArray? {
checkActive()
checkInputLength(data)
try {
return primitive.update(data)
} catch (e: ServiceSpecificException) {
throw e
} catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
throw e
@@ -178,38 +191,66 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
}
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
checkActive()
checkInputLength(data)
try {
val result = primitive.finish(data, signature)
finalized = true
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
return result
} catch (e: ServiceSpecificException) {
throw e
} catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
// Re-throw the exception so the binder can report it to the client.
throw e
}
}
fun abort() {
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", 29)
}
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
}
}
/** The Binder interface for our [SoftwareOperation]. */
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
IKeystoreOperation.Stub() {
@Throws(RemoteException::class)
override fun updateAad(aadInput: ByteArray?) {
operation.updateAad(aadInput)
}
override fun update(input: ByteArray?): ByteArray? {
return operation.update(input)
}
@Throws(RemoteException::class)
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
return operation.finish(input, signature)
}
@Throws(RemoteException::class)
override fun abort() {
operation.abort()
}
@@ -19,6 +19,7 @@ object SystemLogger {
* @param message The message to log.
*/
fun debug(message: String) {
if (!isDebugBuild) return
Log.d(TAG, message)
}
@@ -213,7 +213,7 @@ object CertificateGenerator {
uid: Int,
securityLevel: Int,
): Certificate {
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
val leafNotAfter =
(signingKeyPair.public as? X509Certificate)?.notAfter
?: Date(System.currentTimeMillis() + 31536000000L)
@@ -239,10 +239,10 @@ object CertificateGenerator {
)
val signerAlgorithm =
when (params.algorithm) {
Algorithm.EC -> "SHA256withECDSA"
Algorithm.RSA -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
when (signingKeyPair.private.algorithm) {
"EC" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
}
val contentSigner =
JcaContentSignerBuilder(signerAlgorithm)
+72
View File
@@ -1,3 +1,75 @@
## 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).
- **PADDING encoding** — Fixed ASN.1 encoding of PADDING tag in attestation extension from individual `[6] INTEGER` entries to `[6] SET OF INTEGER`, matching AOSP `attestation_record.h` schema. Broke all RSA key attestation since v4.6.
- **Operation error-path conformance** — Software operations now track finalized state and return `INVALID_OPERATION_HANDLE (-28)` on post-abort calls. Input length guard (32KB) returns `TOO_MUCH_DATA` matching AOSP `operation.rs`. Passes KeyDetector's OperationErrorPathChecker.
- **updateAad support** — Added `updateAad` to `SoftwareOperationBinder`, fixing `AbstractMethodError` on Android 16 where the runtime Stub declares it abstract.
- **Algorithm inference** — `createOperation` now infers algorithm from the stored key pair when operation params omit the ALGORITHM tag, matching AOSP behavior.
---
## TEESimulator-RS v4.6: Rebrand & Detection Fix
- **RTT normalization rework** — Replaced Gaussian sleep (mean=55ms) with a 15ms floor fence. The old approach triggered Chunqiu Native Check 2.8 timing analysis; the floor-only approach satisfies the minimum RTT threshold without creating a detectable delay pattern.
- **Cross-algorithm attestation** — Signing algorithm now derived from the attestation key's actual type, not the generated key's algorithm. Fixes BouncyCastle crash when signing RSA keys with EC attestation keys (Shizuku attestation flow).
- **Device ID attestation** — Serial/IMEI/MEID/secondImei tags now flow through to software cert gen instead of blanket rejection. Only DEVICE_UNIQUE_ATTESTATION is rejected, matching AOSP keystore2 policy.
- **Rebrand to TEESimulator-RS** — Distinguishes this fork from upstream. Version scheme simplified to v{major}.{minor}-{commitCount}.
- **CI streamlined** — Release pipeline uses Gradle-generated filenames directly, eliminating the rename step.
---
## TEESimulator v4.5: Detection Hardening
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) (23-check attestation validator). All keystore-level checks now pass.
- **Key deletion consistency** — After deleting a software-generated key, `getKeyEntry` now correctly returns `KEY_NOT_FOUND` instead of falling through to a stale live-patch fallback. Fixes binder consistency checks that detect ghost key responses.
- **generateKey timing normalization** — Software key generation RTT now matches real TEE latency profile (Gaussian distribution, mean=55ms, floor=15ms). Previously completed in ~4ms, which is an immediate timing side-channel.
- **Delete cleanup scope** — `deleteKey` now clears all cached state (patched chains, attestation keys) regardless of whether the key was software or hardware-generated.
---
## TEESimulator v4.4: AOSP Conformance
- **Binder error reply format** — Aligned EX_SERVICE_SPECIFIC wire layout with AOSP Status.cpp, including the remote stack trace header field.
- **Key enumeration** — Corrected list_past_alias pagination order to match AOSP database.rs semantics.
- **KeyMetadata fields** — Generated key responses now include modificationTimeMs, Tag.ORIGIN, and normalized KeyDescriptor fields per AOSP Keystore2.
- **Parcel handling** — hasException() preserves reply position for downstream consumers.
---
## TEESimulator v4.3: Performance & Reliability
- **Debug log gating** — `SystemLogger.debug()` now skipped entirely in release builds, eliminating unnecessary logcat syscalls on every intercepted transaction.
- **Supervisor backoff** — Exponential restart delay (500ms → 30s cap) prevents CPU spin if the daemon crashes repeatedly. Resets automatically once stable.
- **Process priority** — Daemon runs at nice=10, yielding CPU to foreground apps on constrained devices.
- **Map eviction** — Rate limiter and file lock maps now evict stale entries instead of growing unbounded.
- **CI pipeline** — Single-trigger build→release pipeline with proper changelog extraction and correctly sized artifacts.
---
## TEESimulator v4.2: Detection Evasion Hardening
Fixes 6 detection vectors flagged by attestation validator apps.
### Attestation Policy Enforcement
Replicate AOSP keystore2's `add_required_parameters()` validation that our software keygen path was bypassing:
- **CREATION_DATETIME** — Reject caller-provided input with `INVALID_ARGUMENT (20)`, matching `security_level.rs:424`. Our cert gen still adds its own timestamp, same as real keystore2.
- **Device ID attestation** — Reject ATTESTATION_ID_SERIAL, IMEI, MEID, SECOND_IMEI, and DEVICE_UNIQUE_ATTESTATION with `CANNOT_ATTEST_IDS (-66)`. No consumer app has READ_PRIVILEGED_PHONE_STATE.
- **Error reply format** — Fixed AIDL ServiceSpecificException parcel write order (was errorCode→message, now message→errorCode).
### Certificate Fix
Leaf certificate Subject CN corrected from "Android KeyStore Key" to "Android Keystore Key" (lowercase s), matching AOSP `KeyGenParameterSpec.java:282`. Both Kotlin and Rust paths.
### Binder Timing
Skip interception for system transaction codes (PING, INTERFACE, DUMP) above LAST_CALL_TRANSACTION. Eliminates the JNI round-trip that inflated binder ping ratio to 3.85x (detector threshold: 3.0x).
---
## TEESimulator v4.1: Boot Identity Persistence
Bugfix release. The vbmeta boot key digest was randomizing on every reboot, producing a different RootOfTrust in attestation certificates each boot.
+1 -1
View File
@@ -15,7 +15,7 @@ fi
# --- Version Info ---
VERSION=$(grep_prop version "${TMPDIR}/module.prop")
ui_print "- Installing TEESimulator $VERSION"
ui_print "- Installing TEESimulator-RS $VERSION"
ui_print ""
# --- Architecture Handling ---
+2 -2
View File
@@ -1,7 +1,7 @@
id=tricky_store
name=TEESimulator
name=TEESimulator-RS
version=${REPLACEMEVER}
versionCode=${REPLACEMEVERCODE}
author=JingMatrix, Enginex0
description=Software simulation for Android hardware-backed key pairs with key attestation
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/update.json
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/update.json
+3 -3
View File
@@ -1,6 +1,6 @@
{
"version": "v4.1",
"versionCode": 94,
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.1/TEESimulator-v4.1-Release.zip",
"version": "v4.5",
"versionCode": 111,
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.5/TEESimulator-v4.5-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
}
+1 -1
View File
@@ -51,7 +51,7 @@ fn build_leaf_cert(
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
subject.clone()
} else {
encode_simple_cn_dn("Android KeyStore Key")
encode_simple_cn_dn("Android Keystore Key")
};
// Validity
+1 -1
View File
@@ -235,7 +235,7 @@ print_summary() {
# --- Main ---
echo ""
bold "TEESimulator package pipeline"
bold "TEESimulator-RS package pipeline"
echo ""
[[ "$BUILD_RUST" == true ]] && build_rust
+1 -1
View File
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
}
}
rootProject.name = "TEESimulator"
rootProject.name = "TEESimulator-RS"
include(":stub")
@@ -0,0 +1,14 @@
package android.os;
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;
}
}