Compare commits

..
21 Commits
Author SHA1 Message Date
Enginex0 f50004d9a5 docs(release): write v3.2 changelog and point update.json to fork
Changelog covers all 22 commits since v3.1: rate limiter,
importKey hardening, key persistence, supervisor daemon,
security patch consistency, and lifecycle scripts.
update.json now targets Enginex0/TEESimulator releases.
2026-02-06 23:50:30 +01:00
Enginex0 8c10cf71ce chore(module): add co-author credit, verbose action output, point updates to fork 2026-02-06 23:31:03 +01:00
Enginex0 1ed3d9ad6e fix(install): extract action.sh and uninstall.sh during module install 2026-02-06 23:22:16 +01:00
Enginex0 4107127506 chore(build): bump version to v3.2 2026-02-06 23:17:20 +01:00
Enginex0 e36c4e351c feat(module): add action.sh and uninstall.sh lifecycle scripts
action.sh clears persistent key storage on user trigger.
uninstall.sh kills daemon processes and removes all module
artifacts while preserving target.txt and keybox config.
2026-02-06 22:34:02 +01:00
Enginex0andGKI Builder 1546c3bba0 Derive boot and vendor patch levels from system prop when system=prop
TrickyAddon fetches Pixel bulletin dates for boot/vendor but system=prop
resolves to the real device prop, creating a cross-component date mismatch
on non-Pixel devices. Force all three through the same prop resolution path.
2026-02-06 21:10:59 +01:00
Enginex0andGKI Builder 81a8ce0c60 Rate-limit per-UID hardware keygen and harden importKey eviction
Sliding window limits each UID to 2 hardware generateKey calls per
30s burst window with max 2 concurrent. Overflow falls back to
software cert generation.

importKey post-hook retains patched chains instead of full eviction,
preventing detectors from using generate-then-import to bypass
attestation patching. getKeyEntry serves retained chains for imported
keys that overwrote attested aliases.
2026-02-06 21:10:59 +01:00
Enginex0andGKI Builder 7c4df3e237 Cap interceptable binder payload size at 256KB
Prevents thread starvation from flood attacks targeting the
binder interceptor with oversized payloads.
2026-02-06 21:10:59 +01:00
Enginex0andGKI Builder 1ac08411be Revert "Add X.509 certificate extensions for RFC 5280 compliance"
This reverts commit a11a5e41a2.
2026-02-06 21:10:59 +01:00
Enginex0andGKI Builder a11a5e41a2 Add X.509 certificate extensions for RFC 5280 compliance
- BasicConstraints: CA=false (critical)
- SubjectKeyIdentifier via SHA-1 hash
- AuthorityKeyIdentifier linked to issuer cert
2026-02-06 00:47:32 +01:00
Enginex0andGKI Builder c367aa5efc Add file-level locking to prevent race conditions in key persistence
Per-key ReentrantLock prevents concurrent writes to same key file
2026-02-06 00:31:53 +01:00
Enginex0andGKI Builder 88781ff31d Delegate key re-persistence to GeneratedKeyPersistence layer
Remove duplicate rePersistKeyIfNeeded, use centralized implementation
2026-02-06 00:24:50 +01:00
Enginex0andGKI Builder 409fb5fcc3 Reject oversized aliases to prevent binder buffer exhaustion
MAX_ALIAS_LENGTH (256KB) with 4x safety margin for transaction overhead
2026-02-06 00:24:50 +01:00
Enginex0andGKI Builder 0cf8f70544 fix(pki): strip HTML comments from PEM blocks before parsing
Some upstream keybox sources inject HTML comments inside PEM
certificate blocks. BouncyCastle's PEMParser chokes on these
non-base64 lines, silently failing to load the keybox.

Filter lines starting with <!-- in trimLines() before the content
reaches the PEM parser.
2026-02-06 00:16:07 +01:00
Enginex0andGKI Builder 7ecea09ec6 fix(app): add global uncaught exception handler for clean restart
Individual thread crashes silently kill the thread without bringing
down the process. The half-dead process stays alive but broken, and
the service.sh restart loop never fires.

Install a default uncaught exception handler that logs the error and
calls exitProcess(0), triggering the restart loop for full recovery.
2026-02-06 00:16:07 +01:00
Enginex0andGKI Builder 887c5fc666 fix(config): prevent FileObserver NPE on config file deletion
When a config file is deleted, the event handler sets file=null but
then force-unwraps it with file!! in the when block, crashing the
FileObserver thread. All subsequent config change notifications are
silently lost.

Replace force-unwrap with safe call, log a warning on deletion.
2026-02-06 00:16:07 +01:00
Enginex0andGKI Builder ce0ca18d98 Preserve generated keys across keybox rotation
Only invalidate patched cert chains when keybox changes.
Generated key material is independent and survives rotation.
2026-02-06 00:07:46 +01:00
Enginex0andGKI Builder fa28e9fc71 Integrate key persistence with interceptors
Save keys on generation, restore on daemon startup, delete on cleanup.
Re-persist when cert chain updates via updateSubcomponents.
2026-02-06 00:07:46 +01:00
Enginex0andGKI Builder c3822197b1 Add generated key persistence layer
Persist GENERATE-mode keys to disk so they survive daemon restarts.
Binary format with version header, atomic write via tmp+rename.
2026-02-06 00:07:46 +01:00
Enginex0andGKI Builder f276806096 fix(native): block attestation leak when interceptor service is dead
When the Java interceptor process dies, callback->transact() returns
DEAD_OBJECT but the code fell through to the real keystore, exposing
genuine TEE state to requesting apps.

Add pingBinder() liveness check on pre-transact failure. If the
interceptor is confirmed dead, return DEAD_OBJECT to the caller
instead of forwarding to real hardware. Apps see a transient service
error rather than the actual device attestation state.
2026-02-05 23:57:40 +01:00
Enginex0 9aa4a33c5e Add fork-based supervisor daemon for instant restart 2026-02-05 23:57:24 +01:00
60 changed files with 764 additions and 6963 deletions
+73 -92
View File
@@ -3,10 +3,16 @@ name: Build
on:
push:
branches: [ "main" ]
paths-ignore: [ '**.md' ]
paths-ignore:
- '**.md'
- '.github/**'
- '!.github/workflows/**'
pull_request:
branches: [ "main" ]
paths-ignore: [ '**.md' ]
paths-ignore:
- '**.md'
- '.github/**'
- '!.github/workflows/**'
workflow_dispatch:
concurrency:
@@ -16,9 +22,18 @@ 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:
- uses: actions/checkout@v4
- name: Check out
uses: actions/checkout@v4
with:
submodules: "recursive"
fetch-depth: 0
@@ -30,25 +45,6 @@ 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:
@@ -64,88 +60,73 @@ 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: Read version
id: ver
- name: Prepare artifact
if: success()
id: prepareArtifact
run: |
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
count=$(git rev-list HEAD --count)
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
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
- 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))"
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"
- uses: actions/upload-artifact@v4
- name: Upload release
if: success()
id: release
uses: actions/upload-artifact@v4
with:
name: TEESimulator-RS-release-zip
path: out/TEESimulator-RS-*-Release.zip
name: ${{ steps.prepareArtifact.outputs.releaseName }}
path: "./module-release/*"
retention-days: 30
compression-level: 0
compression-level: 6
- uses: actions/upload-artifact@v4
- name: Upload debug
if: success()
id: debug
uses: actions/upload-artifact@v4
with:
name: TEESimulator-RS-debug-zip
path: out/TEESimulator-RS-*-Debug.zip
name: ${{ steps.prepareArtifact.outputs.debugName }}
path: "./module-debug/*"
retention-days: 7
compression-level: 0
compression-level: 6
- uses: actions/upload-artifact@v4
- name: Upload release mappings
if: success()
uses: actions/upload-artifact@v4
with:
name: release-mappings
path: app/build/outputs/mapping/release
name: release-mappings-${{ github.run_number }}
path: "./app/build/outputs/mapping/release"
retention-days: 30
compression-level: 9
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
- name: Summary
if: always()
run: |
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 }}
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
-6
View File
@@ -1,7 +1 @@
out
.gradle
.kotlin
app/build
build
native-certgen/target
app/src/main/jniLibs
+110 -109
View File
@@ -1,139 +1,140 @@
<p align="center">
<h1 align="center">TEESimulator-RS</h1>
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
<p align="center">
<a href="https://github.com/Enginex0/TEESimulator-RS/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator-RS/actions/workflows/build.yml/badge.svg" alt="Build"></a>
<img src="https://img.shields.io/badge/Android-10%2B-green?logo=android" alt="Android 10+">
<a href="https://t.me/superpowers9"><img src="https://img.shields.io/badge/Telegram-community-blue?logo=telegram" alt="Telegram"></a>
</p>
</p>
# TEESimulator A Full TEE Emulation Framework
---
**TEESimulator** is a system module designed to create a complete, software-based simulation of a hardware-backed Trusted Execution Environment ([TEE](https://source.android.com/docs/security/features/trusty)) for [Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation).
> [!NOTE]
> Fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator) with native Rust certificate generation, key persistence, and AOSP-compliant attestation behavior. For the upstream project, see the original repo.
The project's goal is to move beyond simple certificate patching and build a robust framework that can create and manage virtual, self-consistent cryptographic keys.
## What It Does
## ✨ Core Principles
TEESimulator intercepts Binder IPC at the `ioctl` level inside the `keystore2` process and generates entire certificate chains from scratch, signed by your keybox, with correct attestation extensions. Apps that verify hardware attestation see a legitimate device.
* **Bypass Hardware-Backed Attestation:** The primary goal of this project is to defeat Key Attestation, a security mechanism that allows apps to verify that they are running on a secure, unmodified device. This module provides the tools to bypass these checks on rooted or modified devices.
* **Stateful Emulation:** Instead of patching responses from the real TEE, the ultimate goal is to create and manage virtual keys entirely in a simulated software environment. Any request concerning a virtual key will be handled by the simulator, ensuring perfect consistency without ever touching the real hardware.
* **Architectural Interception:** By hooking low-level Binder IPC calls to the Keystore, the framework can transparently redirect requests for virtual keys to the software-based simulator, while allowing requests for real keys to pass through to the hardware TEE.
* **100% FOSS:** Licensed under GPLv3, ensuring it stays free, auditable, and compliant with open-source laws.
This is not TrickyStore. TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the internals are different: native Rust cert generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and AOSP-spec attestation behavior.
## 📱 Requirements
- Android 10 or above
## Requirements
## 📦 Installation & Configuration
> [!IMPORTANT]
> A valid `keybox.xml` is required for hardware-level attestation. Without one, the module generates software-level certificates that won't pass strict hardware checks.
1. Flash this module via (Magisk / KernelSU / APatch) and reboot. It will replace [TrickyStore](https://github.com/5ec1cff/TrickyStore), [TrickyStoreOSS](https://github.com/beakthoven/TrickyStoreOSS) and their forks.
2. (Optional) Place a hardware-backed `keybox.xml` at `/data/adb/tricky_store/keybox.xml`. This provides the cryptographic "root of trust" for the simulator.
3. (Optional) Customize target packages in `/data/adb/tricky_store/target.txt`.
4. (Optional) Customize the simulated security patch level in `/data/adb/tricky_store/security_patch.txt`.
5. Enjoy!
1. Android 10+
2. Root manager: KernelSU, Magisk, or APatch
3. `keybox.xml` at `/data/adb/tricky_store/keybox.xml`
**All configuration files are monitored and will take effect immediately upon saving.**
## Quick Start
### The `keybox.xml` Root of Trust
1. Download the latest ZIP from [Releases](https://github.com/Enginex0/TEESimulator-RS/releases)
2. Install via your root manager and reboot
3. Place your keybox at `/data/adb/tricky_store/keybox.xml`
4. Configure targets in `/data/adb/tricky_store/target.txt`
5. Verify with Play Integrity or Key Attestation Demo
## Architecture
**Native Cert Generation**`libcertgen.so` generates X.509 chains in Rust using `ring` and manual DER encoding. BouncyCastle fallback for unsupported curves (P-224, P-521, Curve25519).
**Binder Interception** — PLT hook on `ioctl()` in `libc.so` via `lsplt` inside `keystore2`. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions.
**AOSP Compliance** — Self-signed certs for non-attested keys (matching `ta/src/keys.rs`), correct AuthorizationList tag ordering, version-guarded extension fields, `authorize_create` enforcement.
**Key Persistence** — Generated keys survive reboots. File-backed with file-level locking.
**Rate Limiting** — Per-UID hardware keygen cap (2/30s window, 2 concurrent). Overflow falls to software certs.
## Configuration
All config files live at `/data/adb/tricky_store/` and are hot-reloaded via `FileObserver`.
### target.txt
Controls which apps get intercepted and the simulation mode.
| Suffix | Mode |
|--------|------|
| `!` | Force software key generation |
| `?` | Force leaf certificate patching (real TEE key, patched cert) |
| *(none)* | Automatic selection |
Multi-keybox support via `[filename.xml]` headers:
This file provides the master cryptographic identity for the simulator. It contains a private key and a valid, hardware-backed certificate chain from a real device. The simulator uses this to sign the virtual certificates it generates, making them appear legitimate to verifiers.
```xml
<?xml version="1.0"?>
<AndroidAttestation>
<Keybox DeviceID="...">
<Key algorithm="ecdsa|rsa">
<PrivateKey format="pem">...</PrivateKey>
<CertificateChain>...</CertificateChain>
</Key>
</Keybox>
</AndroidAttestation>
```
### Mode and Keybox Configuration (`target.txt`)
TEESimulator currently operates in two primary modes as it transitions towards full emulation.
You can control the simulation mode and the specific keybox.xml file used on a per-package basis.
#### Mode Suffixes
* **`!` → Force Generation Mode:** Creates a complete, software-based virtual key. This is the foundation of the full TEE simulation.
* **`?` → Force Leaf Hacking Mode:** A legacy mode where a real TEE key is generated, but its attestation certificate is intercepted and modified.
* **No symbol → Automatic Mode:** The module selects the most appropriate mode for the device.
#### Multi-Keybox Configuration
You can specify different keybox files for different groups of applications. This is done by adding a line with the filename in square brackets (e.g., [demo_keybox.xml]).
All applications listed after this line will use the specified keybox file, until a new keybox is declared. Applications listed before any custom keybox declaration will use the default `keybox.xml`.
For example:
```
# These two apps will use the default /data/adb/tricky_store/keybox.xml
com.google.android.gms!
io.github.vvb2060.keyattestation?
# Switch to a different keybox for the following apps.
# The file must be located at /data/adb/tricky_store/aosp_keybox.xml
[aosp_keybox.xml]
com.google.android.gsf
# Switch again to another keybox.
# The file must be located at /data/adb/tricky_store/demo_keybox.xml
[demo_keybox.xml]
org.matrix.demo
```
### security_patch.txt
### Security Patch Level (`security_patch.txt`)
Override patch levels reported in attestation certificates. Global defaults at top, per-package overrides with `[package.name]`.
This file allows you to configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` that the simulator will report in its patched or forged attestation certificates.
| Key | Scope |
|-----|-------|
| `system` | OS patch level |
| `vendor` | Vendor patch level |
| `boot` | Boot/kernel patch level |
| `all` | Sets all three |
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change the actual system properties of your device.
Special values: `today`, `YYYY-MM-DD` templates, `no` (omit tag), `device_default`, `prop` (read from system property).
#### Global and Per-Package Configuration
You can set a global patch level that applies to all applications, and you can also override these settings for specific packages. The syntax is hierarchical:
* Settings defined at the top of the file, before any `[package.name]` line, are **global** and serve as the default for all apps.
* To create a specific configuration for an application, add its package name in square brackets (e.g., `[com.google.android.gms]`). All settings following this line will apply *only* to that package until a new package context is declared.
#### Configuration Keys and Values
You can specify the patch level for the following components using a `key=value` format:
* `system`: The main OS patch level.
* `vendor`: The vendor patch level.
* `boot`: The boot/kernel patch level.
* `all`: A convenient shorthand to set the same date for `system`, `vendor`, and `boot` simultaneously. Any individual key can still be used to override the value set by `all`.
Dates should be provided in `YYYY-MM-DD` format (e.g., `2025-11-05`).
#### Special Keywords
In addition to static dates, several special keywords provide advanced, dynamic control:
* **`today`**: Dynamically uses the current date every time an attestation is generated. This ensures the device always appears up-to-date without needing manual edits.
* **Date Templates**: You can create semi-dynamic dates using `YYYY`, `MM`, and `DD` as placeholders for the current year, month, and day. For example, `YYYY-MM-05` will always resolve to the 5th of the current month and year.
* **`no`**: This keyword instructs the simulator to **completely omit** the corresponding patch level tag from the generated attestation.
* **`device_default`**: This keyword forces the simulator to fall back and use the device's **real hardware value** for that specific patch level. This is essential for creating exceptions to a global override or an `all` rule.
#### Example Configuration
This example demonstrates how to combine global settings, per-package overrides, and special keywords for fine-grained control.
```
# --- Global Configuration ---
# This is the default for all apps unless specified otherwise.
# - Forge a recent system patch level, the 5th of the current month (a common patch date).
# - Use the device's real vendor patch level.
# - Do not report a boot patch level at all.
system=YYYY-MM-05
vendor=device_default
boot=no
# --- Per-Package Override for Google Play Services ---
# This app will report an older, specific date for its system patch.
# It will inherit the global settings for vendor (device_default) and boot (no).
[com.google.android.gms]
system=2025-10-01
system=2024-10-01
# --- Per-Package Override for a Demo App ---
# This app gets a completely custom configuration.
[org.matrix.demo]
# Set a base date for all patch levels...
all=2025-09-15
# ...but make an exception: use the real boot patch level instead of the one from 'all'.
boot=device_default
```
## Building from Source
Prerequisites: JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
```bash
git clone --recursive https://github.com/Enginex0/TEESimulator-RS.git
cd TEESimulator-RS
./gradlew zipRelease zipDebug
```
Output ZIPs in `out/`. Gradle invokes `cargo ndk` automatically to cross-compile `libcertgen.so`.
Push to `main` or use **Actions > Build > Run workflow** to trigger CI.
## Compatibility
| Root Manager | Status |
|---|---|
| KernelSU | Tested (Action button + lifecycle scripts) |
| Magisk | Supported |
| APatch | Supported |
## Community
<p align="center">
<a href="https://t.me/superpowers9">
<img src="https://img.shields.io/badge/SuperPowers_Telegram-Join-blue?style=for-the-badge&logo=telegram" alt="Telegram">
</a>
</p>
## Credits
- [JingMatrix](https://github.com/JingMatrix/TEESimulator) — original TEESimulator and interception architecture
- [5ec1cff](https://github.com/5ec1cff/TrickyStore) — TrickyStore, the project that pioneered keystore interception
- [LSPlt](https://github.com/LSPosed/LSPlt) — PLT hook library
- [ring](https://github.com/briansmith/ring) — Rust cryptography library
- [MhmRdd](https://github.com/MhmRdd) — AOSP compliance work via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157)
- [fatalcoder524](https://github.com/fatalcoder524) — contributor and collaborator
- [huguangares](https://github.com/huguangares) — collaborator and tester
## License
[GNU General Public License v3.0](LICENSE)
+17 -47
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 = "v6.0.0"
val verName = "v3.2"
android {
namespace = "org.matrix.TEESimulator"
@@ -71,35 +71,6 @@ dependencies {
implementation(libs.bcpkix)
}
// --- Rust native cert gen build task ---
val buildRustCertgen by tasks.registering(Exec::class) {
group = "TEESimulator-RS Native Build"
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
workingDir = rootProject.projectDir.resolve("native-certgen")
commandLine(
"cargo", "ndk",
"-t", "arm64-v8a",
"-o", rootProject.projectDir.resolve("app/src/main/jniLibs").absolutePath,
"build", "--release"
)
inputs.dir(rootProject.projectDir.resolve("native-certgen/src"))
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.toml"))
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.lock"))
outputs.dir(rootProject.projectDir.resolve("app/src/main/jniLibs"))
environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath)
}
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
tasks.configureEach {
if (name.endsWith("JniLibFolders") && name.startsWith("merge")) {
dependsOn(buildRustCertgen)
}
}
androidComponents {
onVariants(selector().all()) { variant ->
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
@@ -108,22 +79,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-RS-$verName-$gitCommitCount-$capitalized.zip"
val zipFileName = "TEESimulator-$verName-$gitCommitCount-$gitCommitHash-$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-RS Module Packaging"
group = "TEESimulator 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(buildRustCertgen)
dependsOn("strip${capitalized}DebugSymbols")
if (isDebug) {
from(variant.artifacts.get(SingleArtifact.APK)) {
@@ -140,14 +110,13 @@ androidComponents {
}
}
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")
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.
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so")
}
// Now, copy and process the files from 'module' directory.
@@ -162,7 +131,8 @@ androidComponents {
// Use expand() for simple key-value replacement.
expand(
"REPLACEMEVERCODE" to gitCommitCount.toString(),
"REPLACEMEVER" to "$verName-$gitCommitCount",
"REPLACEMEVER" to
"$verName ($gitCommitCount-$gitCommitHash-${variant.name})",
)
}
@@ -173,7 +143,7 @@ androidComponents {
// Task 2: Zip the prepared files from the temporary directory.
val zipTask =
tasks.register<Zip>("zip${capitalized}") {
group = "TEESimulator-RS Module Packaging"
group = "TEESimulator Module Packaging"
description = "Creates the flashable zip for the ${variant.name} module."
dependsOn(prepareModuleFilesTask)
@@ -186,7 +156,7 @@ androidComponents {
fun createInstallTasks(rootProvider: String, installCli: String) {
val pushTask =
tasks.register<Exec>("push${rootProvider}Module${capitalized}") {
group = "TEESimulator-RS Module Installation"
group = "TEESimulator Module Installation"
description =
"Pushes the ${variant.name} module to the device for $rootProvider."
dependsOn(zipTask)
@@ -200,7 +170,7 @@ androidComponents {
val installTask =
tasks.register<Exec>("install${rootProvider}${capitalized}") {
group = "TEESimulator-RS Module Installation"
group = "TEESimulator Module Installation"
description = "Installs the ${variant.name} module via $rootProvider."
dependsOn(pushTask)
commandLine(
@@ -213,7 +183,7 @@ androidComponents {
}
tasks.register<Exec>("install${rootProvider}AndReboot${capitalized}") {
group = "TEESimulator-RS Module Installation"
group = "TEESimulator Module Installation"
description = "Installs the ${variant.name} module via $rootProvider and reboots."
dependsOn(installTask)
commandLine("adb", "reboot")
-6
View File
@@ -7,9 +7,3 @@
-keepclasseswithmembers class org.matrix.TEESimulator.App {
public static void main(java.lang.String[]);
}
-keepclasseswithmembers class org.matrix.TEESimulator.pki.NativeCertGen {
native <methods>;
*;
}
-keep class org.matrix.TEESimulator.pki.CertGenConfig { *; }
-1
View File
@@ -5,7 +5,6 @@ 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)
+36 -38
View File
@@ -235,21 +235,19 @@ class BinderInterceptor : public BBinder {
struct RegistrationEntry {
wp<IBinder> target;
sp<IBinder> callback_interface;
std::vector<uint32_t> filtered_codes;
};
// Reader-Writer lock for the registry to allow concurrent reads (lookups)
mutable std::shared_mutex registry_mutex_;
std::map<wp<IBinder>, RegistrationEntry> registry_;
public:
BinderInterceptor() = default;
bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
// Checks if a specific Binder instance is currently registered for interception
bool isBinderIntercepted(const wp<BBinder> &target) const {
std::shared_lock lock(registry_mutex_);
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();
return registry_.find(target) != registry_.end();
}
// Main entry point for processing the "Man-in-the-Middle" logic
@@ -350,14 +348,14 @@ 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;
// 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)
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks
if (txn_data->data_size > kMaxInterceptableDataSize)
return;
bool hijack = false;
@@ -389,7 +387,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->shouldIntercept(wp_target, txn_data->code)) {
if (g_interceptor_instance->isBinderIntercepted(wp_target)) {
info.transaction_code = txn_data->code;
info.target_binder = wp_target; // Assign the valid weak pointer
hijack = true;
@@ -428,29 +426,44 @@ void processBinderReadBuffer(const binder_write_read &bwr) {
uintptr_t ptr = bwr.read_buffer;
uintptr_t end = ptr + bwr.read_consumed;
LOGV("[Hook] Processing Read Buffer: Size=%llu, Consumed=%llu", bwr.read_size, bwr.read_consumed);
while (ptr < end) {
// Ensure we can read at least the command header
if (end - ptr < sizeof(uint32_t))
break;
uint32_t cmd = *reinterpret_cast<const uint32_t *>(ptr);
ptr += sizeof(uint32_t);
// Calculate payload size from the ioctl command code
size_t cmd_size = _IOC_SIZE(cmd);
// Log the command using our generated to-string function
LOGV("[Driver -> User] Command: %s (0x%x), DataSize: %zu", getBinderReturnCommandName(cmd), cmd, cmd_size);
// Safety check: ensure the command's data does not exceed the buffer
if (ptr + cmd_size > end) {
LOGE("[Hook] Buffer overrun parsing command 0x%x", cmd);
LOGE("[Hook] Buffer overflow detected while parsing command %s", getBinderReturnCommandName(cmd));
break;
}
if (__builtin_expect(cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX, 0)) {
binder_transaction_data *txn;
// We are primarily interested in BR_TRANSACTION commands to intercept
if (cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX) {
binder_transaction_data *txn = nullptr;
if (cmd == BR_TRANSACTION_SEC_CTX) {
txn = &reinterpret_cast<binder_transaction_data_secctx *>(ptr)->transaction_data;
// The data is wrapped in a secctx struct
auto *wrapper = reinterpret_cast<binder_transaction_data_secctx *>(ptr);
txn = &wrapper->transaction_data;
} else {
txn = reinterpret_cast<binder_transaction_data *>(ptr);
}
inspectAndRewriteTransaction(txn);
}
// Advance pointer to the next command
ptr += cmd_size;
}
}
@@ -470,17 +483,13 @@ int intercepted_ioctl(int fd, int request, ...) {
// 1. Call original kernel ioctl to let the driver do its work
int result = g_original_ioctl(fd, request, arg);
// 2. After the call returns, check if it was a BINDER_WRITE_READ and if it succeeded
if (result >= 0 && request == BINDER_WRITE_READ && arg != nullptr) {
const auto *bwr = static_cast<const binder_write_read *>(arg);
// Fast reject: only enter the parser if the buffer could contain a BR_TRANSACTION.
// Pings, ref ops, and looper management never produce BR_TRANSACTION, so scanning
// their buffers is pure overhead (~2-5us per ioctl in debug builds).
if (bwr->read_consumed >= sizeof(uint32_t)) {
uint32_t first_cmd = *reinterpret_cast<const uint32_t *>(bwr->read_buffer);
if (first_cmd == BR_TRANSACTION || first_cmd == BR_TRANSACTION_SEC_CTX
|| bwr->read_consumed > sizeof(uint32_t) + _IOC_SIZE(first_cmd)) {
processBinderReadBuffer(*bwr);
}
// We only care about data read FROM the driver (i.e., incoming commands)
if (bwr->read_consumed > 0) {
processBinderReadBuffer(*bwr);
}
}
@@ -523,29 +532,18 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) {
if (data.readStrongBinder(&callback) != OK || !callback)
return BAD_VALUE;
// We can only intercept local Binders (BBinder), not remote proxies (BpBinder)
if (target->localBinder() == nullptr) {
LOGE("Cannot intercept remote binder proxies.");
return BAD_TYPE;
}
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, std::move(codes)};
registry_[weak_target] = {weak_target, callback};
LOGI("Interceptor registered for binder %p", target.get());
return OK;
}
+1 -20
View File
@@ -2,13 +2,11 @@
#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;
@@ -29,12 +27,7 @@ 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) {
@@ -46,7 +39,6 @@ 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);
@@ -58,18 +50,7 @@ int main(int argc, char *argv[]) {
if (should_exit) break;
// 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;
}
// Instant restart - no delay
}
return 0;
@@ -13,8 +13,8 @@ import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.NativeCertGen
import org.matrix.TEESimulator.util.AndroidDeviceUtils
import kotlin.system.exitProcess
/**
* Main application object for TEESimulator. This object manages the application's lifecycle,
@@ -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.
@@ -31,13 +33,15 @@ object App {
*/
@JvmStatic
fun main(args: Array<String>) {
SystemLogger.info("Welcome to TEESimulator!")
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
SystemLogger.error("Uncaught exception on ${thread.name}", throwable)
SystemLogger.error("Uncaught exception on thread '${thread.name}'. Exiting for restart.", throwable)
exitProcess(0)
}
SystemLogger.info("Welcome to TEESimulator!")
try {
// Initialize the Android framework environment
prepareEnvironment()
// Initialize and start the appropriate keystore interceptors.
initializeInterceptors()
@@ -53,8 +57,6 @@ object App {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
// This starts the message queue processing. It blocks here indefinitely
// processing messages until Looper.myLooper().quit() is called.
Looper.loop()
@@ -2,11 +2,8 @@ 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
@@ -43,12 +40,11 @@ object AttestationBuilder {
securityLevel: Int,
): Extension {
val keyDescription = buildKeyDescription(params, uid, securityLevel)
SystemLogger.verbose {
val formattedString = keyDescription.joinToString(separator = ", ") {
var formattedString =
keyDescription.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it)
}
"Forged attestation data: $formattedString"
}
SystemLogger.verbose("Forged attestation data: ${formattedString}")
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
}
@@ -131,59 +127,33 @@ object AttestationBuilder {
return properties
}
/** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */
private fun buildKeyDescription(
params: KeyMintAttestation,
uid: Int,
securityLevel: Int,
): ASN1Sequence {
val creationTime = System.currentTimeMillis()
val teeEnforced = buildTeeEnforcedList(params, 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 softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
val fields =
arrayOf(
ASN1Integer(AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()),
ASN1Enumerated(securityLevel),
ASN1Integer(AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()),
ASN1Enumerated(securityLevel),
DEROctetString(params.attestationChallenge ?: ByteArray(0)),
DEROctetString(uniqueId),
ASN1Integer(
AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()
), // attestationVersion
ASN1Enumerated(securityLevel), // attestationSecurityLevel
ASN1Integer(
AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()
), // keymasterVersion
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
DEROctetString(ByteArray(0)), // uniqueId
softwareEnforced,
teeEnforced,
)
return DERSequence(fields)
}
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)
.array()
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
return mac.doFinal(message).copyOf(16)
}
private val hbk: ByteArray by lazy {
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
if (file.exists() && file.length() == 32L) {
file.readBytes()
} else {
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,
@@ -212,110 +182,23 @@ 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.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(
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()),
)
)
}
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((params.origin ?: 0).toLong()),
),
ASN1Integer(0L),
), // KeyOrigin.GENERATED
DERTaggedObject(
true,
AttestationConstants.TAG_ROOT_OF_TRUST,
buildRootOfTrust(null),
),
)
)
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
val simulatedProperties = getSimulatedHardwareProperties(uid)
@@ -412,32 +295,20 @@ object AttestationBuilder {
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
* Keystore.
*/
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(creationTimeMs),
)
)
if (params.attestationChallenge != null) {
list.add(
private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence {
val list =
mutableListOf<ASN1Encodable>(
DERTaggedObject(
true,
AttestationConstants.TAG_CREATION_DATETIME,
ASN1Integer(System.currentTimeMillis()),
),
DERTaggedObject(
true,
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
createApplicationId(uid),
)
),
)
}
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
list.add(
DERTaggedObject(
@@ -447,39 +318,7 @@ object AttestationBuilder {
)
)
}
if (params.callerNonce == true) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)
)
}
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())
return DERSequence(list.toTypedArray())
}
/**
@@ -506,12 +345,7 @@ object AttestationBuilder {
* retrieved.
*/
@Throws(Throwable::class)
internal fun createApplicationId(uid: Int): DEROctetString {
val appUid = uid % 100000
if (appUid == 0 || appUid == 1000) {
return buildApplicationIdDer(listOf("AndroidSystem" to 1L), emptySet())
}
private fun createApplicationId(uid: Int): DEROctetString {
val pm =
ConfigurationManager.getPackageManager()
?: throw IllegalStateException("PackageManager not found!")
@@ -519,11 +353,12 @@ object AttestationBuilder {
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
val sha256 = MessageDigest.getInstance("SHA-256")
val packageInfoList = mutableListOf<Pair<String, Long>>()
val packageInfoList = mutableListOf<DERSequence>()
val signatureDigests = mutableSetOf<Digest>()
val userId = uid / 100000
// Process all packages associated with the UID in a single loop.
packages.forEach { packageName ->
val userId = uid / 100000
val packageInfo =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.getPackageInfo(
@@ -536,36 +371,34 @@ object AttestationBuilder {
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
}
packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode)
// Add package information (name and version code) to our list.
packageInfoList.add(
DERSequence(
arrayOf(
DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)),
ASN1Integer(packageInfo.longVersionCode),
)
)
)
// Collect unique signature digests from the signing history.
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
signatureDigests.add(Digest(sha256.digest(signature.toByteArray())))
val digest = sha256.digest(signature.toByteArray())
signatureDigests.add(Digest(digest))
}
}
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(name.toByteArray(StandardCharsets.UTF_8)),
ASN1Integer(version),
)
)
}
// 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(digests.map { DEROctetString(it.digest) }.toTypedArray()),
DERSet(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()),
)
)
return DEROctetString(applicationIdSequence.encoded)
}
}
@@ -44,11 +44,9 @@ 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
@@ -58,10 +56,6 @@ 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
@@ -95,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
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
}
@@ -16,7 +16,6 @@ import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.KeyBox
import org.matrix.TEESimulator.pki.KeyBoxManager
import org.matrix.TEESimulator.util.toHex
import java.util.Date
/**
* Handles the modification (patching) of Android Key Attestation extensions within certificates.
@@ -37,12 +36,7 @@ object AttestationPatcher {
* @return A new, cryptographically valid, patched certificate chain. Returns the original chain
* on any failure.
*/
fun patchCertificateChain(
originalChain: Array<Certificate>?,
uid: Int,
notBefore: Date? = null,
notAfter: Date? = null,
): Array<Certificate> {
fun patchCertificateChain(originalChain: Array<Certificate>?, uid: Int): Array<Certificate> {
if (originalChain.isNullOrEmpty()) {
SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.")
return originalChain ?: emptyArray()
@@ -69,8 +63,6 @@ object AttestationPatcher {
keybox,
originalLeaf.sigAlgName,
uid,
notBefore,
notAfter,
)
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
@@ -119,27 +111,17 @@ object AttestationPatcher {
keybox: KeyBox,
sigAlgName: String,
uid: Int,
notBefore: Date? = null,
notAfter: Date? = null,
): Certificate {
// The issuer of our new leaf is the subject of the first certificate in our custom keybox
// chain.
val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject
val effectiveNotBefore = notBefore ?: originalLeafHolder.notBefore
val effectiveNotAfter = notAfter ?: originalLeafHolder.notAfter
if (notBefore != null || notAfter != null) {
SystemLogger.debug(
"Overriding cert dates: notBefore=${effectiveNotBefore} (was ${originalLeafHolder.notBefore}), notAfter=${effectiveNotAfter} (was ${originalLeafHolder.notAfter})"
)
}
val builder =
X509v3CertificateBuilder(
newIssuer,
originalLeafHolder.serialNumber,
effectiveNotBefore,
effectiveNotAfter,
originalLeafHolder.notBefore,
originalLeafHolder.notAfter,
originalLeafHolder.subject,
originalLeafHolder.subjectPublicKeyInfo,
)
@@ -164,7 +146,7 @@ object AttestationPatcher {
// Log the signature of the newly created certificate to observe its non-deterministic
// nature.
val signatureBytes = (newCertificate as X509Certificate).signature
SystemLogger.verbose { "Signature of patched leaf cert: ${signatureBytes.toHex()}" }
SystemLogger.verbose("Signature of patched leaf cert: ${signatureBytes.toHex()}")
return newCertificate
}
@@ -286,10 +268,8 @@ object AttestationPatcher {
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
SystemLogger.verbose {
val formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
"Original attestation data: $formattedString"
}
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
SystemLogger.verbose("Original attestation data: ${formattedString}")
// Build the new Root of Trust and add/replace it in the map.
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
@@ -316,10 +296,8 @@ object AttestationPatcher {
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
val patchedSequence = DERSequence(allFields)
SystemLogger.verbose {
val formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
"Patched attestation data: $formattedString"
}
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
SystemLogger.verbose("Patched attestation data: ${formattedString}")
val patchedOctets = DEROctetString(patchedSequence)
return Extension(ATTESTATION_OID, false, patchedOctets)
@@ -148,12 +148,11 @@ object DeviceAttestationService {
// The extension's value is an ASN.1 sequence.
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
SystemLogger.verbose {
val formattedString = keyDescriptionSeq.joinToString(separator = ", ") {
var formattedString =
keyDescriptionSeq.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it)
}
"Cached attestation data: $formattedString"
}
SystemLogger.verbose("Cached attestation data: ${formattedString}")
val fields = keyDescriptionSeq.toArray()
val attestVersion =
@@ -250,10 +249,6 @@ object DeviceAttestationService {
verifiedBootKey = null
}
if (verifiedBootHash?.all { it == 0.toByte() } == true) {
verifiedBootHash = null
}
SystemLogger.info(
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
)
@@ -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
@@ -19,9 +18,8 @@ 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>,
val padding: List<Int>,
val purpose: List<Int>,
@@ -41,41 +39,21 @@ data class KeyMintAttestation(
val manufacturer: ByteArray?,
val model: ByteArray?,
val secondImei: ByteArray?,
val activeDateTime: Date?,
val originationExpireDateTime: Date?,
val usageExpireDateTime: Date?,
val usageCountLimit: Int?,
val callerNonce: Boolean?,
val nonce: ByteArray?,
val unlockedDeviceRequired: Boolean?,
val includeUniqueId: Boolean?,
val rollbackResistance: Boolean?,
val earlyBootOnly: Boolean?,
val allowWhileOnBody: Boolean?,
val trustedUserPresenceRequired: Boolean?,
val trustedConfirmationRequired: Boolean?,
val noAuthRequired: 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(
keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(),
// 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 = EC_CURVE, field = EcCurve)]
ecCurve = params.findEcCurve(Tag.EC_CURVE),
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
ecCurveName = params.deriveEcCurveName(),
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
origin = params.findOrigin(Tag.ORIGIN),
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
@@ -117,32 +95,10 @@ 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),
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),
nonce = params.findBlob(Tag.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),
noAuthRequired = params.findBoolean(Tag.NO_AUTH_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 isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
}
// --- Private helper extension functions for parsing KeyParameter arrays ---
@@ -159,10 +115,6 @@ private fun Array<KeyParameter>.findAlgorithm(tag: Int): Int? =
private fun Array<KeyParameter>.findEcCurve(tag: Int): Int? =
this.find { it.tag == tag }?.value?.ecCurve
/** Maps to AOSP field = Origin */
private fun Array<KeyParameter>.findOrigin(tag: Int): Int? =
this.find { it.tag == tag }?.value?.origin
/** Maps to AOSP field = LongInteger */
private fun Array<KeyParameter>.findLongInteger(tag: Int): BigInteger? =
this.find { it.tag == tag }?.value?.longInteger?.toBigInteger()
@@ -191,21 +143,6 @@ 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 }
private fun Array<KeyParameter>.findBoolean(tag: Int): Boolean? =
if (this.any { it.tag == tag }) true else null
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).
@@ -31,6 +31,7 @@ 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)
@@ -38,6 +39,7 @@ 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>()
@@ -66,6 +68,8 @@ 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()
SystemLogger.info("Configuration initialized and file observer started.")
@@ -83,31 +87,33 @@ object ConfigurationManager {
return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE
}
fun shouldPatch(uid: Int): Boolean {
val mode = getPackageModeForUid(uid)
return mode == Mode.PATCH || mode == Mode.AUTO
}
/** Determines if the certificate for a given UID needs to be patched. */
fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH
/** Determines if a new certificate needs to be generated for a given UID. */
fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE
/** Determines if no operation is needed for a given UID. */
fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null
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 (DeviceAttestationService.isTeeFunctional) Mode.PATCH else Mode.GENERATE
null -> continue
Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH
null -> continue // No config for this package, check the next one.
}
}
return null
return null // No configuration found for this UID.
}
/**
@@ -165,6 +171,7 @@ object ConfigurationManager {
newModes[pkg] = Mode.PATCH
newKeyboxes[pkg] = currentKeybox
}
// No suffix means AUTO mode.
else -> {
newModes[trimmedLine] = Mode.AUTO
newKeyboxes[trimmedLine] = currentKeybox
@@ -273,6 +280,29 @@ 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.
@@ -297,10 +327,10 @@ object ConfigurationManager {
)
KeyBoxManager.invalidateCache(path)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
// Clear cached keys possibly containing old certificates
// Patched chains are stale; generated keys survive rotation
org.matrix.TEESimulator.interception.keystore.shim
.KeyMintSecurityLevelInterceptor
.clearAllGeneratedKeys("updating $file")
.invalidatePatchedChains("keybox change: $path")
}
}
}
@@ -330,29 +360,7 @@ object ConfigurationManager {
return iPackageManager
}
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
}
}
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) {
try {
@@ -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 = try {
val result =
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)
}
} catch (e: Throwable) {
SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e)
TransactionResult.ContinueAndSkipPost
}
// The reply parcel is guaranteed to be non-null for our custom transactions.
writeResultToReply(result, reply!!)
return true
}
@@ -293,21 +293,15 @@ abstract class BinderInterceptor : Binder() {
}
}
fun register(
backdoor: IBinder,
target: IBinder,
interceptor: BinderInterceptor,
filteredCodes: IntArray = intArrayOf(),
) {
/** Uses the backdoor binder to register an interceptor for a specific target service. */
fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) {
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 (${filteredCodes.size} filtered codes)")
SystemLogger.info("Registered interceptor for target: $target")
} catch (e: Exception) {
SystemLogger.error("Failed to register binder interceptor.", e)
} finally {
@@ -68,12 +68,11 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
}
}
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, interceptedCodes)
register(backdoor, service, this)
service.linkToDeath(createDeathRecipient(), 0)
onInterceptorReady(service, backdoor)
}
@@ -1,34 +1,17 @@
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)
/** A collection of utility functions to support binder interception. */
object InterceptorUtils {
private const val EX_SERVICE_SPECIFIC = -8
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
val parcel = Parcel.obtain().apply {
writeInt(EX_SERVICE_SPECIFIC)
writeString(null)
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
writeInt(errorCode)
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/**
* Uses reflection to get the integer transaction code for a given method name from a Stub
* class. This is necessary for older Android versions where codes are not public constants.
@@ -125,57 +108,6 @@ object InterceptorUtils {
/** Checks if a reply parcel contains an exception without consuming it. */
fun hasException(reply: Parcel): Boolean {
val exception = runCatching { reply.readException() }.exceptionOrNull()
if (exception != null) reply.setDataPosition(0)
return exception != null
}
fun createServiceSpecificErrorReply(
errorCode: Int
): BinderInterceptor.TransactionResult.OverrideReply {
val parcel =
Parcel.obtain().apply {
writeException(android.os.ServiceSpecificException(errorCode))
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
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()
return runCatching { reply.readException() }.exceptionOrNull() != null
}
}
@@ -1,39 +1,28 @@
package org.matrix.TEESimulator.interception.keystore
import android.annotation.SuppressLint
import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.SecurityLevel
import android.hardware.security.keymint.Tag
import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.system.keystore2.Domain
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.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
import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper
/**
* Interceptor for the `IKeystoreService` on Android S (API 31) and newer.
*
* This version of Keystore delegates most cryptographic operations to `IKeystoreSecurityLevel`
* sub-services (for TEE, StrongBox, etc.). This interceptor's main role is to set up interceptors
* for those sub-services and to patch certificate chains on their way out.
*/
@SuppressLint("BlockedPrivateApi")
object Keystore2Interceptor : AbstractKeystoreInterceptor() {
private val stubBinderClass = IKeystoreService.Stub::class.java
// Transaction codes for the IKeystoreService interface methods we are interested in.
private val GET_KEY_ENTRY_TRANSACTION =
InterceptorUtils.getTransactCode(stubBinderClass, "getKeyEntry")
private val DELETE_KEY_TRANSACTION =
@@ -46,8 +35,6 @@ 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 transactionNames: Map<Int, String> by lazy {
stubBinderClass.declaredFields
@@ -58,65 +45,33 @@ 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()
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
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,
)
.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) {
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
}
private fun setupSecurityLevelInterceptors(service: IKeystoreService, backdoor: IBinder) {
// Attempt to get and intercept the TEE security level service.
runCatching {
service.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT)?.let { tee ->
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
val interceptor =
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
register(
backdoor,
tee.asBinder(),
interceptor,
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
)
register(backdoor, tee.asBinder(), interceptor)
interceptor.loadPersistedKeys()
}
}
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
// Attempt to get and intercept the StrongBox security level service.
runCatching {
service.getSecurityLevel(SecurityLevel.STRONGBOX)?.let { strongbox ->
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
val interceptor =
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
register(
backdoor,
strongbox.asBinder(),
interceptor,
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
)
register(backdoor, strongbox.asBinder(), interceptor)
interceptor.loadPersistedKeys()
}
}
@@ -132,20 +87,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
callingPid: Int,
data: Parcel,
): TransactionResult {
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)
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
val isGMS = packages.contains("com.google.android.gms")
if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) {
if (ConfigurationManager.shouldSkipUid(callingUid))
return TransactionResult.ContinueAndSkipPost
}
return runCatching {
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
@@ -180,49 +126,24 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
if (code == DELETE_KEY_TRANSACTION) {
val keyId =
if (descriptor.alias != null) {
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) {
deletedSoftwareKeys.add(keyId)
SystemLogger.info(
"[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
}
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
val keyId = KeyIdentifier(callingUid, descriptor.alias)
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)
if (code == DELETE_KEY_TRANSACTION) {
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
SystemLogger.info(
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
)
return InterceptorUtils.createSuccessReply(writeResultCode = false)
}
return TransactionResult.Continue
return TransactionResult.ContinueAndSkipPost
}
val response =
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
?: return TransactionResult.Continue
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
SystemLogger.info("${descriptor.alias} was an attestation key")
@@ -241,7 +162,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
)
}
// Let most calls go through to the real service.
return TransactionResult.ContinueAndSkipPost
}
@@ -259,26 +179,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction
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) {
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
return runCatching {
@@ -294,159 +195,73 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
TransactionResult.SkipTransaction
}
} else if (code == GET_KEY_ENTRY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val keyDescriptor =
data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
logTransaction(
txId,
"post-${transactionNames[code]!!} ${keyDescriptor.alias}",
callingUid,
callingPid,
)
if (!ConfigurationManager.shouldPatch(callingUid))
return TransactionResult.SkipTransaction
runCatching {
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
SystemLogger.info("Handling post-${transactionNames[code]!!} ${keyDescriptor.alias}")
return try {
val response =
reply.readTypedObject(KeyEntryResponse.CREATOR)
?: return TransactionResult.SkipTransaction
reply.setDataPosition(0) // Reset for potential reuse.
val originalChain = CertificateHelper.getCertificateChain(response)
val authorizations = response.metadata?.authorizations
val origin =
authorizations
?.find { it.keyParameter.tag == Tag.ORIGIN }
?.let { it.keyParameter.value.origin }
if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) {
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
if (userUpdatedKeys.remove(keyId)) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch" }
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
if (retainedChain == null) {
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
return TransactionResult.SkipTransaction
}
val authorizations = response.metadata.authorizations
val parsedParameters =
KeyMintAttestation(
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
)
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: isImport=${parsedParameters.isImportKey()} origin=${parsedParameters.origin} inImportedKeys=${KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)} hasPatchedChain=${KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) != null} isAttestKey=${parsedParameters.isAttestKey()}" }
if (parsedParameters.isImportKey()) {
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
if (retainedChain == null) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip" }
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
return TransactionResult.SkipTransaction
}
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)" }
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
response.metadata.authorizations =
InterceptorUtils.patchAuthorizations(
response.metadata.authorizations,
callingUid,
)
return InterceptorUtils.createTypedObjectReply(response)
}
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" }
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
return TransactionResult.SkipTransaction
}
if (parsedParameters.isAttestKey()) {
SystemLogger.warning(
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
)
val keyData =
CertificateGenerator.generateAttestedKeyPair(
callingUid,
keyId.alias,
null,
parsedParameters,
response.metadata.keySecurityLevel,
) ?: throw Exception("Failed to create overriding attest key pair.")
CertificateHelper.updateCertificateChain(
response.metadata,
keyData.second.toTypedArray(),
)
.getOrThrow()
response.metadata.authorizations =
InterceptorUtils.patchAuthorizations(
response.metadata.authorizations,
callingUid,
)
val newNspace = SecureRandom().nextLong()
response.metadata.key?.let { it.nspace = newNspace }
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
keyData.first,
null,
newNspace,
response,
parsedParameters,
)
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
GeneratedKeyPersistence.save(
keyId = keyId,
keyPair = keyData.first,
nspace = newNspace,
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)
if (originalChain == null || originalChain.size < 2) {
SystemLogger.info(
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
)
return TransactionResult.SkipTransaction
}
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
val finalChain: Array<Certificate>
if (cachedChain != null) {
SystemLogger.debug(
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
)
finalChain = cachedChain
} else {
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
}
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
.getOrThrow()
response.metadata.authorizations =
InterceptorUtils.patchAuthorizations(
response.metadata.authorizations,
callingUid,
)
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)
}
.onFailure {
SystemLogger.error(
"[TX_ID: $txId] Failed to modify hardware KeyEntryResponse.",
it,
if (originalChain == null || originalChain.size < 2) {
SystemLogger.info(
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
)
return TransactionResult.SkipTransaction
}
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
val finalChain: Array<Certificate>
if (cachedChain != null) {
SystemLogger.debug(
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
)
finalChain = cachedChain
} else {
// Live patch fallback for keys created before simulator started
SystemLogger.info(
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
)
finalChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
}
CertificateHelper.updateCertificateChain(response.metadata, finalChain).getOrThrow()
InterceptorUtils.createTypedObjectReply(response)
} catch (e: Exception) {
SystemLogger.error("[TX_ID: $txId] Failed to patch certificate chain.", e)
TransactionResult.SkipTransaction
}
}
return TransactionResult.SkipTransaction
}
@@ -454,29 +269,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
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) {
descriptor.alias?.let {
val kid = KeyIdentifier(callingUid, it)
userUpdatedKeys.add(kid)
SystemLogger.trace { "[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys" }
}
return TransactionResult.ContinueAndSkipPost
}
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor?.nspace)
?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
val metadata = generatedKeyInfo.response.metadata
@@ -407,9 +407,8 @@ private data class LegacyKeygenParameters(
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,
blockMode = listOf<Int>(),
padding = listOf<Int>(),
purpose = this.purpose,
@@ -431,24 +430,6 @@ private data class LegacyKeygenParameters(
manufacturer = null,
model = null,
secondImei = null,
activeDateTime = null,
originationExpireDateTime = null,
usageExpireDateTime = null,
usageCountLimit = null,
callerNonce = null,
nonce = null,
unlockedDeviceRequired = null,
includeUniqueId = null,
rollbackResistance = null,
earlyBootOnly = null,
allowWhileOnBody = null,
trustedUserPresenceRequired = null,
trustedConfirmationRequired = null,
noAuthRequired = null,
maxUsesPerBoot = null,
maxBootLevel = null,
minMacLength = null,
rsaOaepMgfDigest = emptyList(),
)
}
@@ -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
@@ -1,74 +0,0 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.Tag
import org.matrix.TEESimulator.attestation.KeyMintAttestation
object AuthorizeCreate {
fun check(
keyParams: KeyMintAttestation?,
opParams: KeyMintAttestation,
rawOpParams: Array<KeyParameter>? = null,
): Int? {
if (keyParams == null) return null
val purpose = opParams.purpose.firstOrNull() ?: return null
// Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior)
return checkAlgorithmPurpose(keyParams, purpose)
?: checkPurpose(keyParams, purpose)
?: checkTemporalValidity(keyParams, purpose)
?: checkCallerNonce(keyParams, purpose, rawOpParams)
}
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
val algo = keyParams.algorithm
if ((algo == Algorithm.EC || algo == Algorithm.RSA) &&
(purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT)
) {
return KeystoreErrorCodes.unsupportedPurpose
}
if (algo == Algorithm.RSA && purpose == KeyPurpose.AGREE_KEY)
return KeystoreErrorCodes.unsupportedPurpose
return null
}
private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
if (purpose == KeyPurpose.WRAP_KEY)
return KeystoreErrorCodes.incompatiblePurpose
if (purpose !in keyParams.purpose)
return KeystoreErrorCodes.incompatiblePurpose
return null
}
private fun checkTemporalValidity(keyParams: KeyMintAttestation, purpose: Int): Int? {
val now = System.currentTimeMillis()
keyParams.activeDateTime?.let { activeDate ->
if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid
}
keyParams.originationExpireDateTime?.let { expireDate ->
if (purpose == KeyPurpose.SIGN || purpose == KeyPurpose.ENCRYPT) {
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
}
}
keyParams.usageExpireDateTime?.let { expireDate ->
if (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.DECRYPT) {
if (now > expireDate.time) return KeystoreErrorCodes.keyExpired
}
}
return null
}
private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array<KeyParameter>?): Int? {
if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null
if (keyParams.callerNonce == true) return null
if (rawOpParams?.any { it.tag == Tag.NONCE } == true)
return KeystoreErrorCodes.callerNonceProhibited
return null
}
}
@@ -129,7 +129,6 @@ 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}")
@@ -159,7 +158,6 @@ 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)
@@ -303,10 +301,9 @@ object GeneratedKeyPersistence {
return
}
val keyPair = generatedKeyInfo.keyPair ?: return
save(
keyId = keyId,
keyPair = keyPair,
keyPair = generatedKeyInfo.keyPair,
nspace = generatedKeyInfo.nspace,
securityLevel = secLevel,
certChain = newChain.toList(),
@@ -44,8 +44,6 @@ class OperationInterceptor(
private val ABORT_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
val INTERCEPTED_CODES = intArrayOf(FINISH_TRANSACTION, ABORT_TRANSACTION)
private val transactionNames: Map<Int, String> by lazy {
IKeystoreOperation.Stub::class
.java
@@ -3,15 +3,10 @@ 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.ServiceSpecificException
import java.util.concurrent.locks.LockSupport
import android.os.RemoteException
import android.system.keystore2.IKeystoreOperation
import android.system.keystore2.KeyParameters
import java.security.KeyPair
import java.security.Signature
import java.security.SignatureException
@@ -20,16 +15,16 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger
// A sealed interface to represent the different cryptographic operations we can perform.
private sealed interface CryptoPrimitive {
fun updateAad(aadInput: ByteArray?) {
throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
}
fun update(data: ByteArray?): ByteArray?
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
fun abort()
fun getBeginParameters(): Array<KeyParameter>? = null
}
// Helper object to map KeyMint constants to JCA algorithm strings.
private object JcaAlgorithmMapper {
fun mapSignatureAlgorithm(params: KeyMintAttestation): String {
val digest =
@@ -39,18 +34,16 @@ 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}"
)
}
else ->
throw ServiceSpecificException(
KeystoreErrorCodes.incompatibleAlgorithm,
"Unsupported signature algorithm: ${params.algorithm}",
)
}
return "${digest}with${keyAlgo}"
}
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
@@ -59,32 +52,30 @@ private object JcaAlgorithmMapper {
Algorithm.RSA -> "RSA"
Algorithm.AES -> "AES"
else ->
throw ServiceSpecificException(
KeystoreErrorCodes.incompatibleAlgorithm,
"Unsupported cipher algorithm: ${params.algorithm}",
throw IllegalArgumentException(
"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"
}
}
// Concrete implementation for Signing.
private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
@@ -104,6 +95,7 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi
override fun abort() {}
}
// Concrete implementation for Verification.
private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive {
private val signature: Signature =
Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply {
@@ -117,290 +109,107 @@ 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 ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature to verify is null")
}
if (signature == null) throw SignatureException("Signature to verify is null")
if (!this.signature.verify(signature)) {
throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature verification failed")
// Throwing an exception is how Keystore signals verification failure.
throw SignatureException("Signature verification failed")
}
// A successful verification returns no data.
return null
}
override fun abort() {}
}
// Concrete implementation for Encryption/Decryption.
private class CipherPrimitive(
cryptoKey: java.security.Key,
keyPair: KeyPair,
params: KeyMintAttestation,
private val opMode: Int,
) : CryptoPrimitive {
private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM
private val cipher: Cipher =
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
val nonce = params.nonce
if (nonce != null && isAead) {
init(opMode, cryptoKey, javax.crypto.spec.GCMParameterSpec(128, nonce))
} else if (nonce != null) {
init(opMode, cryptoKey, javax.crypto.spec.IvParameterSpec(nonce))
} else {
init(opMode, cryptoKey)
}
val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private
init(opMode, key)
}
override fun updateAad(aadInput: ByteArray?) {
if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag)
if (aadInput != null) cipher.updateAAD(aadInput)
}
override fun update(data: ByteArray?): ByteArray? =
if (data != null) cipher.update(data) else null
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
if (data != null) cipher.doFinal(data) else cipher.doFinal()
override fun getBeginParameters(): Array<KeyParameter>? {
val iv = cipher.iv ?: return null
return arrayOf(
KeyParameter().apply {
tag = Tag.NONCE
value = KeyParameterValue.blob(iv)
}
)
}
override fun abort() {}
}
private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
private val agreement: javax.crypto.KeyAgreement =
javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) }
override fun update(data: ByteArray?): ByteArray? = null
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
if (data == null)
throw ServiceSpecificException(
KeystoreErrorCodes.invalidArgument,
"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() {}
}
class SoftwareOperation(
private val txId: Long,
keyPair: KeyPair?,
secretKey: javax.crypto.SecretKey?,
params: KeyMintAttestation,
private val latencyFloorMs: Long = 0L,
) {
/**
* 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 var finalized = false
private set
var onFinishCallback: (() -> Unit)? = null
val beginParameters: KeyParameters?
get() {
val params = primitive.getBeginParameters() ?: return null
if (params.isEmpty()) return null
return KeyParameters().apply { keyParameter = params }
}
init {
// The "Strategy" pattern: choose the implementation based on the purpose.
// For simplicity, we only consider the first purpose listed.
val purpose = params.purpose.firstOrNull()
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
primitive =
when (purpose) {
KeyPurpose.SIGN -> Signer(keyPair!!, params)
KeyPurpose.VERIFY -> Verifier(keyPair!!, params)
KeyPurpose.ENCRYPT -> {
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!!)
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 ServiceSpecificException(
KeystoreErrorCodes.unsupportedPurpose,
"Unsupported operation purpose: $purpose",
)
throw UnsupportedOperationException("Unsupported operation purpose: $purpose")
}
}
private fun checkActive() {
if (finalized) {
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
}
}
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}")
checkActive()
checkInputLength(aadInput)
primitive.updateAad(aadInput)
}
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) {
throw e
} catch (e: Exception) {
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
throw mapToServiceSpecificException(e)
throw e
}
}
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) LockSupport.parkNanos(delayMs * 1_000_000)
}
finalized = true
onFinishCallback?.invoke()
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)
throw mapToServiceSpecificException(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.")
}
private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) {
is SignatureException -> ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message)
is javax.crypto.BadPaddingException -> ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message)
is javax.crypto.IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message)
is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message)
else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message)
}
companion object {
private const val MAX_RECEIVE_DATA = 0x8000
}
}
internal 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)
}
val invalidTag: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_TAG", -76)
}
val verificationFailed: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "VERIFICATION_FAILED", -30)
}
val invalidArgument: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_ARGUMENT", -38)
}
val invalidInputLength: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_INPUT_LENGTH", -21)
}
val incompatibleKey: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_KEY", -31)
}
val incompatiblePurpose: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_PURPOSE", -13)
}
val unsupportedPurpose: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "UNSUPPORTED_PURPOSE", -14)
}
val incompatibleAlgorithm: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_ALGORITHM", -18)
}
val keyNotYetValid: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_NOT_YET_VALID", -39)
}
val keyExpired: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "KEY_EXPIRED", -40)
}
val callerNonceProhibited: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "CALLER_NONCE_PROHIBITED", -55)
}
val unknownError: Int by lazy {
resolveField("android.hardware.security.keymint.ErrorCode", "UNKNOWN_ERROR", -1000)
}
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() {
@Synchronized
override fun updateAad(aadInput: ByteArray?) {
operation.updateAad(aadInput)
}
@Synchronized
@Throws(RemoteException::class)
override fun update(input: ByteArray?): ByteArray? {
return operation.update(input)
}
@Synchronized
@Throws(RemoteException::class)
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
return operation.finish(input, signature)
}
@Synchronized
@Throws(RemoteException::class)
override fun abort() {
operation.abort()
}
@@ -1,86 +1,41 @@
package org.matrix.TEESimulator.logging
import android.util.Log
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import org.matrix.TEESimulator.BuildConfig
/**
* A centralized logging utility for the TEESimulator application. This object provides a consistent
* logging tag and format for all application logs, making it easier to filter and debug in Logcat.
*
* Includes a rate limiter that caps logd syscalls during binder stress to prevent thread pool
* contention. The first [RATE_LIMIT_BURST] messages per [RATE_LIMIT_WINDOW_MS] window are logged
* normally; subsequent messages are suppressed and a summary is emitted when the window resets.
*/
object SystemLogger {
@PublishedApi internal const val TAG = "TEESimulator"
// The tag used for all log messages from this application.
private const val TAG = "TEESimulator"
@PublishedApi internal val isDebugBuild = BuildConfig.DEBUG
// Rate limiter: allow BURST messages per WINDOW, then suppress until window resets.
private const val RATE_LIMIT_BURST = 15
private const val RATE_LIMIT_WINDOW_MS = 1000L
private val windowStart = AtomicLong(System.currentTimeMillis())
private val windowCount = AtomicInteger(0)
private val suppressedCount = AtomicInteger(0)
/**
* Returns true if this message should be emitted. Resets the window if expired
* and emits a suppression summary for the previous window.
*/
@PublishedApi internal fun acquireLogPermit(): Boolean {
val now = System.currentTimeMillis()
val start = windowStart.get()
if (now - start > RATE_LIMIT_WINDOW_MS) {
// Window expired: reset and emit suppression summary if needed.
if (windowStart.compareAndSet(start, now)) {
val suppressed = suppressedCount.getAndSet(0)
windowCount.set(1) // this call counts as #1 in the new window
if (suppressed > 0) {
Log.i(TAG, "[rate-limit] suppressed $suppressed log messages in previous window")
}
return true
}
}
val count = windowCount.incrementAndGet()
if (count <= RATE_LIMIT_BURST) return true
suppressedCount.incrementAndGet()
return false
}
private val isDebugBuild = BuildConfig.DEBUG
/**
* Logs a debug message. Use this for fine-grained information that is useful for debugging.
*
* @param message The message to log.
*/
fun debug(message: String) {
if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.d(TAG, message)
}
/** Lazy debug: lambda only evaluates if message will be logged. */
inline fun debug(message: () -> String) {
if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.d(TAG, message())
}
/**
* Logs an informational message. Use this to report major application lifecycle events.
*
* @param message The message to log.
*/
fun info(message: String) {
if (!acquireLogPermit()) return
Log.i(TAG, message)
}
/** Lazy info: lambda only evaluates if message will be logged. */
inline fun info(message: () -> String) {
if (!acquireLogPermit()) return
Log.i(TAG, message())
}
/**
* Logs a warning message. Warnings are never rate-limited.
* Logs a warning message. Use this to report unexpected but non-fatal issues.
*
* @param message The message to log.
* @param throwable An optional exception to log with the message.
*/
fun warning(message: String, throwable: Throwable? = null) {
if (throwable != null) {
@@ -91,7 +46,11 @@ object SystemLogger {
}
/**
* Logs an error message. Errors are never rate-limited.
* Logs an error message. Use this to report fatal errors or exceptions that disrupt
* functionality.
*
* @param message The message to log.
* @param throwable An optional exception to log with the message.
*/
fun error(message: String, throwable: Throwable? = null) {
if (throwable != null) {
@@ -104,22 +63,11 @@ object SystemLogger {
/**
* Logs a verbose message. This level is for highly detailed logs that are generally not needed
* unless tracking a very specific issue.
*
* @param message The message to log.
*/
fun verbose(message: String) {
if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.v(TAG, message)
}
/** Lazy verbose: lambda only evaluates if message will be logged. */
inline fun verbose(message: () -> String) {
if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.v(TAG, message())
}
inline fun trace(message: () -> String) {
if (!isDebugBuild) return
Log.w(TAG, message())
}
}
@@ -8,6 +8,7 @@ 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
@@ -35,8 +36,6 @@ import org.matrix.TEESimulator.logging.SystemLogger
*/
object CertificateGenerator {
private const val UNDEFINED_NOT_AFTER = 253402300799000L
/**
* Generates a software-based cryptographic key pair.
*
@@ -50,10 +49,7 @@ object CertificateGenerator {
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
Algorithm.RSA ->
"RSA" to
RSAKeyGenParameterSpec(
params.keySize,
params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4,
)
RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent)
else ->
throw IllegalArgumentException(
"Unsupported algorithm: ${params.algorithm}"
@@ -92,15 +88,11 @@ object CertificateGenerator {
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
)
return try {
// AOSP ta/src/keys.rs:451-478: no challenge + no attestKey = self-signed, depth 1
if (challenge == null && attestKeyAlias == null) {
SystemLogger.trace { "[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}" }
return listOf(buildSelfSignedCertificate(subjectKeyPair, params))
}
return runCatching {
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
// Determine the signing key and issuer. If an attestKey is provided, use it.
// Otherwise, fall back to the root key from the keybox.
val (signingKey, issuer) =
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
@@ -109,20 +101,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()
}
/**
@@ -136,7 +128,7 @@ object CertificateGenerator {
params: KeyMintAttestation,
securityLevel: Int,
): Pair<KeyPair, List<Certificate>>? {
return try {
return runCatching {
SystemLogger.info(
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
)
@@ -152,12 +144,11 @@ 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) =
@@ -172,10 +163,7 @@ object CertificateGenerator {
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
}
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
?: throw android.os.ServiceSpecificException(
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
"No attestation key for algorithm $algorithmName in $keyboxFile",
)
?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName")
}
/** Retrieves the key pair and issuer name for a given attestation key alias. */
@@ -225,16 +213,17 @@ object CertificateGenerator {
uid: Int,
securityLevel: Int,
): Certificate {
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
val notBefore = params.certificateNotBefore ?: Date(0)
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
val leafNotAfter =
(signingKeyPair.public as? X509Certificate)?.notAfter
?: Date(System.currentTimeMillis() + 31536000000L)
val builder =
JcaX509v3CertificateBuilder(
issuer,
params.certificateSerial ?: BigInteger.ONE,
notBefore,
notAfter,
params.certificateNotBefore ?: Date(),
params.certificateNotAfter ?: leafNotAfter,
subject,
subjectKeyPair.public,
)
@@ -244,17 +233,16 @@ object CertificateGenerator {
if (keyUsageBits != 0) {
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
}
if (params.attestationChallenge != null) {
builder.addExtension(
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
)
}
// Add our custom, simulated attestation extension.
builder.addExtension(
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
)
val signerAlgorithm =
when (signingKeyPair.private.algorithm) {
"EC", "ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
when (params.algorithm) {
Algorithm.EC -> "SHA256withECDSA"
Algorithm.RSA -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
}
val contentSigner =
JcaContentSignerBuilder(signerAlgorithm)
@@ -263,39 +251,4 @@ object CertificateGenerator {
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
}
// AOSP ta/src/keys.rs:452-478, ta/src/cert.rs:111-114
private fun buildSelfSignedCertificate(
keyPair: KeyPair,
params: KeyMintAttestation,
): Certificate {
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
val notBefore = params.certificateNotBefore ?: Date(0)
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
val builder = JcaX509v3CertificateBuilder(
subject,
params.certificateSerial ?: BigInteger.ONE,
notBefore,
notAfter,
subject,
keyPair.public,
)
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
if (keyUsageBits != 0) {
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
}
val signerAlgorithm = when (keyPair.private.algorithm) {
"EC", "ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}")
}
val contentSigner = JcaContentSignerBuilder(signerAlgorithm)
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keyPair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
}
}
@@ -1,124 +0,0 @@
package org.matrix.TEESimulator.pki
import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.KeyFactory
import java.security.KeyPair
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec
import org.matrix.TEESimulator.logging.SystemLogger
data class CertGenConfig(
val algorithm: Int,
val keySize: Int,
val ecCurve: Int,
val rsaPublicExponent: Long,
val attestationChallenge: ByteArray?,
val purposes: IntArray,
val digests: IntArray,
val certSerial: ByteArray?,
val certSubject: ByteArray?,
val certNotBefore: Long,
val certNotAfter: Long,
val keyboxPrivateKey: ByteArray,
val keyboxCertChain: ByteArray,
val securityLevel: Int,
val attestVersion: Int,
val keymasterVersion: Int,
val osVersion: Int,
val osPatchLevel: Int,
val vendorPatchLevel: Int,
val bootPatchLevel: Int,
val bootKey: ByteArray,
val bootHash: ByteArray,
val creationDatetime: Long,
val attestationApplicationId: ByteArray,
val moduleHash: ByteArray?,
val idBrand: ByteArray?,
val idDevice: ByteArray?,
val idProduct: ByteArray?,
val idSerial: ByteArray?,
val idImei: ByteArray?,
val idMeid: ByteArray?,
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 {
private const val LOG_DIR = "/data/adb/tricky_store/logs"
@Volatile
var isAvailable: Boolean = false
private set
fun initialize(libraryPath: String) {
try {
System.load(libraryPath)
initLogging(false, LOG_DIR)
isAvailable = true
SystemLogger.info("NativeCertGen: loaded libcertgen.so successfully")
} catch (e: UnsatisfiedLinkError) {
SystemLogger.error("NativeCertGen: failed to load libcertgen.so, falling back to BouncyCastle", e)
}
}
external fun generateAttestedKeyPair(config: CertGenConfig): ByteArray?
private external fun initLogging(verbose: Boolean, logDir: String): Boolean
private external fun dumpLogs(): String?
fun dump(): String? = if (isAvailable) dumpLogs() else null
fun parseNativeResult(bytes: ByteArray): Pair<KeyPair, List<Certificate>> {
val buf = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN)
val pkLen = buf.getInt()
if (pkLen < 0 || pkLen > buf.remaining()) {
throw IllegalStateException("Invalid private key length: $pkLen")
}
val pkBytes = ByteArray(pkLen)
buf.get(pkBytes)
val numCerts = buf.getInt()
if (numCerts < 0 || numCerts > buf.remaining()) {
throw IllegalStateException("Invalid cert count: $numCerts")
}
val certs = mutableListOf<Certificate>()
val certFactory = CertificateFactory.getInstance("X.509")
repeat(numCerts) {
val certLen = buf.getInt()
if (certLen < 0 || certLen > buf.remaining()) {
throw IllegalStateException("Invalid cert length: $certLen")
}
val certBytes = ByteArray(certLen)
buf.get(certBytes)
certs.add(certFactory.generateCertificate(ByteArrayInputStream(certBytes)))
}
if (certs.isEmpty()) {
throw IllegalStateException("No certificates in native result")
}
val algorithmName = when (certs[0].publicKey.algorithm) {
"EC", "ECDSA" -> "EC"
"RSA" -> "RSA"
else -> certs[0].publicKey.algorithm
}
val keyFactory = KeyFactory.getInstance(algorithmName)
val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(pkBytes))
val publicKey = certs[0].publicKey
return Pair(KeyPair(publicKey, privateKey), certs)
}
}
@@ -1,11 +1,9 @@
package org.matrix.TEESimulator.util
import android.content.pm.PackageManager
import android.hardware.security.keymint.SecurityLevel
import android.os.Build
import android.os.SystemProperties
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.security.MessageDigest
import java.time.LocalDate
import java.util.concurrent.ThreadLocalRandom
@@ -13,6 +11,7 @@ import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERSet
import org.matrix.TEESimulator.attestation.DeviceAttestationService
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
@@ -91,33 +90,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 +157,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 {
@@ -387,17 +353,9 @@ object AndroidDeviceUtils {
if (securityLevel == SecurityLevel.STRONGBOX) {
return 300
}
val cached = DeviceAttestationService.CachedAttestationData?.attestVersion
val version = cached
return DeviceAttestationService.CachedAttestationData?.attestVersion
?: attestVersionMap[Build.VERSION.SDK_INT]
?: 400 // Default to a recent version
val source = when {
cached != null -> "cache"
attestVersionMap.containsKey(Build.VERSION.SDK_INT) -> "map"
else -> "default"
}
SystemLogger.debug("attestVersion=$version source=$source securityLevel=$securityLevel")
return version
}
/**
@@ -406,178 +364,59 @@ object AndroidDeviceUtils {
* @param securityLevel The security level, used to determine the correct attestation version.
* @return The appropriate Keymaster or KeyMint version number.
*/
fun getKeymasterVersion(securityLevel: Int): Int = getAttestVersion(securityLevel)
fun getKeymasterVersion(securityLevel: Int): Int {
val attestVersion = getAttestVersion(securityLevel)
return if (attestVersion >= 100) attestVersion else 41 // Keymaster 4.1 for older versions
}
// --- APEX and Module Hash Properties ---
// Minimal protobuf parser for apex_manifest.pb (field 1: name, field 2: version)
private class MinimalApexManifestParser(private val data: ByteArray) {
var pos = 0
fun parse(): Pair<String, Long>? {
var name: String? = null
var version: Long? = null
while (pos < data.size) {
val tag = readVarint()
val fieldNum = tag ushr 3
val wireType = (tag and 0x07).toInt()
when (fieldNum) {
1L -> {
val length = readVarint().toInt()
if (pos + length > data.size) return null
name = String(data, pos, length, Charsets.UTF_8)
pos += length
}
2L -> {
version = readVarint()
}
else -> skipField(wireType)
}
}
return if (name != null && version != null) {
name to version
} else {
null
}
}
private fun readVarint(): Long {
var value = 0L
var shift = 0
while (pos < data.size) {
val b = data[pos++].toInt()
value = value or ((b and 0x7F).toLong() shl shift)
if ((b and 0x80) == 0) return value
shift += 7
}
return value
}
private fun skipField(wireType: Int) {
when (wireType) {
0 -> readVarint()
1 -> pos += 8
2 -> {
val len = readVarint().toInt()
pos += len
}
5 -> pos += 4
else -> throw IllegalStateException("Unknown wire type $wireType")
}
}
}
private val apexInfos: List<Pair<String, Long>> by lazy {
val results = mutableListOf<Pair<String, Long>>()
val apexRoot = File("/apex")
if (!apexRoot.exists() || !apexRoot.isDirectory) {
return@lazy emptyList()
}
apexRoot.listFiles()?.forEach { file ->
if (!file.isDirectory) return@forEach
val name = file.name
if (name.startsWith(".")) return@forEach
if (name.contains("@")) return@forEach
if (name == "sharedlibs") return@forEach
val manifestFile = File(file, "apex_manifest.pb")
if (manifestFile.exists()) {
runCatching {
val bytes = FileInputStream(manifestFile).use { it.readBytes() }
val parser = MinimalApexManifestParser(bytes)
parser.parse()?.let { (pkgName, version) -> results.add(pkgName to version) }
}
runCatching {
val pm = ConfigurationManager.getPackageManager()
val packages =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm?.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0)
} else {
@Suppress("DEPRECATION")
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
}
packages?.list.orEmpty().map { it.packageName to it.longVersionCode }
}
.getOrElse {
SystemLogger.error("Failed to get APEX package information.", it)
emptyList()
}
}
results.distinctBy { it.first }
}
val moduleHash: ByteArray by lazy {
DeviceAttestationService.CachedAttestationData?.moduleHash
?: runCatching {
data class ModuleEntry(
val nameEncoded: ByteArray,
val fullEncoded: ByteArray,
)
// TODO: figure out the correct calculation
val moduleSequences = ASN1EncodableVector()
val modules =
apexInfos.map { (packageName, versionCode) ->
val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
val versionInt = ASN1Integer(versionCode)
// 1. Create a DERSequence for each module.
apexInfos.forEach { (packageName, versionCode) ->
val moduleVector = ASN1EncodableVector()
// Use explicit UTF-8 encoding for the package name.
moduleVector.add(DEROctetString(packageName.toByteArray(Charsets.UTF_8)))
moduleVector.add(ASN1Integer(versionCode))
moduleSequences.add(DERSequence(moduleVector))
}
val vec = ASN1EncodableVector()
vec.add(nameOctet)
vec.add(versionInt)
val sequence = DERSequence(vec)
// 2. Create a DERSet. Bouncy Castle will automatically handle
// the sorting based on the DER-encoded value of each sequence.
val modulesSet = DERSet(moduleSequences)
// AOSP sorts by encoded name only, not full sequence
ModuleEntry(
nameEncoded = nameOctet.encoded,
fullEncoded = sequence.encoded,
)
}
// 3. Get the final DER-encoded byte array of the SET.
val encodedModules = modulesSet.encoded
val sortedModules =
modules.sortedWith { m1, m2 ->
compareByteArrays(m1.nameEncoded, m2.nameEncoded)
}
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
val finalDerSet = encodeAsDerSet(payload)
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
// 4. Compute the SHA-256 hash.
MessageDigest.getInstance("SHA-256").digest(encodedModules)
}
.getOrElse {
SystemLogger.error("Failed to compute module hash.", it)
ByteArray(32)
ByteArray(32) // Return empty hash on failure
}
}
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
val length = minOf(a.size, b.size)
for (i in 0 until length) {
val byteA = a[i].toInt() and 0xFF
val byteB = b[i].toInt() and 0xFF
if (byteA != byteB) {
return byteA - byteB
}
}
return a.size - b.size
}
private fun encodeAsDerSet(payload: ByteArray): ByteArray {
val out = ByteArrayOutputStream()
out.write(0x31)
writeDerLength(out, payload.size)
out.write(payload)
return out.toByteArray()
}
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
if (length < 128) {
out.write(length)
} else {
var size = length
val bytes = ArrayList<Byte>()
while (size > 0) {
bytes.add((size and 0xFF).toByte())
size = size ushr 8
}
out.write(0x80 or bytes.size)
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")
}
}
@@ -1,64 +0,0 @@
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
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)
}
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)
}
}
+20 -2
View File
@@ -2,10 +2,28 @@
MODDIR=${0%/*}
CONFIG_DIR=/data/adb/tricky_store
echo "============================================"
echo " TEESimulator — Key Storage Maintenance"
echo "============================================"
echo ""
if [ -d "$CONFIG_DIR/persistent_keys" ]; then
KEY_COUNT=$(find "$CONFIG_DIR/persistent_keys" -name "*.bin" 2>/dev/null | wc -l)
STORAGE_SIZE=$(du -sh "$CONFIG_DIR/persistent_keys" 2>/dev/null | cut -f1)
echo " Cached keys found : $KEY_COUNT"
echo " Storage used : $STORAGE_SIZE"
echo ""
rm -rf "$CONFIG_DIR/persistent_keys"
mkdir -p "$CONFIG_DIR/persistent_keys"
echo "Persistent key storage cleared"
echo " [OK] All cached attestation keys purged"
echo " [OK] Fresh keys will generate on next request"
else
echo "No persistent key storage found"
echo " No persistent key storage found"
echo " Nothing to clear"
fi
echo ""
echo "============================================"
+28 -198
View File
@@ -1,213 +1,43 @@
## TEESimulator-RS v6.0.0
## TEESimulator v3.2: Anti-Detection Hardening & Key Persistence
Repository consolidation release. All tee-rebuild work merged as the new main branch.
### AOSP Self-Signed Cert Compliance
- No-challenge keys now generate self-signed certs (subject == issuer, depth 1), matching AOSP `ta/src/keys.rs:451-478`
- Both Kotlin (BouncyCastle) and Rust (native-certgen) paths corrected
- Eliminates attestation behavioral probes that detect keybox issuer on non-attested keys
### Stability
- Binder stress crash hardening for concurrent generateKey calls
- AUTO mode TEE race for consistent attestation on devices with working G10
- Oversized transactions routed to software gen instead of crashing
- Operation-time params (BLOCK_MODE, PADDING, DIGEST) passed through to CipherPrimitive
### Banking App Compatibility
- Bare `target.txt` entries now default to AUTO mode, resolved at config level to PATCH (working TEE) or GENERATE (broken TEE)
- Fixes BHIM and similar banking apps that require TEE-backed attestation keys
- Restores v5.0 behavior where AUTO was resolved before the interceptor dispatch, avoiding the non-deterministic `raceTeePatch` path
### Infrastructure
- Version scheme changed to semver (v6.0.0)
- Repository moved to TEESimulator-RS as canonical source
---
## 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).
- **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.
On devices where the kernel doesn't set `ro.boot.vbmeta.public_key_digest`, the fallback chain hit random generation every boot because `resetprop` overrides for `ro.boot.*` props don't survive reboots. Added file-based persistence (`boot_hash.bin`, `boot_key.bin`) between the TEE cache and random fallback. Once determined, boot identity values persist across reboots.
Verified on Redmi 14C: second boot reads from persistent file instead of regenerating.
---
## TEESimulator v4.0: Native Rust Cert Generation
Major release. Certificate chain generation rebuilt from the ground up in Rust, replacing the BouncyCastle Java path for EC and RSA keys. Hardened against every known detector app.
### Native Cert Generation
The headline feature. `libcertgen.so` generates X.509 certificate chains using `ring` (EC-P256/P384) and `rsa` (RSA-2048/4096) with manual DER assembly. No more BouncyCastle quirks — issuer/subject DN bytes are injected directly from the keybox, ensuring byte-perfect chain linkage. BouncyCastle remains as fallback for unsupported curves (P-224, P-521, Curve25519).
This release hardens TEESimulator against active attestation probing by detector apps (DuckDetector, Luna, GarfieldHan) while introducing persistent key storage that survives daemon restarts and reboots.
### Anti-Detection Hardening
- **Challenge validation** — Oversized attestation challenges (>128 bytes) now return `INVALID_INPUT_LENGTH (-21)`, matching real KeyMint behavior. Previously accepted silently — DuckDetector exploited this.
- **Per-UID rate limiter** — 2 hardware keygens per 30s burst, 2 concurrent max. Overflow falls back to software certs. Blocks DuckDetector-style keygen flooding that starves GMS.
- **importKey eviction guard** — Retained patch chains prevent generate-then-import attacks that evict cached attestation data.
- **256KB native payload cap** — Oversized binder payloads bypass interception cleanly instead of stalling threads.
- **Alias size rejection** — Oversized key aliases rejected before they hit the binder buffer.
* **Per-UID Hardware Keygen Rate Limiter**: Caps hardware key generation at 2 per 30-second window with 2 max concurrent requests per UID. Overflow requests fall back to software certificate generation, preventing binder thread starvation from flood attacks.
* **importKey Eviction Defense**: Retains patched attestation chains when `importKey` overwrites an attested alias. Blocks the generate-then-import attack vector used by GarfieldHan and similar detectors.
* **Native Binder Payload Cap**: Bypasses interception for payloads exceeding 256KB, preventing thread starvation from oversized binder transactions.
* **Oversized Alias Rejection**: Rejects aliases that would exhaust the binder buffer, closing another flooding vector.
### Security Patch Consistency
* **Three-Way Patch Level Alignment**: When `system=prop` in `security_patch.txt`, boot and vendor patch levels are forced to `prop` as well. All three ASN.1 attestation tags (706/718/719) now resolve via `SystemProperties.get()` to match what detector apps see through `getprop`.
### Key Persistence
Generated keys now survive reboots. File-backed storage with file-level locking, preserved across keybox rotations. Banking and biometric apps that cache attestation keys no longer break after restart.
* **Generated Key Persistence Layer**: Keys from `generateKey` are persisted to disk in binary format with version headers and atomic writes (tmp + rename).
* **Automatic Restoration**: Persisted keys are restored on daemon startup without re-attestation.
* **Keybox Rotation Survival**: Generated keys survive keybox.xml changes — only PATCH-mode cert chains are invalidated.
* **File-Level Locking**: Concurrent read/write access to persisted keys is serialized to prevent corruption.
### Attestation Fixes
### Process Reliability
- Null out all-zero `verifiedBootHash` from TEE cache (fingerprinting vector)
- Correct `module_hash` field to match AOSP Keystore2 format
- Override pre-existing attest keys instead of skipping them
- Strip HTML comments from PEM blocks in keybox parsing
- Security patch consistency — `system=prop` forces boot/vendor to match
* **Fork-Based Supervisor Daemon**: Replaces the restart loop with a native fork-based supervisor for near-instant recovery.
* **Attestation Leak Blocking**: Returns `DEAD_OBJECT` to callers when the interceptor service is unavailable, preventing unpatched attestation from leaking through.
* **Global Exception Handler**: Catches uncaught exceptions and triggers clean daemon restart instead of silent death.
* **FileObserver NPE Fix**: Prevents crash when config files are deleted while being observed.
### Upstream Cherry-Picks
* **KeyUsage per HAL spec** (#119): Correct certificate KeyUsage based on KeyPurpose.
* **Reference leak fix** (#122): Resolve strong reference leak and warnings in binder interception.
### Module Lifecycle
- Supervisor daemon keeps the interceptor alive
- KSU Action button clears persistent key cache
- Clean uninstall removes all traces (persistent keys, TEE status, daemon)
* **`action.sh`**: Purge persistent key storage via KSU Manager Action button. Shows key count and storage size before clearing.
* **`uninstall.sh`**: Clean module removal — kills daemon, removes generated data, preserves `target.txt`, `keybox.xml`, and `security_patch.txt`.
### Stability
### PKI Fixes
- FileObserver NPE on config deletion fixed
- Global uncaught exception handler — daemon stays alive on unexpected errors
- PEM parsing hardened against malformed keybox files
* Strip HTML comments from PEM blocks before parsing.
### Tested Against
DuckDetector, Luna, Play Integrity, Key Attestation Demo — all passing on Redmi 14C (Android 14, Beanpod KeyMaster, KSU).
+1 -9
View File
@@ -15,7 +15,7 @@ fi
# --- Version Info ---
VERSION=$(grep_prop version "${TMPDIR}/module.prop")
ui_print "- Installing TEESimulator-RS $VERSION"
ui_print "- Installing TEESimulator $VERSION"
ui_print ""
# --- Architecture Handling ---
@@ -68,7 +68,6 @@ ui_print "- Extracting $ARCH libraries"
install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH"
install_file "lib/$ABI_DIR/libinject.so" "$MODPATH"
install_file "lib/$ABI_DIR/libsupervisor.so" "$MODPATH"
install_file "lib/$ABI_DIR/libcertgen.so" "$MODPATH"
ui_print ""
mv "$MODPATH/libinject.so" "$MODPATH/inject"
@@ -91,10 +90,3 @@ 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
+2 -2
View File
@@ -1,7 +1,7 @@
id=tricky_store
name=TEESimulator-RS
name=TEESimulator
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-RS/main/module/update.json
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/update.json
-1
View File
@@ -9,4 +9,3 @@ done
rm -rf "$CONFIG_DIR/persistent_keys"
rm -f "$CONFIG_DIR/tee_status.txt"
rm -f "$CONFIG_DIR/boot_hash.bin" "$CONFIG_DIR/boot_key.bin"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"version": "v6.0.0",
"versionCode": 155,
"zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/latest/download/TEESimulator-RS-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
"version": "v3.2",
"versionCode": 82,
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v3.2/TEESimulator-v3.2-82-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
}
-11
View File
@@ -1,11 +0,0 @@
[target.aarch64-linux-android]
linker = "aarch64-linux-android29-clang"
[target.armv7-linux-androideabi]
linker = "armv7a-linux-androideabi29-clang"
[target.i686-linux-android]
linker = "i686-linux-android29-clang"
[target.x86_64-linux-android]
linker = "x86_64-linux-android29-clang"
-1166
View File
File diff suppressed because it is too large Load Diff
-32
View File
@@ -1,32 +0,0 @@
[package]
name = "certgen"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
jni = { version = "0.21.1", default-features = false }
ring = "0.17.14"
rsa = { version = "0.9", features = ["sha2"] }
pkcs8 = { version = "0.10", features = ["alloc"] }
rand = "0.8"
der = { version = "0.7.10", features = ["alloc", "oid"] }
const-oid = "0.9.6"
x509-cert = { version = "0.2.5", features = ["pem"] }
time = { version = "0.3", features = ["std"] }
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
libc = "0.2"
zip = { version = "2.2", default-features = false, features = ["deflate"] }
serde_json = "1.0"
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = "symbols"
panic = "abort"
-8
View File
@@ -1,8 +0,0 @@
[toolchain]
channel = "stable"
targets = [
"aarch64-linux-android",
"armv7-linux-androideabi",
"i686-linux-android",
"x86_64-linux-android",
]
-721
View File
@@ -1,721 +0,0 @@
use crate::error::Result;
use crate::types::CertGenParams;
const DO_NOT_REPORT: i32 = -1;
pub fn build_attestation_extension(params: &CertGenParams) -> Result<Vec<u8>> {
let sw = build_software_enforced(params)?;
let tee = build_tee_enforced(params)?;
let mut inner = Vec::new();
// attestationVersion — INTEGER
inner.extend_from_slice(&enc_integer(params.attest_version as i64));
// attestationSecurityLevel — ENUMERATED, not INTEGER
inner.extend_from_slice(&enc_enumerated(params.security_level));
// keymintVersion — INTEGER
inner.extend_from_slice(&enc_integer(params.keymaster_version as i64));
// keymintSecurityLevel — ENUMERATED, not INTEGER
inner.extend_from_slice(&enc_enumerated(params.security_level));
// attestationChallenge — OCTET STRING
inner.extend_from_slice(&enc_octet_string(
params.attestation_challenge.as_deref().unwrap_or(&[]),
));
// uniqueId — OCTET STRING (always empty)
inner.extend_from_slice(&enc_octet_string(&[]));
// softwareEnforced
inner.extend_from_slice(&sw);
// teeEnforced
inner.extend_from_slice(&tee);
Ok(enc_sequence(&inner))
}
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)));
// Tag 709: ATTESTATION_APPLICATION_ID — OCTET STRING
// The bytes are already the DER-encoded AttestationApplicationId wrapped in OCTET STRING
// by the Kotlin layer. We wrap them in an EXPLICIT tag.
if !params.attestation_application_id.is_empty() {
fields.push((709, enc_octet_string(&params.attestation_application_id)));
}
// Tag 724: MODULE_HASH — OCTET STRING (only if attestVersion >= 400)
if params.attest_version >= 400 {
if let Some(ref hash) = params.module_hash {
fields.push((724, enc_octet_string(hash)));
}
}
Ok(build_authorization_list(&mut fields))
}
fn build_tee_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
// Tag 1: PURPOSE — SET OF INTEGER
if !params.purposes.is_empty() {
fields.push((1, build_set_of_integer(&params.purposes)));
}
// Tag 2: ALGORITHM — INTEGER
fields.push((2, enc_integer(params.algorithm as i32 as i64)));
// Tag 3: KEY_SIZE — INTEGER
fields.push((3, enc_integer(params.key_size as i64)));
// Tag 5: DIGEST — SET OF INTEGER
if !params.digests.is_empty() {
fields.push((5, build_set_of_integer(&params.digests)));
}
// Tag 10: EC_CURVE — INTEGER (only for EC keys)
if let Some(curve) = params.ec_curve {
fields.push((10, enc_integer(curve as i32 as i64)));
}
// 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)));
// Tag 704: ROOT_OF_TRUST — SEQUENCE
fields.push((704, build_root_of_trust(params)));
// Tag 705: OS_VERSION — INTEGER
if params.os_version != DO_NOT_REPORT {
fields.push((705, enc_integer(params.os_version as i64)));
}
// Tag 706: OS_PATCHLEVEL — INTEGER
if params.os_patch_level != DO_NOT_REPORT {
fields.push((706, enc_integer(params.os_patch_level as i64)));
}
// Tags 710-717: ATTESTATION_ID_* — OCTET STRING (optional)
if let Some(ref v) = params.id_brand {
fields.push((710, enc_octet_string(v)));
}
if let Some(ref v) = params.id_device {
fields.push((711, enc_octet_string(v)));
}
if let Some(ref v) = params.id_product {
fields.push((712, enc_octet_string(v)));
}
if let Some(ref v) = params.id_serial {
fields.push((713, enc_octet_string(v)));
}
if let Some(ref v) = params.id_imei {
fields.push((714, enc_octet_string(v)));
}
if let Some(ref v) = params.id_meid {
fields.push((715, enc_octet_string(v)));
}
if let Some(ref v) = params.id_manufacturer {
fields.push((716, enc_octet_string(v)));
}
if let Some(ref v) = params.id_model {
fields.push((717, enc_octet_string(v)));
}
// Tag 718: VENDOR_PATCHLEVEL — INTEGER
if params.vendor_patch_level != DO_NOT_REPORT {
fields.push((718, enc_integer(params.vendor_patch_level as i64)));
}
// Tag 719: BOOT_PATCHLEVEL — INTEGER
if params.boot_patch_level != DO_NOT_REPORT {
fields.push((719, enc_integer(params.boot_patch_level as i64)));
}
// Tag 723: ATTESTATION_ID_SECOND_IMEI — OCTET STRING (only if attestVersion >= 300)
if params.attest_version >= 300 {
if let Some(ref v) = params.id_second_imei {
fields.push((723, enc_octet_string(v)));
}
}
Ok(build_authorization_list(&mut fields))
}
fn build_root_of_trust(params: &CertGenParams) -> Vec<u8> {
let mut inner = Vec::new();
// verifiedBootKey — OCTET STRING (32 bytes)
inner.extend_from_slice(&enc_octet_string(&params.boot_key));
// deviceLocked — BOOLEAN TRUE (0xFF, not 0x01)
inner.extend_from_slice(&enc_boolean(true));
// verifiedBootState — ENUMERATED 0 (Verified), not INTEGER
inner.extend_from_slice(&enc_enumerated(0));
// verifiedBootHash — OCTET STRING (32 bytes)
inner.extend_from_slice(&enc_octet_string(&params.boot_hash));
enc_sequence(&inner)
}
fn build_authorization_list(fields: &mut Vec<(u32, Vec<u8>)>) -> Vec<u8> {
fields.sort_by_key(|(tag, _)| *tag);
let mut inner = Vec::new();
for (tag, value) in fields.iter() {
inner.extend_from_slice(&enc_explicit_tag(*tag, value));
}
enc_sequence(&inner)
}
fn build_set_of_integer(values: &[i32]) -> Vec<u8> {
// DER SET OF: elements sorted by encoded byte value
let mut encoded: Vec<Vec<u8>> = values.iter().map(|v| enc_integer(*v as i64)).collect();
encoded.sort();
let mut inner = Vec::new();
for e in &encoded {
inner.extend_from_slice(e);
}
enc_set(&inner)
}
// --- DER primitives ---
fn enc_length(len: usize) -> Vec<u8> {
if len < 0x80 {
vec![len as u8]
} else if len <= 0xFF {
vec![0x81, len as u8]
} else if len <= 0xFFFF {
vec![0x82, (len >> 8) as u8, len as u8]
} else if len <= 0xFF_FFFF {
vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
} else {
vec![
0x84,
(len >> 24) as u8,
(len >> 16) as u8,
(len >> 8) as u8,
len as u8,
]
}
}
fn enc_integer(value: i64) -> Vec<u8> {
// DER INTEGER: tag 0x02, minimal two's complement big-endian
let bytes = integer_bytes(value);
let mut out = vec![0x02];
out.extend_from_slice(&enc_length(bytes.len()));
out.extend_from_slice(&bytes);
out
}
fn integer_bytes(value: i64) -> Vec<u8> {
if value == 0 {
return vec![0x00];
}
let raw = value.to_be_bytes();
// Find first significant byte
let mut start = 0;
if value > 0 {
while start < 7 && raw[start] == 0x00 {
start += 1;
}
// If high bit set, need leading 0x00 to keep positive
if raw[start] & 0x80 != 0 {
let mut out = vec![0x00];
out.extend_from_slice(&raw[start..]);
return out;
}
} else {
while start < 7 && raw[start] == 0xFF {
start += 1;
}
// If high bit clear, need leading 0xFF to keep negative
if raw[start] & 0x80 == 0 {
let mut out = vec![0xFF];
out.extend_from_slice(&raw[start..]);
return out;
}
}
raw[start..].to_vec()
}
fn enc_enumerated(value: i32) -> Vec<u8> {
// DER ENUMERATED: tag 0x0A, same value encoding as INTEGER
let bytes = integer_bytes(value as i64);
let mut out = vec![0x0A];
out.extend_from_slice(&enc_length(bytes.len()));
out.extend_from_slice(&bytes);
out
}
fn enc_octet_string(data: &[u8]) -> Vec<u8> {
let mut out = vec![0x04];
out.extend_from_slice(&enc_length(data.len()));
out.extend_from_slice(data);
out
}
fn enc_null() -> Vec<u8> {
vec![0x05, 0x00]
}
fn enc_boolean(value: bool) -> Vec<u8> {
// DER BOOLEAN: TRUE = 0xFF, FALSE = 0x00
vec![0x01, 0x01, if value { 0xFF } else { 0x00 }]
}
fn enc_sequence(contents: &[u8]) -> Vec<u8> {
let mut out = vec![0x30];
out.extend_from_slice(&enc_length(contents.len()));
out.extend_from_slice(contents);
out
}
fn enc_set(contents: &[u8]) -> Vec<u8> {
let mut out = vec![0x31];
out.extend_from_slice(&enc_length(contents.len()));
out.extend_from_slice(contents);
out
}
fn enc_explicit_tag(tag_number: u32, inner: &[u8]) -> Vec<u8> {
// EXPLICIT context-specific constructed tag
let mut out = Vec::new();
if tag_number < 31 {
// Short form: single byte 0xA0 | tag_number
out.push(0xA0 | tag_number as u8);
} else {
// Long form: 0xBF followed by base-128 encoding of tag number
out.push(0xBF);
enc_base128_tag(&mut out, tag_number);
}
out.extend_from_slice(&enc_length(inner.len()));
out.extend_from_slice(inner);
out
}
fn enc_base128_tag(out: &mut Vec<u8>, tag: u32) {
// Base-128 with continuation bits: MSB first, bit 7 set on all but last byte
let mut digits = Vec::new();
let mut val = tag;
digits.push((val & 0x7F) as u8);
val >>= 7;
while val > 0 {
digits.push((val & 0x7F) as u8 | 0x80);
val >>= 7;
}
// Written MSB first
for b in digits.iter().rev() {
out.push(*b);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Algorithm, EcCurve};
#[test]
fn test_enc_integer_zero() {
assert_eq!(enc_integer(0), vec![0x02, 0x01, 0x00]);
}
#[test]
fn test_enc_integer_small_positive() {
assert_eq!(enc_integer(3), vec![0x02, 0x01, 0x03]);
assert_eq!(enc_integer(127), vec![0x02, 0x01, 0x7F]);
}
#[test]
fn test_enc_integer_needs_leading_zero() {
// 128 = 0x80, high bit set so needs 0x00 prefix
assert_eq!(enc_integer(128), vec![0x02, 0x02, 0x00, 0x80]);
assert_eq!(enc_integer(256), vec![0x02, 0x02, 0x01, 0x00]);
}
#[test]
fn test_enc_integer_multi_byte() {
// 140000 = 0x02_22_E0
assert_eq!(enc_integer(140000), vec![0x02, 0x03, 0x02, 0x22, 0xE0]);
}
#[test]
fn test_enc_integer_large() {
// 20250301 = 0x01_34_FE_BD
assert_eq!(
enc_integer(20250301),
vec![0x02, 0x04, 0x01, 0x34, 0xFE, 0xBD]
);
}
#[test]
fn test_enc_enumerated() {
// SecurityLevel TEE = 1
assert_eq!(enc_enumerated(1), vec![0x0A, 0x01, 0x01]);
// VerifiedBootState Verified = 0
assert_eq!(enc_enumerated(0), vec![0x0A, 0x01, 0x00]);
}
#[test]
fn test_enc_boolean_true() {
// DER: TRUE = 0xFF
assert_eq!(enc_boolean(true), vec![0x01, 0x01, 0xFF]);
}
#[test]
fn test_enc_null() {
assert_eq!(enc_null(), vec![0x05, 0x00]);
}
#[test]
fn test_enc_octet_string_empty() {
assert_eq!(enc_octet_string(&[]), vec![0x04, 0x00]);
}
#[test]
fn test_enc_explicit_tag_short() {
// Tag 1 wrapping INTEGER 2: A1 03 02 01 02
let inner = enc_integer(2);
let tagged = enc_explicit_tag(1, &inner);
assert_eq!(tagged, vec![0xA1, 0x03, 0x02, 0x01, 0x02]);
}
#[test]
fn test_enc_explicit_tag_10() {
// Tag 10: 0xAA
let inner = enc_integer(1);
let tagged = enc_explicit_tag(10, &inner);
assert_eq!(tagged[0], 0xAA);
}
#[test]
fn test_enc_explicit_tag_503() {
// Tag 503: 0xBF 0x83 0x77
// 503 = 3*128 + 119 => 0x83 0x77
let inner = enc_null();
let tagged = enc_explicit_tag(503, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x83, 0x77]);
}
#[test]
fn test_enc_explicit_tag_704() {
// Tag 704: 0xBF 0x85 0x40
// 704 = 5*128 + 64 => 0x85 0x40
let inner = enc_sequence(&[]);
let tagged = enc_explicit_tag(704, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x40]);
}
#[test]
fn test_enc_explicit_tag_718() {
// Tag 718: 0xBF 0x85 0x4E
let inner = enc_integer(20250301);
let tagged = enc_explicit_tag(718, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4E]);
}
#[test]
fn test_enc_explicit_tag_719() {
// Tag 719: 0xBF 0x85 0x4F
let inner = enc_integer(20250301);
let tagged = enc_explicit_tag(719, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4F]);
}
#[test]
fn test_enc_explicit_tag_701() {
// Tag 701: 0xBF 0x85 0x3D
let inner = enc_integer(1000);
let tagged = enc_explicit_tag(701, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x3D]);
}
#[test]
fn test_enc_explicit_tag_709() {
// Tag 709: 0xBF 0x85 0x45
let inner = enc_octet_string(&[0x01]);
let tagged = enc_explicit_tag(709, &inner);
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x45]);
}
#[test]
fn test_build_set_of_integer_sorted() {
// SET OF INTEGER must sort by encoded bytes
let result = build_set_of_integer(&[3, 2]);
// Expect sorted: INTEGER 2 before INTEGER 3
let expected = enc_set(&[0x02, 0x01, 0x02, 0x02, 0x01, 0x03]);
assert_eq!(result, expected);
}
#[test]
fn test_root_of_trust_structure() {
let params = make_test_params();
let rot = build_root_of_trust(&params);
// Should be a SEQUENCE (0x30)
assert_eq!(rot[0], 0x30);
// Find BOOLEAN TRUE inside
let rot_inner = &rot[2..]; // skip tag+length
// First: OCTET STRING (32 bytes boot key)
assert_eq!(rot_inner[0], 0x04);
assert_eq!(rot_inner[1], 0x20); // 32 bytes
// After boot key (34 bytes): BOOLEAN TRUE
assert_eq!(rot_inner[34], 0x01); // BOOLEAN tag
assert_eq!(rot_inner[35], 0x01); // length 1
assert_eq!(rot_inner[36], 0xFF); // TRUE = 0xFF
// Then ENUMERATED 0 (verifiedBootState)
assert_eq!(rot_inner[37], 0x0A); // ENUMERATED tag, not 0x02
assert_eq!(rot_inner[38], 0x01);
assert_eq!(rot_inner[39], 0x00);
}
#[test]
fn test_do_not_report_omits_fields() {
let mut params = make_test_params();
params.os_patch_level = DO_NOT_REPORT;
params.vendor_patch_level = DO_NOT_REPORT;
params.boot_patch_level = DO_NOT_REPORT;
let tee = build_tee_enforced(&params).unwrap();
let hex = hex_string(&tee);
// Tags 706, 718, 719 should not appear
// Tag 706 = BF 85 42, 718 = BF 85 4E, 719 = BF 85 4F
assert!(!hex.contains("bf8542"), "os_patch_level should be omitted");
assert!(
!hex.contains("bf854e"),
"vendor_patch_level should be omitted"
);
assert!(
!hex.contains("bf854f"),
"boot_patch_level should be omitted"
);
}
#[test]
fn test_key_description_security_level_is_enumerated() {
let params = make_test_params();
let ext = build_attestation_extension(&params).unwrap();
// KeyDescription is a SEQUENCE: 0x30 ...
assert_eq!(ext[0], 0x30);
// Skip SEQUENCE tag + length to get to inner fields
let inner = skip_tlv_header(&ext);
// Field 0: attestationVersion — INTEGER (0x02)
assert_eq!(inner[0], 0x02);
let (_, rest) = skip_one_tlv(inner);
// Field 1: attestationSecurityLevel — ENUMERATED (0x0A)
assert_eq!(rest[0], 0x0A, "attestationSecurityLevel must be ENUMERATED");
let (_, rest) = skip_one_tlv(rest);
// Field 2: keymintVersion — INTEGER (0x02)
assert_eq!(rest[0], 0x02);
let (_, rest) = skip_one_tlv(rest);
// Field 3: keymintSecurityLevel — ENUMERATED (0x0A)
assert_eq!(rest[0], 0x0A, "keymintSecurityLevel must be ENUMERATED");
}
#[test]
fn test_authorization_list_sorted_by_tag() {
let params = make_test_params();
let tee = build_tee_enforced(&params).unwrap();
let inner = skip_tlv_header(&tee);
let tags = extract_tag_numbers(inner);
let mut sorted = tags.clone();
sorted.sort();
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(&params).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(&params).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(&params).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();
let ext = build_attestation_extension(&params).unwrap();
// Must be valid DER: starts with SEQUENCE tag
assert_eq!(ext[0], 0x30);
// Length must account for all inner bytes
let (header_len, total_content_len) = parse_tlv_lengths(&ext);
assert_eq!(ext.len(), header_len + total_content_len);
}
// --- test helpers ---
fn make_test_params() -> CertGenParams {
CertGenParams {
algorithm: Algorithm::Ec,
key_size: 256,
ec_curve: Some(EcCurve::P256),
rsa_public_exponent: 0,
attestation_challenge: Some(vec![0xAB; 32]),
purposes: vec![2, 3],
digests: vec![4],
cert_serial: None,
cert_subject: None,
cert_not_before: -1,
cert_not_after: -1,
keybox_private_key: vec![],
keybox_cert_chain: vec![],
security_level: 1,
attest_version: 200,
keymaster_version: 200,
os_version: 140000,
os_patch_level: 202503,
vendor_patch_level: 20250301,
boot_patch_level: 20250301,
boot_key: vec![0x01; 32],
boot_hash: vec![0x02; 32],
creation_datetime: 1709913600000,
attestation_application_id: vec![0xDE, 0xAD],
module_hash: None,
id_brand: None,
id_device: None,
id_product: None,
id_serial: None,
id_imei: None,
id_meid: None,
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,
}
}
fn hex_string(data: &[u8]) -> String {
data.iter().map(|b| format!("{:02x}", b)).collect()
}
fn skip_tlv_header(data: &[u8]) -> &[u8] {
let (header_len, _) = parse_tlv_lengths(data);
&data[header_len..]
}
fn skip_one_tlv(data: &[u8]) -> (usize, &[u8]) {
let (header_len, content_len) = parse_tlv_lengths(data);
let total = header_len + content_len;
(total, &data[total..])
}
fn parse_tlv_lengths(data: &[u8]) -> (usize, usize) {
// Returns (header_bytes, content_bytes)
let tag_len = tag_byte_len(data);
let len_start = tag_len;
if data[len_start] < 0x80 {
(len_start + 1, data[len_start] as usize)
} else {
let num_len_bytes = (data[len_start] & 0x7F) as usize;
let mut content_len = 0usize;
for i in 0..num_len_bytes {
content_len = (content_len << 8) | data[len_start + 1 + i] as usize;
}
(len_start + 1 + num_len_bytes, content_len)
}
}
fn tag_byte_len(data: &[u8]) -> usize {
if data[0] & 0x1F != 0x1F {
1
} else {
let mut i = 1;
while data[i] & 0x80 != 0 {
i += 1;
}
i + 1
}
}
fn extract_tag_numbers(mut data: &[u8]) -> Vec<u32> {
let mut tags = Vec::new();
while !data.is_empty() {
let tag = read_tag_number(data);
tags.push(tag);
let (_, rest) = skip_one_tlv(data);
data = rest;
}
tags
}
fn read_tag_number(data: &[u8]) -> u32 {
if data[0] & 0x1F != 0x1F {
(data[0] & 0x1F) as u32
} else {
let mut val = 0u32;
let mut i = 1;
loop {
val = (val << 7) | (data[i] & 0x7F) as u32;
if data[i] & 0x80 == 0 {
break;
}
i += 1;
}
val
}
}
}
-582
View File
@@ -1,582 +0,0 @@
use crate::error::{CertGenError, Result};
use crate::keybox::ParsedKeybox;
use crate::types::{Algorithm, CertGenParams, GeneratedKeyPair};
use time::OffsetDateTime;
const ATTESTATION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 11129, 2, 1, 17];
// Signature algorithm OIDs
const OID_SHA256_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 2];
const OID_SHA384_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 3];
const OID_SHA256_WITH_RSA: &[u64] = &[1, 2, 840, 113549, 1, 1, 11];
// Extension OIDs
const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15];
// AOSP ta/src/keys.rs:451-478: no challenge = self-signed leaf, chain depth 1
pub fn build_self_signed_cert(
key_pair: &GeneratedKeyPair,
params: &CertGenParams,
) -> Result<Vec<Vec<u8>>> {
let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?;
let sig_alg_der = signature_algorithm_for_signing_key(&key_pair.private_key_pkcs8, params.algorithm)?;
let serial_bytes = if let Some(ref serial) = params.cert_serial {
serial.clone()
} else {
vec![1u8]
};
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
subject.clone()
} else {
encode_simple_cn_dn("Android Keystore Key")
};
let not_before = timestamp_to_datetime(params.cert_not_before)?;
let not_after = if params.cert_not_after == -1 {
// No keybox fallback available; use far-future (year 9999)
OffsetDateTime::from_unix_timestamp(253402300799)
.unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365 * 30))
} else {
timestamp_to_datetime(params.cert_not_after)?
};
let extensions_der = build_extensions(None, &params.purposes)?;
let version_der = encode_der_explicit_tag(0, &encode_der_integer(&[2]));
let serial_der = encode_der_integer(&serial_bytes);
let validity_der = encode_validity(&not_before, &not_after);
let extensions_tagged = encode_der_explicit_tag(3, &extensions_der);
// issuer == subject (self-signed, per AOSP ta/src/cert.rs:111-114)
let tbs_der = encode_der_sequence(&[
&version_der,
&serial_der,
&sig_alg_der,
&subject_dn_der,
&validity_der,
&subject_dn_der,
&spki_der,
&extensions_tagged,
]);
let signature_bytes = sign_tbs(&tbs_der, &key_pair.private_key_pkcs8, params.algorithm)?;
let signature_bit_string = encode_der_bit_string(&signature_bytes);
let cert_der = encode_der_sequence(&[
&tbs_der,
&sig_alg_der,
&signature_bit_string,
]);
Ok(vec![cert_der])
}
pub fn build_certificate_chain(
key_pair: &GeneratedKeyPair,
attestation_ext_der: Option<&[u8]>,
keybox: &ParsedKeybox,
params: &CertGenParams,
) -> Result<Vec<Vec<u8>>> {
let leaf_der = build_leaf_cert(key_pair, attestation_ext_der, keybox, params)?;
let mut chain = Vec::with_capacity(1 + keybox.cert_chain_ders.len());
chain.push(leaf_der);
for cert_der in &keybox.cert_chain_ders {
chain.push(cert_der.clone());
}
Ok(chain)
}
fn build_leaf_cert(
key_pair: &GeneratedKeyPair,
attestation_ext_der: Option<&[u8]>,
keybox: &ParsedKeybox,
params: &CertGenParams,
) -> Result<Vec<u8>> {
let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?;
let sig_alg_der = signature_algorithm_for_signing_key(&keybox.signing_key_der, params.algorithm)?;
// Serial number
let serial_bytes = if let Some(ref serial) = params.cert_serial {
serial.clone()
} else {
vec![1u8]
};
// Subject DN
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
subject.clone()
} else {
encode_simple_cn_dn("Android Keystore Key")
};
// Validity
let not_before = timestamp_to_datetime(params.cert_not_before)?;
let not_after = if params.cert_not_after == -1 {
OffsetDateTime::from_unix_timestamp(keybox.leaf_not_after)
.unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365))
} else {
timestamp_to_datetime(params.cert_not_after)?
};
let extensions_der = build_extensions(attestation_ext_der, &params.purposes)?;
// TBS Certificate
let version_der = encode_der_explicit_tag(0, &encode_der_integer(&[2]));
let serial_der = encode_der_integer(&serial_bytes);
let validity_der = encode_validity(&not_before, &not_after);
let extensions_tagged = encode_der_explicit_tag(3, &extensions_der);
let tbs_der = encode_der_sequence(&[
&version_der,
&serial_der,
&sig_alg_der,
&keybox.issuer_dn_der, // RAW bytes — no re-encoding
&validity_der,
&subject_dn_der,
&spki_der,
&extensions_tagged,
]);
// Sign the TBS
let signature_bytes = sign_tbs(&tbs_der, &keybox.signing_key_der, params.algorithm)?;
let signature_bit_string = encode_der_bit_string(&signature_bytes);
// Final certificate: SEQUENCE { TBS, sigAlgorithm, signature }
let cert_der = encode_der_sequence(&[
&tbs_der,
&sig_alg_der,
&signature_bit_string,
]);
Ok(cert_der)
}
fn sign_tbs(tbs_der: &[u8], signing_key_der: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
match algorithm {
Algorithm::Ec => sign_tbs_ec(tbs_der, signing_key_der),
Algorithm::Rsa => sign_tbs_rsa(tbs_der, signing_key_der),
}
}
fn sign_tbs_ec(tbs_der: &[u8], signing_key_der: &[u8]) -> Result<Vec<u8>> {
// Determine EC curve from the signing key's PKCS8 AlgorithmIdentifier
let alg = detect_ec_signing_algorithm(signing_key_der)?;
let key_pair = ring::signature::EcdsaKeyPair::from_pkcs8(alg, signing_key_der, &ring::rand::SystemRandom::new())
.map_err(|e| CertGenError::SigningFailed(format!("EC key parse: {e}")))?;
let rng = ring::rand::SystemRandom::new();
let sig = key_pair.sign(&rng, tbs_der)
.map_err(|e| CertGenError::SigningFailed(format!("EC sign: {e}")))?;
Ok(sig.as_ref().to_vec())
}
fn detect_ec_signing_algorithm(pkcs8_der: &[u8]) -> Result<&'static ring::signature::EcdsaSigningAlgorithm> {
use der::Decode;
let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der)
.map_err(|e| CertGenError::SigningFailed(format!("PKCS8 parse: {e}")))?;
let params_oid = info.algorithm.parameters_oid()
.map_err(|e| CertGenError::SigningFailed(format!("EC curve OID: {e}")))?;
let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse()
.map_err(|_| CertGenError::SigningFailed("OID parse".into()))?;
let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse()
.map_err(|_| CertGenError::SigningFailed("OID parse".into()))?;
if params_oid == p256_oid {
Ok(&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING)
} else if params_oid == p384_oid {
Ok(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING)
} else {
Err(CertGenError::SigningFailed(format!("unsupported EC curve OID: {params_oid}")))
}
}
fn sign_tbs_rsa(tbs_der: &[u8], signing_key_der: &[u8]) -> Result<Vec<u8>> {
use rsa::pkcs8::DecodePrivateKey;
use rsa::signature::{SignatureEncoding, SignerMut};
use rsa::pkcs1v15::SigningKey;
use rsa::sha2::Sha256;
let private_key = rsa::RsaPrivateKey::from_pkcs8_der(signing_key_der)
.map_err(|e| CertGenError::SigningFailed(format!("RSA key parse: {e}")))?;
let mut signing_key = SigningKey::<Sha256>::new(private_key);
let signature = signing_key.sign(tbs_der);
Ok(signature.to_vec())
}
fn signature_algorithm_for_signing_key(signing_key_der: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
match algorithm {
Algorithm::Ec => {
let ring_alg = detect_ec_signing_algorithm(signing_key_der)?;
// Determine OID from the algorithm used
let oid = if std::ptr::eq(ring_alg, &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING) {
OID_SHA384_WITH_ECDSA
} else {
OID_SHA256_WITH_ECDSA
};
let oid_der = encode_der_oid(oid);
Ok(encode_der_sequence(&[&oid_der]))
}
Algorithm::Rsa => {
let oid_der = encode_der_oid(OID_SHA256_WITH_RSA);
let null_der = vec![0x05, 0x00];
Ok(encode_der_sequence(&[&oid_der, &null_der]))
}
}
}
fn extract_spki_from_pkcs8(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
use der::Decode;
let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der)
.map_err(|e| CertGenError::CertBuildFailed(format!("PKCS8 parse for SPKI: {e}")))?;
// Reconstruct SPKI from AlgorithmIdentifier + public key
// For EC: derive public key from private key via ring
// For RSA: derive from rsa crate
let alg_id_oid = info.algorithm.oid;
let ec_oid: const_oid::ObjectIdentifier = "1.2.840.10045.2.1".parse()
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
if alg_id_oid == ec_oid {
extract_ec_spki(pkcs8_der, &info)
} else {
extract_rsa_spki(pkcs8_der)
}
}
fn extract_ec_spki(pkcs8_der: &[u8], info: &pkcs8::PrivateKeyInfo) -> Result<Vec<u8>> {
use ring::signature::KeyPair as _;
let params_oid = info.algorithm.parameters_oid()
.map_err(|e| CertGenError::CertBuildFailed(format!("EC curve OID: {e}")))?;
let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse()
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse()
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
let (ring_alg, curve_oid_der): (&ring::signature::EcdsaSigningAlgorithm, Vec<u8>) = if params_oid == p256_oid {
(&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, encode_der_oid(&[1, 2, 840, 10045, 3, 1, 7]))
} else if params_oid == p384_oid {
(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, encode_der_oid(&[1, 3, 132, 0, 34]))
} else {
return Err(CertGenError::CertBuildFailed(format!("unsupported EC curve: {params_oid}")));
};
let kp = ring::signature::EcdsaKeyPair::from_pkcs8(
ring_alg,
pkcs8_der,
&ring::rand::SystemRandom::new(),
).map_err(|e| CertGenError::CertBuildFailed(format!("EC key parse: {e}")))?;
let ec_kp = kp.public_key().as_ref().to_vec();
// SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (public key) }
// AlgorithmIdentifier = SEQUENCE { ecPublicKey OID, curve OID }
let ec_oid_der = encode_der_oid(&[1, 2, 840, 10045, 2, 1]);
let alg_id = encode_der_sequence(&[&ec_oid_der, &curve_oid_der]);
let pub_key_bits = encode_der_bit_string(&ec_kp);
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
}
fn extract_rsa_spki(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
use rsa::pkcs8::DecodePrivateKey;
let private_key = rsa::RsaPrivateKey::from_pkcs8_der(pkcs8_der)
.map_err(|e| CertGenError::CertBuildFailed(format!("RSA key parse: {e}")))?;
let public_key = rsa::RsaPublicKey::from(&private_key);
// Encode RSA public key as DER: SEQUENCE { n INTEGER, e INTEGER }
use rsa::traits::PublicKeyParts;
let n_bytes = public_key.n().to_bytes_be();
let e_bytes = public_key.e().to_bytes_be();
let rsa_pub_der = encode_der_sequence(&[
&encode_der_integer(&n_bytes),
&encode_der_integer(&e_bytes),
]);
// SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (DER-encoded RSAPublicKey) }
let rsa_oid_der = encode_der_oid(&[1, 2, 840, 113549, 1, 1, 1]);
let null_der = vec![0x05, 0x00];
let alg_id = encode_der_sequence(&[&rsa_oid_der, &null_der]);
let pub_key_bits = encode_der_bit_string(&rsa_pub_der);
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
}
fn build_extensions(attestation_ext_der: Option<&[u8]>, purposes: &[i32]) -> Result<Vec<u8>> {
let mut extensions: Vec<Vec<u8>> = Vec::new();
let ku_byte = map_key_usage_byte(purposes);
if ku_byte != 0 {
let ku_ext = build_key_usage_extension(ku_byte);
extensions.push(ku_ext);
}
if let Some(attest_der) = attestation_ext_der {
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attest_der);
extensions.push(attest_ext);
}
Ok(encode_der_sequence_of(&extensions))
}
fn build_extension(oid_der: &[u8], critical: bool, value_der: &[u8]) -> Vec<u8> {
let value_octet_string = encode_der_octet_string(value_der);
if critical {
let critical_der = encode_der_boolean(true);
encode_der_sequence(&[oid_der, &critical_der, &value_octet_string])
} else {
encode_der_sequence(&[oid_der, &value_octet_string])
}
}
fn build_key_usage_extension(ku_byte: u8) -> Vec<u8> {
// DER BIT STRING: minimal encoding requires trimming trailing zero bits
let unused_bits = ku_byte.trailing_zeros().min(7) as u8;
// BIT STRING = tag (0x03) + length(2) + unused_bits + byte
let bit_string = vec![0x03, 0x02, unused_bits, ku_byte];
let oid_der = encode_der_oid(OID_KEY_USAGE);
let value_octet_string = encode_der_octet_string(&bit_string);
let critical_der = encode_der_boolean(true);
encode_der_sequence(&[&oid_der, &critical_der, &value_octet_string])
}
// KeyUsage BIT STRING byte layout (RFC 5280):
// byte[0] bit 7 = digitalSignature (0x80)
// byte[0] bit 6 = nonRepudiation (0x40)
// byte[0] bit 5 = keyEncipherment (0x20)
// byte[0] bit 4 = dataEncipherment (0x10)
// byte[0] bit 3 = keyAgreement (0x08)
// byte[0] bit 2 = keyCertSign (0x04)
// byte[0] bit 1 = cRLSign (0x02)
// byte[0] bit 0 = encipherOnly (0x01)
// byte[1] bit 7 = decipherOnly (0x80)
fn map_key_usage_byte(purposes: &[i32]) -> u8 {
let mut bits: u8 = 0;
for &purpose in purposes {
match purpose {
2 => bits |= 0x80, // SIGN -> digitalSignature
1 => bits |= 0x10, // DECRYPT -> dataEncipherment
5 => bits |= 0x20, // WRAP_KEY -> keyEncipherment
6 => bits |= 0x08, // AGREE_KEY -> keyAgreement
7 => bits |= 0x04, // ATTEST_KEY -> keyCertSign
_ => {}
}
}
bits
}
fn encode_validity(not_before: &OffsetDateTime, not_after: &OffsetDateTime) -> Vec<u8> {
let nb = encode_time(not_before);
let na = encode_time(not_after);
encode_der_sequence(&[&nb, &na])
}
fn encode_time(dt: &OffsetDateTime) -> Vec<u8> {
let year = dt.year();
if (1950..2050).contains(&year) {
encode_utctime(dt)
} else {
encode_gentime(dt)
}
}
fn encode_utctime(dt: &OffsetDateTime) -> Vec<u8> {
// UTCTime: YYMMDDHHMMSSZ
let year = dt.year() % 100;
let s = format!(
"{:02}{:02}{:02}{:02}{:02}{:02}Z",
year, dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second()
);
let mut out = Vec::with_capacity(2 + s.len());
out.push(0x17); // UTCTime tag
out.extend_from_slice(&encode_der_length_bytes(s.len()));
out.extend_from_slice(s.as_bytes());
out
}
fn encode_gentime(dt: &OffsetDateTime) -> Vec<u8> {
// GeneralizedTime: YYYYMMDDHHMMSSZ
let s = format!(
"{:04}{:02}{:02}{:02}{:02}{:02}Z",
dt.year(), dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second()
);
let mut out = Vec::with_capacity(2 + s.len());
out.push(0x18); // GeneralizedTime tag
out.extend_from_slice(&encode_der_length_bytes(s.len()));
out.extend_from_slice(s.as_bytes());
out
}
fn encode_simple_cn_dn(cn: &str) -> Vec<u8> {
// Name = SEQUENCE OF RelativeDistinguishedName
// RDN = SET OF AttributeTypeAndValue
// ATV = SEQUENCE { OID, UTF8String }
let cn_oid = encode_der_oid(&[2, 5, 4, 3]);
let cn_value = encode_der_utf8string(cn);
let atv = encode_der_sequence(&[&cn_oid, &cn_value]);
let rdn = encode_der_set(&[&atv]);
encode_der_sequence(&[&rdn])
}
fn timestamp_to_datetime(ts: i64) -> Result<OffsetDateTime> {
if ts == -1 {
return Ok(OffsetDateTime::now_utc());
}
OffsetDateTime::from_unix_timestamp(ts / 1000)
.map_err(|e| CertGenError::CertBuildFailed(format!("invalid timestamp {ts}: {e}")))
}
// ---------------------------------------------------------------------------
// DER encoding primitives
// ---------------------------------------------------------------------------
fn encode_der_length_bytes(len: usize) -> Vec<u8> {
if len < 0x80 {
vec![len as u8]
} else if len <= 0xFF {
vec![0x81, len as u8]
} else if len <= 0xFFFF {
vec![0x82, (len >> 8) as u8, len as u8]
} else if len <= 0xFF_FFFF {
vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
} else {
vec![0x84, (len >> 24) as u8, (len >> 16) as u8, (len >> 8) as u8, len as u8]
}
}
fn encode_der_tag_length_value(tag: u8, content: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + 4 + content.len());
out.push(tag);
out.extend_from_slice(&encode_der_length_bytes(content.len()));
out.extend_from_slice(content);
out
}
fn encode_der_sequence(items: &[&[u8]]) -> Vec<u8> {
let total: usize = items.iter().map(|i| i.len()).sum();
let mut content = Vec::with_capacity(total);
for item in items {
content.extend_from_slice(item);
}
encode_der_tag_length_value(0x30, &content)
}
fn encode_der_sequence_of(items: &[Vec<u8>]) -> Vec<u8> {
let total: usize = items.iter().map(|i| i.len()).sum();
let mut content = Vec::with_capacity(total);
for item in items {
content.extend_from_slice(item);
}
encode_der_tag_length_value(0x30, &content)
}
fn encode_der_set(items: &[&[u8]]) -> Vec<u8> {
let total: usize = items.iter().map(|i| i.len()).sum();
let mut content = Vec::with_capacity(total);
for item in items {
content.extend_from_slice(item);
}
encode_der_tag_length_value(0x31, &content)
}
fn encode_der_explicit_tag(tag_num: u8, content: &[u8]) -> Vec<u8> {
encode_der_tag_length_value(0xA0 | tag_num, content)
}
fn encode_der_integer(value: &[u8]) -> Vec<u8> {
// DER INTEGER must have minimal encoding and leading 0x00 if high bit set
if value.is_empty() {
return encode_der_tag_length_value(0x02, &[0x00]);
}
// Strip leading zeros (but keep at least one byte)
let mut start = 0;
while start < value.len() - 1 && value[start] == 0 {
start += 1;
}
let trimmed = &value[start..];
// Add leading 0x00 if high bit is set (positive integer)
if trimmed[0] & 0x80 != 0 {
let mut padded = Vec::with_capacity(1 + trimmed.len());
padded.push(0x00);
padded.extend_from_slice(trimmed);
encode_der_tag_length_value(0x02, &padded)
} else {
encode_der_tag_length_value(0x02, trimmed)
}
}
fn encode_der_bit_string(bits: &[u8]) -> Vec<u8> {
// BIT STRING: tag 0x03, length, unused_bits (0), content
let mut content = Vec::with_capacity(1 + bits.len());
content.push(0x00); // 0 unused bits
content.extend_from_slice(bits);
encode_der_tag_length_value(0x03, &content)
}
fn encode_der_octet_string(content: &[u8]) -> Vec<u8> {
encode_der_tag_length_value(0x04, content)
}
fn encode_der_utf8string(s: &str) -> Vec<u8> {
encode_der_tag_length_value(0x0C, s.as_bytes())
}
fn encode_der_boolean(val: bool) -> Vec<u8> {
encode_der_tag_length_value(0x01, &[if val { 0xFF } else { 0x00 }])
}
fn encode_der_oid(components: &[u64]) -> Vec<u8> {
if components.len() < 2 {
return encode_der_tag_length_value(0x06, &[]);
}
let mut content = Vec::new();
// First two components encoded as 40 * c[0] + c[1]
content.push((components[0] * 40 + components[1]) as u8);
for &c in &components[2..] {
encode_oid_subidentifier(&mut content, c);
}
encode_der_tag_length_value(0x06, &content)
}
fn encode_oid_subidentifier(buf: &mut Vec<u8>, mut value: u64) {
if value == 0 {
buf.push(0);
return;
}
// Encode in base-128 with continuation bits
let mut bytes = Vec::new();
while value > 0 {
bytes.push((value & 0x7F) as u8);
value >>= 7;
}
bytes.reverse();
// Set high bit on all but the last byte
for i in 0..bytes.len() - 1 {
bytes[i] |= 0x80;
}
buf.extend_from_slice(&bytes);
}
-75
View File
@@ -1,75 +0,0 @@
use std::fmt;
#[derive(Debug)]
pub enum CertGenError {
Jni(String),
NullParam(&'static str),
UnsupportedAlgorithm(i32),
UnsupportedEcCurve(i32),
KeyGenFailed(String),
CertBuildFailed(String),
KeyboxParseFailed(String),
AttestationBuildFailed(String),
DerError(der::Error),
EmptyKeyboxChain,
ChallengeTooLong(usize),
InvalidParameter(String),
SigningFailed(String),
SerializationFailed(String),
}
impl fmt::Display for CertGenError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Jni(msg) => write!(f, "JNI error: {}", msg),
Self::NullParam(name) => write!(f, "null required parameter: {}", name),
Self::UnsupportedAlgorithm(v) => write!(f, "unsupported algorithm: {}", v),
Self::UnsupportedEcCurve(v) => write!(f, "unsupported EC curve: {}", v),
Self::KeyGenFailed(msg) => write!(f, "key generation failed: {}", msg),
Self::CertBuildFailed(msg) => write!(f, "certificate build failed: {}", msg),
Self::KeyboxParseFailed(msg) => write!(f, "keybox parse failed: {}", msg),
Self::AttestationBuildFailed(msg) => write!(f, "attestation build failed: {}", msg),
Self::DerError(e) => write!(f, "DER error: {}", e),
Self::EmptyKeyboxChain => write!(f, "keybox certificate chain is empty"),
Self::ChallengeTooLong(len) => write!(f, "attestation challenge too long: {} bytes (max 128)", len),
Self::InvalidParameter(msg) => write!(f, "invalid parameter: {}", msg),
Self::SigningFailed(msg) => write!(f, "signing failed: {}", msg),
Self::SerializationFailed(msg) => write!(f, "serialization failed: {}", msg),
}
}
}
impl std::error::Error for CertGenError {}
impl From<jni::errors::Error> for CertGenError {
fn from(e: jni::errors::Error) -> Self {
Self::Jni(e.to_string())
}
}
impl From<der::Error> for CertGenError {
fn from(e: der::Error) -> Self {
Self::DerError(e)
}
}
impl From<ring::error::Unspecified> for CertGenError {
fn from(e: ring::error::Unspecified) -> Self {
Self::KeyGenFailed(e.to_string())
}
}
impl From<ring::error::KeyRejected> for CertGenError {
fn from(e: ring::error::KeyRejected) -> Self {
Self::KeyGenFailed(e.to_string())
}
}
impl From<rsa::Error> for CertGenError {
fn from(e: rsa::Error) -> Self {
Self::KeyGenFailed(e.to_string())
}
}
pub type Result<T> = std::result::Result<T, CertGenError>;
-96
View File
@@ -1,96 +0,0 @@
use crate::error::{CertGenError, Result};
use der::{Decode, Encode};
use x509_cert::Certificate;
pub struct ParsedKeybox {
pub signing_key_der: Vec<u8>,
pub issuer_dn_der: Vec<u8>,
pub cert_chain_ders: Vec<Vec<u8>>,
pub leaf_not_after: i64,
}
pub fn parse_keybox(cert_chain_bytes: &[u8], private_key_bytes: &[u8]) -> Result<ParsedKeybox> {
let certs = split_der_certificates(cert_chain_bytes)?;
if certs.is_empty() {
return Err(CertGenError::KeyboxParseFailed("no certificates found".into()));
}
let leaf = Certificate::from_der(&certs[0])
.map_err(|e| CertGenError::KeyboxParseFailed(format!("leaf cert parse: {e}")))?;
let issuer_dn_der = leaf.tbs_certificate.subject.to_der()
.map_err(|e| CertGenError::KeyboxParseFailed(format!("subject DN encode: {e}")))?;
let not_after = leaf.tbs_certificate.validity.not_after;
let leaf_not_after = not_after.to_unix_duration().as_secs() as i64;
Ok(ParsedKeybox {
signing_key_der: private_key_bytes.to_vec(),
issuer_dn_der,
cert_chain_ders: certs,
leaf_not_after,
})
}
fn split_der_certificates(data: &[u8]) -> Result<Vec<Vec<u8>>> {
let mut certs = Vec::new();
let mut offset = 0;
while offset < data.len() {
if data[offset] != 0x30 {
return Err(CertGenError::KeyboxParseFailed(
format!("expected SEQUENCE tag 0x30 at offset {offset}, got 0x{:02x}", data[offset])
));
}
let (content_len, header_len) = parse_der_length(&data[offset + 1..])?;
let total_len = 1 + header_len + content_len;
if offset + total_len > data.len() {
return Err(CertGenError::KeyboxParseFailed(
format!("cert at offset {offset} extends beyond buffer: need {total_len}, have {}", data.len() - offset)
));
}
certs.push(data[offset..offset + total_len].to_vec());
offset += total_len;
}
if certs.is_empty() {
return Err(CertGenError::KeyboxParseFailed("no certificates in chain".into()));
}
Ok(certs)
}
// Returns (content_length, number_of_length_bytes_consumed)
fn parse_der_length(data: &[u8]) -> Result<(usize, usize)> {
if data.is_empty() {
return Err(CertGenError::KeyboxParseFailed("truncated DER length".into()));
}
let first = data[0];
if first < 0x80 {
// Short form: length is the byte itself
return Ok((first as usize, 1));
}
// Long form: low 7 bits = number of subsequent length bytes
let num_bytes = (first & 0x7f) as usize;
if num_bytes == 0 || num_bytes > 4 {
return Err(CertGenError::KeyboxParseFailed(
format!("unsupported DER length encoding: 0x{first:02x}")
));
}
if 1 + num_bytes > data.len() {
return Err(CertGenError::KeyboxParseFailed("truncated multi-byte DER length".into()));
}
let mut len: usize = 0;
for i in 0..num_bytes {
len = (len << 8) | (data[1 + i] as usize);
}
Ok((len, 1 + num_bytes))
}
-59
View File
@@ -1,59 +0,0 @@
use crate::error::{CertGenError, Result};
use crate::types::{Algorithm, EcCurve, GeneratedKeyPair};
pub fn generate_key_pair(
algorithm: Algorithm,
key_size: u32,
ec_curve: Option<EcCurve>,
rsa_public_exponent: u64,
) -> Result<GeneratedKeyPair> {
match algorithm {
Algorithm::Ec => {
let curve = ec_curve.ok_or_else(|| CertGenError::InvalidParameter("ec_curve required for EC".into()))?;
generate_ec_key_pair(curve)
}
Algorithm::Rsa => generate_rsa_key_pair(key_size, rsa_public_exponent),
}
}
fn generate_ec_key_pair(curve: EcCurve) -> Result<GeneratedKeyPair> {
let alg = match curve {
EcCurve::P256 => &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
EcCurve::P384 => &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING,
_ => return Err(CertGenError::UnsupportedEcCurve(curve as i32)),
};
let rng = ring::rand::SystemRandom::new();
let pkcs8_doc = ring::signature::EcdsaKeyPair::generate_pkcs8(alg, &rng)?;
Ok(GeneratedKeyPair {
private_key_pkcs8: pkcs8_doc.as_ref().to_vec(),
})
}
fn generate_rsa_key_pair(key_size: u32, rsa_public_exponent: u64) -> Result<GeneratedKeyPair> {
use pkcs8::EncodePrivateKey;
if !matches!(key_size, 2048 | 3072 | 4096) {
return Err(CertGenError::InvalidParameter(
format!("RSA key size must be 2048, 3072, or 4096; got {key_size}")
));
}
let exp = if rsa_public_exponent == 0 {
rsa::BigUint::from(65537u64)
} else {
rsa::BigUint::from(rsa_public_exponent)
};
let mut rng = rand::thread_rng();
let private_key = rsa::RsaPrivateKey::new_with_exp(&mut rng, key_size as usize, &exp)
.map_err(|e| CertGenError::KeyGenFailed(e.to_string()))?;
let pkcs8_der = private_key.to_pkcs8_der()
.map_err(|e| CertGenError::SerializationFailed(e.to_string()))?;
Ok(GeneratedKeyPair {
private_key_pkcs8: pkcs8_der.as_bytes().to_vec(),
})
}
-342
View File
@@ -1,342 +0,0 @@
#![deny(clippy::unwrap_used, clippy::expect_used)]
mod error;
mod types;
mod keygen;
pub mod keybox;
pub mod attestation;
pub mod certbuilder;
pub mod logging;
use jni::objects::{JByteArray, JClass, JIntArray, JObject, JString};
use jni::sys::{jboolean, jbyteArray, jstring};
use jni::JNIEnv;
use crate::error::{CertGenError, Result};
use crate::types::{Algorithm, CertGenParams, EcCurve};
// ---------------------------------------------------------------------------
// JNI entry: generateAttestedKeyPair
// ---------------------------------------------------------------------------
#[no_mangle]
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_generateAttestedKeyPair(
mut env: JNIEnv,
_class: JClass,
config: JObject,
) -> jbyteArray {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
generate_attested_inner(&mut env, &config)
}));
match result {
Ok(Ok(raw)) => raw,
Ok(Err(e)) => {
tracing::error!(%e, "generateAttestedKeyPair failed");
let _ = env.throw_new(
"java/lang/RuntimeException",
format!("NativeCertGen: {e}"),
);
std::ptr::null_mut()
}
Err(_) => {
tracing::error!("generateAttestedKeyPair panicked");
let _ = env.throw_new(
"java/lang/RuntimeException",
"NativeCertGen: internal panic",
);
std::ptr::null_mut()
}
}
}
fn generate_attested_inner(env: &mut JNIEnv, config: &JObject) -> Result<jbyteArray> {
let params = extract_config(env, config)?;
let key_pair = keygen::generate_key_pair(
params.algorithm,
params.key_size,
params.ec_curve,
params.rsa_public_exponent,
)?;
let keybox = keybox::parse_keybox(&params.keybox_cert_chain, &params.keybox_private_key)?;
let cert_chain = if params.attestation_challenge.is_some() {
let attest_ext = attestation::build_attestation_extension(&params)?;
certbuilder::build_certificate_chain(&key_pair, Some(&attest_ext), &keybox, &params)?
} else {
tracing::info!("no attestation challenge, generating self-signed cert (depth 1)");
certbuilder::build_self_signed_cert(&key_pair, &params)?
};
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
let out = env.byte_array_from_slice(&blob)?;
Ok(out.into_raw())
}
// ---------------------------------------------------------------------------
// JNI entry: initLogging
// ---------------------------------------------------------------------------
#[no_mangle]
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_initLogging(
mut env: JNIEnv,
_class: JClass,
verbose: jboolean,
log_dir: JString,
) -> jboolean {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
init_logging_inner(&mut env, verbose, &log_dir)
}));
match result {
Ok(Ok(())) => 1,
Ok(Err(e)) => {
let _ = env.throw_new(
"java/lang/RuntimeException",
format!("NativeCertGen initLogging: {e}"),
);
0
}
Err(_) => {
let _ = env.throw_new(
"java/lang/RuntimeException",
"NativeCertGen initLogging: internal panic",
);
0
}
}
}
fn init_logging_inner(env: &mut JNIEnv, verbose: jboolean, log_dir: &JString) -> Result<()> {
let dir: String = env.get_string(log_dir)?.into();
logging::init(verbose != 0, &dir, 2, 3)
.map_err(|e| CertGenError::Jni(format!("logging init failed: {e}")))?;
Ok(())
}
// ---------------------------------------------------------------------------
// JNI entry: dumpLogs
// ---------------------------------------------------------------------------
#[no_mangle]
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_dumpLogs(
mut env: JNIEnv,
_class: JClass,
) -> jstring {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
dump_logs_inner(&mut env)
}));
match result {
Ok(Ok(raw)) => raw,
Ok(Err(e)) => {
tracing::error!(%e, "dumpLogs failed");
std::ptr::null_mut()
}
Err(_) => {
tracing::error!("dumpLogs panicked");
std::ptr::null_mut()
}
}
}
fn dump_logs_inner(env: &mut JNIEnv) -> Result<jstring> {
logging::dump::execute_dump()
.map_err(|e| CertGenError::Jni(format!("dump failed: {e}")))?;
// Read the dump path written by execute_dump
let path = std::fs::read_to_string("/data/adb/tricky_store/.dump_path")
.map_err(|e| CertGenError::Jni(format!("read dump path: {e}")))?;
let jpath = env.new_string(&path)?;
Ok(jpath.into_raw())
}
// ---------------------------------------------------------------------------
// Config extraction from Java CertGenConfig object
// ---------------------------------------------------------------------------
fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
let algorithm = get_int(env, config, "algorithm")?;
let key_size = get_int(env, config, "keySize")?;
let ec_curve_raw = get_int(env, config, "ecCurve")?;
let rsa_pub_exp = get_long(env, config, "rsaPublicExponent")?;
let cert_not_before = get_long(env, config, "certNotBefore")?;
let cert_not_after = get_long(env, config, "certNotAfter")?;
let security_level = get_int(env, config, "securityLevel")?;
let attest_version = get_int(env, config, "attestVersion")?;
let keymaster_version = get_int(env, config, "keymasterVersion")?;
let os_version = get_int(env, config, "osVersion")?;
let os_patch_level = get_int(env, config, "osPatchLevel")?;
let vendor_patch_level = get_int(env, config, "vendorPatchLevel")?;
let boot_patch_level = get_int(env, config, "bootPatchLevel")?;
let creation_datetime = get_long(env, config, "creationDatetime")?;
let attestation_challenge = get_nullable_byte_array(env, config, "attestationChallenge")?;
let purposes = get_int_array(env, config, "purposes")?;
let digests = get_int_array(env, config, "digests")?;
let cert_serial = get_nullable_byte_array(env, config, "certSerial")?;
let cert_subject = get_nullable_byte_array(env, config, "certSubject")?;
let keybox_private_key = get_byte_array(env, config, "keyboxPrivateKey")?;
let keybox_cert_chain = get_byte_array(env, config, "keyboxCertChain")?;
let boot_key = get_byte_array(env, config, "bootKey")?;
let boot_hash = get_byte_array(env, config, "bootHash")?;
let attestation_app_id = get_byte_array(env, config, "attestationApplicationId")?;
let module_hash = get_nullable_byte_array(env, config, "moduleHash")?;
let id_brand = get_nullable_byte_array(env, config, "idBrand")?;
let id_device = get_nullable_byte_array(env, config, "idDevice")?;
let id_product = get_nullable_byte_array(env, config, "idProduct")?;
let id_serial = get_nullable_byte_array(env, config, "idSerial")?;
let id_imei = get_nullable_byte_array(env, config, "idImei")?;
let id_meid = get_nullable_byte_array(env, config, "idMeid")?;
let id_manufacturer = get_nullable_byte_array(env, config, "idManufacturer")?;
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,
ec_curve: if algorithm == 3 {
Some(EcCurve::try_from(ec_curve_raw)?)
} else {
None
},
rsa_public_exponent: rsa_pub_exp as u64,
attestation_challenge,
purposes,
digests,
cert_serial,
cert_subject,
cert_not_before,
cert_not_after,
keybox_private_key,
keybox_cert_chain,
security_level,
attest_version,
keymaster_version,
os_version,
os_patch_level,
vendor_patch_level,
boot_patch_level,
boot_key,
boot_hash,
creation_datetime,
attestation_application_id: attestation_app_id,
module_hash,
id_brand,
id_device,
id_product,
id_serial,
id_imei,
id_meid,
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,
})
}
// ---------------------------------------------------------------------------
// JNI field accessor helpers — called 35+ times, justifies the abstraction
// ---------------------------------------------------------------------------
fn get_int(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i32> {
Ok(env.get_field(obj, name, "I")?.i()?)
}
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() {
return Err(CertGenError::NullParam(name));
}
let arr: JByteArray = field.into();
let len = env.get_array_length(&arr)?;
let mut buf = vec![0i8; len as usize];
env.get_byte_array_region(&arr, 0, &mut buf)?;
env.delete_local_ref(arr)?;
Ok(buf.into_iter().map(|b| b as u8).collect())
}
fn get_nullable_byte_array(
env: &mut JNIEnv,
obj: &JObject,
name: &str,
) -> Result<Option<Vec<u8>>> {
let field = env.get_field(obj, name, "[B")?.l()?;
if field.is_null() {
return Ok(None);
}
let arr: JByteArray = field.into();
let len = env.get_array_length(&arr)?;
let mut buf = vec![0i8; len as usize];
env.get_byte_array_region(&arr, 0, &mut buf)?;
env.delete_local_ref(arr)?;
Ok(Some(buf.into_iter().map(|b| b as u8).collect()))
}
fn get_int_array(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<Vec<i32>> {
let field = env.get_field(obj, name, "[I")?.l()?;
if field.is_null() {
return Ok(vec![]);
}
let arr: JIntArray = field.into();
let len = env.get_array_length(&arr)?;
let mut buf = vec![0i32; len as usize];
env.get_int_array_region(&arr, 0, &mut buf)?;
env.delete_local_ref(arr)?;
Ok(buf)
}
// ---------------------------------------------------------------------------
// Binary result assembly (doc 09 section 4.1)
// ---------------------------------------------------------------------------
fn assemble_result(private_key: &[u8], cert_chain: &[Vec<u8>]) -> Vec<u8> {
let total = 4 + private_key.len()
+ 4
+ cert_chain.iter().map(|c| 4 + c.len()).sum::<usize>();
let mut buf = Vec::with_capacity(total);
// Private key segment
buf.extend_from_slice(&(private_key.len() as u32).to_be_bytes());
buf.extend_from_slice(private_key);
// Cert count
buf.extend_from_slice(&(cert_chain.len() as u32).to_be_bytes());
// Each cert: length-prefixed DER
for cert in cert_chain {
buf.extend_from_slice(&(cert.len() as u32).to_be_bytes());
buf.extend_from_slice(cert);
}
buf
}
-208
View File
@@ -1,208 +0,0 @@
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
const DUMP_DIR: &str = "/sdcard/Download";
const LOCK_PATH: &str = "/data/adb/tricky_store/.dump_lock";
const DUMP_PATH_FILE: &str = "/data/adb/tricky_store/.dump_path";
const LOG_DIR: &str = "/data/adb/tricky_store/logs";
const BASE_DIR: &str = "/data/adb/tricky_store";
const LOGCAT_SIZE_LIMIT: usize = 2 * 1024 * 1024;
struct FlockGuard {
_file: File,
}
impl FlockGuard {
fn acquire() -> Result<Self, Box<dyn std::error::Error>> {
if let Some(parent) = Path::new(LOCK_PATH).parent() {
fs::create_dir_all(parent)?;
}
let file = File::create(LOCK_PATH)?;
let fd = {
use std::os::unix::io::AsRawFd;
file.as_raw_fd()
};
let ret = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
if ret != 0 {
return Err("dump already in progress".into());
}
Ok(Self { _file: file })
}
}
impl Drop for FlockGuard {
fn drop(&mut self) {
// flock released automatically when file descriptor closes
}
}
fn random_name(len: usize) -> String {
use rand::Rng;
let mut rng = rand::thread_rng();
(0..len)
.map(|_| {
let idx = rng.gen_range(0..36u8);
if idx < 10 {
(b'0' + idx) as char
} else {
(b'a' + idx - 10) as char
}
})
.collect()
}
fn collect_logcat(tag: &str) -> Vec<u8> {
let output = Command::new("logcat")
.args(["-d", "-s", tag])
.output();
match output {
Ok(o) => {
let mut data = o.stdout;
data.truncate(LOGCAT_SIZE_LIMIT);
data
}
Err(_) => Vec::new(),
}
}
fn collect_device_info() -> String {
let mut info = String::new();
if let Ok(output) = Command::new("uname").arg("-a").output() {
info.push_str(&format!(
"uname={}\n",
String::from_utf8_lossy(&output.stdout).trim()
));
}
for (key, prop) in [
("device", "ro.product.device"),
("build", "ro.build.display.id"),
("android", "ro.build.version.release"),
] {
if let Ok(output) = Command::new("getprop").arg(prop).output() {
info.push_str(&format!(
"{}={}\n",
key,
String::from_utf8_lossy(&output.stdout).trim()
));
}
}
// KSU version
if let Ok(ver) = fs::read_to_string("/data/adb/ksu/version") {
info.push_str(&format!("ksu={}\n", ver.trim()));
}
// Module version from module.prop
if let Ok(prop) = fs::read_to_string("/data/adb/modules/tricky_store/module.prop") {
for line in prop.lines() {
if let Some(ver) = line.strip_prefix("version=") {
info.push_str(&format!("module={}\n", ver.trim()));
break;
}
}
}
info
}
fn read_file_bytes(path: &str) -> Option<Vec<u8>> {
let mut buf = Vec::new();
File::open(path).ok()?.read_to_end(&mut buf).ok()?;
Some(buf)
}
fn epoch_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
pub fn execute_dump() -> Result<(), Box<dyn std::error::Error>> {
let _lock = FlockGuard::acquire()?;
let _ = fs::create_dir_all(DUMP_DIR);
let zip_name = format!("{}.zip", random_name(8));
let zip_path = format!("{}/{}", DUMP_DIR, zip_name);
let zip_file = File::create(&zip_path)?;
let mut zip = zip::ZipWriter::new(zip_file);
let options =
zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
let mut file_count = 0u32;
// Log files
let log_files = [
"certgen.log",
"certgen.log.1",
"certgen.log.2",
"certgen.log.3",
"certgen.log.4",
];
for name in &log_files {
let path = format!("{}/{}", LOG_DIR, name);
if let Some(data) = read_file_bytes(&path) {
zip.start_file(*name, options)?;
zip.write_all(&data)?;
file_count += 1;
}
}
// Logcat
let logcat = collect_logcat("TEESimulator");
if !logcat.is_empty() {
zip.start_file("logcat-teesimulator.log", options)?;
zip.write_all(&logcat)?;
file_count += 1;
}
// Config files
for name in ["tee_status.txt", "security_patch.txt"] {
let path = format!("{}/{}", BASE_DIR, name);
if let Some(data) = read_file_bytes(&path) {
zip.start_file(name, options)?;
zip.write_all(&data)?;
file_count += 1;
}
}
// Device info
let device_info = collect_device_info();
if !device_info.is_empty() {
zip.start_file("device-info.txt", options)?;
zip.write_all(device_info.as_bytes())?;
file_count += 1;
}
// Manifest
let manifest = serde_json::json!({
"timestamp": epoch_millis(),
"version": env!("CARGO_PKG_VERSION"),
"files": file_count,
});
zip.start_file("manifest.json", options)?;
zip.write_all(manifest.to_string().as_bytes())?;
zip.finish()?;
let zip_size = fs::metadata(&zip_path).map(|m| m.len()).unwrap_or(0);
fs::write(DUMP_PATH_FILE, &zip_path)?;
let result = serde_json::json!({
"zip": zip_path,
"size": zip_size,
"files": file_count + 1, // +1 for manifest
});
println!("{}", result);
tracing::info!(path = %zip_path, size = zip_size, "diagnostic dump created");
Ok(())
}
-93
View File
@@ -1,93 +0,0 @@
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::sync::Mutex;
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::layer::Context;
use tracing_subscriber::Layer;
const KMSG_PATH: &str = "/dev/kmsg";
const TAG: &str = "TEESimulator";
pub struct KmsgLayer {
writer: Mutex<Option<File>>,
}
impl KmsgLayer {
pub fn new() -> Self {
let file = OpenOptions::new().write(true).open(KMSG_PATH).ok();
Self {
writer: Mutex::new(file),
}
}
}
fn syslog_priority(level: &Level) -> u8 {
match *level {
Level::ERROR => 3,
Level::WARN => 4,
Level::INFO => 6,
Level::DEBUG | Level::TRACE => 7,
}
}
struct MessageVisitor {
message: String,
fields: String,
}
impl MessageVisitor {
fn new() -> Self {
Self {
message: String::new(),
fields: String::new(),
}
}
}
impl Visit for MessageVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
let raw = format!("{:?}", value);
// Strip surrounding debug quotes if present
self.message = raw
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(&raw)
.to_string();
} else {
if !self.fields.is_empty() {
self.fields.push(' ');
}
self.fields.push_str(&format!("{}={:?}", field.name(), value));
}
}
}
impl<S: Subscriber> Layer<S> for KmsgLayer {
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let mut guard = match self.writer.lock() {
Ok(g) => g,
Err(_) => return,
};
let file = match guard.as_mut() {
Some(f) => f,
None => return,
};
let priority = syslog_priority(event.metadata().level());
let mut visitor = MessageVisitor::new();
event.record(&mut visitor);
let line = if visitor.fields.is_empty() {
format!("<{}>{}: {}\n", priority, TAG, visitor.message)
} else {
format!(
"<{}>{}: {} {}\n",
priority, TAG, visitor.message, visitor.fields
)
};
let _ = file.write_all(line.as_bytes());
}
}
-41
View File
@@ -1,41 +0,0 @@
mod kmsg;
mod rotating;
pub mod sysfs;
pub mod dump;
use std::path::Path;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose";
pub fn init(
verbose_flag: bool,
log_dir: &str,
max_size_mb: u64,
max_files: usize,
) -> Result<(), Box<dyn std::error::Error>> {
let verbose = verbose_flag || Path::new(VERBOSE_MARKER).exists();
let (max_size, max_files) = if verbose {
(5 * 1024 * 1024, 5)
} else {
(max_size_mb * 1024 * 1024, max_files)
};
let level = if verbose { "trace" } else { "info" };
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
let kmsg_layer = kmsg::KmsgLayer::new();
let rotating_layer = rotating::RotatingFileLayer::new(log_dir, max_size, max_files);
let stderr_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);
// Idempotent — second call returns Ok instead of propagating SetGlobalDefaultError
let _ = tracing_subscriber::registry()
.with(filter)
.with(kmsg_layer)
.with(rotating_layer)
.with(stderr_layer)
.try_init();
Ok(())
}
-166
View File
@@ -1,166 +0,0 @@
use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::layer::Context;
use tracing_subscriber::Layer;
struct RotatingState {
dir: PathBuf,
current: Option<File>,
current_size: u64,
max_size: u64,
max_files: usize,
}
pub struct RotatingFileLayer {
state: Mutex<RotatingState>,
}
impl RotatingFileLayer {
pub fn new(dir: &str, max_size: u64, max_files: usize) -> Self {
let dir = PathBuf::from(dir);
let _ = fs::create_dir_all(&dir);
let (file, size) = open_current_log(&dir);
Self {
state: Mutex::new(RotatingState {
dir,
current: file,
current_size: size,
max_size,
max_files,
}),
}
}
}
fn open_current_log(dir: &Path) -> (Option<File>, u64) {
let path = dir.join("certgen.log");
let size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.ok();
(file, size)
}
fn rotate(state: &mut RotatingState) {
// Close current handle before renaming
state.current.take();
let dir = &state.dir;
// Delete the oldest rotated file before shifting
let oldest = dir.join(format!("certgen.log.{}", state.max_files));
if oldest.exists() {
let _ = fs::remove_file(&oldest);
}
// Shift older files up: .{N} -> .{N+1}
for i in (1..state.max_files).rev() {
let from = dir.join(format!("certgen.log.{}", i));
let to = dir.join(format!("certgen.log.{}", i + 1));
if from.exists() {
let _ = fs::rename(&from, &to);
}
}
// Current -> .1
let current_path = dir.join("certgen.log");
let first_rotated = dir.join("certgen.log.1");
if current_path.exists() {
let _ = fs::rename(&current_path, &first_rotated);
}
let (file, size) = open_current_log(dir);
state.current = file;
state.current_size = size;
}
fn epoch_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
fn level_str(level: &Level) -> &'static str {
match *level {
Level::ERROR => "ERROR",
Level::WARN => "WARN",
Level::INFO => "INFO",
Level::DEBUG => "DEBUG",
Level::TRACE => "TRACE",
}
}
struct LogVisitor {
message: String,
fields: String,
}
impl LogVisitor {
fn new() -> Self {
Self {
message: String::new(),
fields: String::new(),
}
}
}
impl Visit for LogVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
let raw = format!("{:?}", value);
self.message = raw
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(&raw)
.to_string();
} else {
if !self.fields.is_empty() {
self.fields.push(' ');
}
self.fields.push_str(&format!("{}={:?}", field.name(), value));
}
}
}
impl<S: Subscriber> Layer<S> for RotatingFileLayer {
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let mut state = match self.state.lock() {
Ok(s) => s,
Err(_) => return,
};
if state.current_size >= state.max_size {
rotate(&mut state);
}
let file = match state.current.as_mut() {
Some(f) => f,
None => return,
};
let ts = epoch_secs();
let lvl = level_str(event.metadata().level());
let target = event.metadata().target();
let mut visitor = LogVisitor::new();
event.record(&mut visitor);
let line = if visitor.fields.is_empty() {
format!("{} [{}] {}: {}\n", ts, lvl, target, visitor.message)
} else {
format!(
"{} [{}] {}: {} {}\n",
ts, lvl, target, visitor.message, visitor.fields
)
};
if file.write_all(line.as_bytes()).is_ok() {
state.current_size += line.len() as u64;
}
}
}
-38
View File
@@ -1,38 +0,0 @@
use std::fs;
use std::path::Path;
const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose";
pub fn is_verbose() -> bool {
Path::new(VERBOSE_MARKER).exists()
}
pub fn set_verbose_marker(enabled: bool) -> Result<(), Box<dyn std::error::Error>> {
if enabled {
if let Some(parent) = Path::new(VERBOSE_MARKER).parent() {
fs::create_dir_all(parent)?;
}
fs::write(VERBOSE_MARKER, "")?;
} else if Path::new(VERBOSE_MARKER).exists() {
fs::remove_file(VERBOSE_MARKER)?;
}
Ok(())
}
pub fn enable() -> Result<(), Box<dyn std::error::Error>> {
set_verbose_marker(true)?;
tracing::info!("verbose logging enabled via marker file");
Ok(())
}
pub fn disable() -> Result<(), Box<dyn std::error::Error>> {
set_verbose_marker(false)?;
tracing::info!("verbose logging disabled, marker file removed");
Ok(())
}
pub fn status() -> Result<(), Box<dyn std::error::Error>> {
let state = if is_verbose() { "enabled" } else { "disabled" };
tracing::info!(verbose = state, "verbose marker status");
Ok(())
}
-100
View File
@@ -1,100 +0,0 @@
use crate::error::CertGenError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum Algorithm {
Rsa = 1,
Ec = 3,
}
impl TryFrom<i32> for Algorithm {
type Error = CertGenError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
1 => Ok(Self::Rsa),
3 => Ok(Self::Ec),
_ => Err(CertGenError::UnsupportedAlgorithm(value)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum EcCurve {
P224 = 0,
P256 = 1,
P384 = 2,
P521 = 3,
Curve25519 = 4,
}
impl TryFrom<i32> for EcCurve {
type Error = CertGenError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::P224),
1 => Ok(Self::P256),
2 => Ok(Self::P384),
3 => Ok(Self::P521),
4 => Ok(Self::Curve25519),
_ => Err(CertGenError::UnsupportedEcCurve(value)),
}
}
}
pub struct CertGenParams {
pub algorithm: Algorithm,
pub key_size: u32,
pub ec_curve: Option<EcCurve>,
pub rsa_public_exponent: u64,
pub attestation_challenge: Option<Vec<u8>>,
pub purposes: Vec<i32>,
pub digests: Vec<i32>,
pub cert_serial: Option<Vec<u8>>,
pub cert_subject: Option<Vec<u8>>,
pub cert_not_before: i64,
pub cert_not_after: i64,
pub keybox_private_key: Vec<u8>,
pub keybox_cert_chain: Vec<u8>,
pub security_level: i32,
pub attest_version: i32,
pub keymaster_version: i32,
pub os_version: i32,
pub os_patch_level: i32,
pub vendor_patch_level: i32,
pub boot_patch_level: i32,
pub boot_key: Vec<u8>,
pub boot_hash: Vec<u8>,
pub creation_datetime: i64,
pub attestation_application_id: Vec<u8>,
pub module_hash: Option<Vec<u8>>,
pub id_brand: Option<Vec<u8>>,
pub id_device: Option<Vec<u8>>,
pub id_product: Option<Vec<u8>>,
pub id_serial: Option<Vec<u8>>,
pub id_imei: Option<Vec<u8>>,
pub id_meid: Option<Vec<u8>>,
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 {
pub private_key_pkcs8: Vec<u8>,
}
-262
View File
@@ -1,262 +0,0 @@
#!/usr/bin/env bash
# Build, package, deploy, and verify TEESimulator module ZIPs.
# Usage: ./scripts/package.sh [flags]
#
# Examples:
# ./scripts/package.sh --release # build release ZIP
# ./scripts/package.sh --all --clean # clean build, both variants
# ./scripts/package.sh --release --deploy --reboot # build, push, install, reboot
# ./scripts/package.sh --deploy --verify # deploy latest ZIP + verify via logcat
# ./scripts/package.sh --rust --release # build Rust crate first, then release
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OUT_DIR="$PROJECT_ROOT/out"
VARIANT=""
CLEAN=false
DEPLOY=false
REBOOT=false
VERIFY=false
BUILD_RUST=false
CLEAR_KEYS=false
TRACE=false
ROOT_PROVIDER="ksu"
red() { printf '\033[0;31m%s\033[0m\n' "$*"; }
green() { printf '\033[0;32m%s\033[0m\n' "$*"; }
yellow() { printf '\033[0;33m%s\033[0m\n' "$*"; }
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
usage() {
cat <<EOF
Usage: $(basename "$0") [options]
Build variants (pick one, or --all):
--release Build release variant (default if none specified)
--debug Build debug variant
--all Build both debug and release
Build options:
--clean Run gradle clean before building
--rust Build native-certgen Rust crate before Gradle
Deploy options:
--deploy Push ZIP to device and install
--reboot Reboot device after install
--clear-keys Clear persistent_keys before deploy
--verify Run logcat verification after deploy
--root PROVIDER Root provider: ksu (default), magisk, apatch
Misc:
-v, --verbose Print every command as it runs (set -x)
--help Show this help
EOF
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
--release) VARIANT="release"; shift ;;
--debug) VARIANT="debug"; shift ;;
--all) VARIANT="all"; shift ;;
--clean) CLEAN=true; shift ;;
--deploy) DEPLOY=true; shift ;;
--reboot) REBOOT=true; shift ;;
--verify) VERIFY=true; shift ;;
--rust) BUILD_RUST=true; shift ;;
--clear-keys) CLEAR_KEYS=true; shift ;;
-v|--verbose) TRACE=true; shift ;;
--root) ROOT_PROVIDER="$2"; shift 2 ;;
--help|-h) usage ;;
*) red "Unknown flag: $1"; usage ;;
esac
done
[[ -z "$VARIANT" ]] && VARIANT="release"
[[ "$TRACE" == true ]] && set -x
case "$ROOT_PROVIDER" in
ksu) INSTALL_CMD="ksud module install" ;;
magisk) INSTALL_CMD="magisk --install-module" ;;
apatch) INSTALL_CMD="/data/adb/apd module install" ;;
*) red "Unknown root provider: $ROOT_PROVIDER"; exit 1 ;;
esac
build_rust() {
local cargo_toml="$PROJECT_ROOT/native-certgen/Cargo.toml"
if [[ ! -f "$cargo_toml" ]]; then
red "native-certgen/Cargo.toml not found — skipping Rust build"
return 0
fi
bold "==> Building native-certgen (aarch64)"
if ! command -v cargo-ndk &>/dev/null; then
red "cargo-ndk not found. Install: cargo install cargo-ndk"
exit 1
fi
(cd "$PROJECT_ROOT/native-certgen" && \
cargo ndk -t arm64-v8a --platform 29 -- build --release)
local so="$PROJECT_ROOT/native-certgen/target/aarch64-linux-android/release/libcertgen.so"
if [[ -f "$so" ]]; then
local size
size=$(du -h "$so" | cut -f1)
green " libcertgen.so built ($size)"
else
red " libcertgen.so not found after build"
exit 1
fi
}
gradle_build() {
local tasks=()
[[ "$CLEAN" == true ]] && tasks+=(clean)
case "$VARIANT" in
release) tasks+=(zipRelease) ;;
debug) tasks+=(zipDebug) ;;
all) tasks+=(zipDebug zipRelease) ;;
esac
bold "==> Gradle: ${tasks[*]}"
(cd "$PROJECT_ROOT" && ./gradlew "${tasks[@]}")
}
find_latest_zip() {
local pattern="$1"
ls -t "$OUT_DIR"/$pattern 2>/dev/null | head -1
}
deploy_zip() {
local zip="$1"
local name
name=$(basename "$zip")
if ! adb get-state &>/dev/null; then
red "No ADB device connected"
exit 1
fi
if [[ "$CLEAR_KEYS" == true ]]; then
bold "==> Clearing persistent_keys"
adb shell "rm -rf /data/adb/tricky_store/persistent_keys/*" 2>/dev/null || true
fi
bold "==> Deploying $name"
adb push "$zip" /data/local/tmp/module.zip
adb shell "su -c '$INSTALL_CMD /data/local/tmp/module.zip'"
green " Installed via $ROOT_PROVIDER"
if [[ "$REBOOT" == true ]]; then
bold "==> Rebooting"
adb reboot
echo " Waiting for device..."
adb wait-for-device
sleep 10
local pid
pid=$(adb shell "pidof TEESimulator" 2>/dev/null || true)
if [[ -n "$pid" ]]; then
green " Daemon alive (PID $pid)"
else
yellow " Daemon not yet started — check logcat"
fi
fi
}
verify_device() {
bold "==> Verification"
if ! adb get-state &>/dev/null; then
red "No ADB device connected"
exit 1
fi
local pid
pid=$(adb shell "pidof TEESimulator" 2>/dev/null || true)
if [[ -n "$pid" ]]; then
green " Daemon: running (PID $pid)"
else
red " Daemon: not running"
fi
local tee_status
tee_status=$(adb shell "cat /data/adb/tricky_store/tee_status.txt" 2>/dev/null || echo "N/A")
echo " TEE status: $tee_status"
local sec_patch
sec_patch=$(adb shell "cat /data/adb/tricky_store/security_patch.txt" 2>/dev/null || echo "N/A")
echo " Security patch config: $(echo "$sec_patch" | head -1)"
local errors
errors=$(adb logcat -d -s TEESimulator 2>/dev/null | \
grep -iE "error|exception" | \
grep -v "StrongBox\|SurfaceRuntime\|ClassLoader\|HARDWARE_TYPE_UNAVAILABLE" | \
wc -l)
if [[ "$errors" -eq 0 ]]; then
green " Logcat errors: 0"
else
yellow " Logcat errors: $errors (run: adb logcat -d -s TEESimulator | grep -iE 'error|exception')"
fi
local throttle_events
throttle_events=$(adb logcat -d -s TEESimulator 2>/dev/null | \
grep -cE "RATE_LIMITED|CONCURRENT_LIMITED" || true)
echo " Rate limit events: $throttle_events"
}
print_summary() {
echo ""
bold "==> Build Summary"
local variants=()
case "$VARIANT" in
release) variants=(Release) ;;
debug) variants=(Debug) ;;
all) variants=(Debug Release) ;;
esac
for v in "${variants[@]}"; do
local zip
zip=$(find_latest_zip "*-${v}.zip")
if [[ -n "$zip" ]]; then
local size
size=$(du -h "$zip" | cut -f1)
green " $v: $(basename "$zip") ($size)"
else
red " $v: ZIP not found"
fi
done
}
# --- Main ---
echo ""
bold "TEESimulator-RS package pipeline"
echo ""
[[ "$BUILD_RUST" == true ]] && build_rust
gradle_build
print_summary
if [[ "$DEPLOY" == true ]]; then
local_variant="$VARIANT"
[[ "$local_variant" == "all" ]] && local_variant="release"
cap="${local_variant^}"
zip=$(find_latest_zip "*-${cap}.zip")
if [[ -z "$zip" ]]; then
red "No $cap ZIP found to deploy"
exit 1
fi
deploy_zip "$zip"
fi
[[ "$VERIFY" == true ]] && verify_device
echo ""
green "Done."
+1 -1
View File
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
}
}
rootProject.name = "TEESimulator-RS"
rootProject.name = "TEESimulator"
include(":stub")
@@ -13,8 +13,6 @@ 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!");
@@ -1,8 +0,0 @@
package android.os;
public class SELinux {
public static boolean checkSELinuxAccess(
String scon, String tcon, String tclass, String perm) {
throw new UnsupportedOperationException("STUB!");
}
}
@@ -17,10 +17,6 @@ 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 +0,0 @@
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;
}
}